import '../_common/components/embed-card/embed-card-caption.js'; import { PathFinder } from '@blockeditor/block-std'; import { assertExists } from '@blockeditor/global/utils'; import { flip, offset } from '@floating-ui/dom'; import { html, nothing } from 'lit'; import { customElement, property, query, state } from 'lit/decorators.js'; import { classMap } from 'lit/directives/class-map.js'; import { ref } from 'lit/directives/ref.js'; import { repeat } from 'lit/directives/repeat.js'; import { styleMap } from 'lit/directives/style-map.js'; import type { EmbedCardCaption } from '../_common/components/embed-card/embed-card-caption.js'; import { HoverController } from '../_common/components/hover/controller.js'; import { EMBED_CARD_HEIGHT, EMBED_CARD_WIDTH } from '../_common/consts.js'; import { EmbedBlockElement } from '../_common/embed-block-helper/embed-block-element.js'; import { OpenIcon } from '../_common/icons/text.js'; import { getEmbedCardIcons } from '../_common/utils/url.js'; import type { EmbedGithubStyles } from './embed-github-model.js'; import { type EmbedGithubModel, githubUrlRegex } from './embed-github-model.js'; import type { EmbedGithubService } from './embed-github-service.js'; import { GithubIcon, styles } from './styles.js'; import { getGithubStatusIcon, refreshEmbedGithubStatus, refreshEmbedGithubUrlData, } from './utils.js'; @customElement('affine-embed-github-block') export class EmbedGithubBlockComponent extends EmbedBlockElement< EmbedGithubModel, EmbedGithubService > { static override styles = styles; override _cardStyle: (typeof EmbedGithubStyles)[number] = 'horizontal'; @state() private _isSelected = false; @property({ attribute: false }) loading = false; @state() showCaption = false; @query('embed-card-caption') captionElement!: EmbedCardCaption; open = () => { let link = this.model.url; if (!link.match(/^[a-zA-Z]+:\/\//)) { link = 'https://' + link; } window.open(link, '_blank'); }; refreshData = () => { refreshEmbedGithubUrlData(this).catch(console.error); }; refreshStatus = () => { refreshEmbedGithubStatus(this).catch(console.error); }; private _selectBlock() { const selectionManager = this.host.selection; const blockSelection = selectionManager.create('block', { path: this.path, }); selectionManager.setGroup('note', [blockSelection]); } private _handleClick() { if (!this.isInSurface) { this._selectBlock(); } } private _handleDoubleClick(event: MouseEvent) { event.stopPropagation(); this.open(); } private _handleAssigneeClick(assignee: string) { const link = `https://www.github.com/${assignee}`; window.open(link, '_blank'); } override connectedCallback() { super.connectedCallback(); if (!!this.model.caption && !!this.model.caption.length) { this.showCaption = true; } if (!this.model.owner || !this.model.repo || !this.model.githubId) { this.page.withoutTransact(() => { const url = this.model.url; const urlMatch = url.match(githubUrlRegex); if (urlMatch) { const [, owner, repo, githubType, githubId] = urlMatch; this.page.updateBlock(this.model, { owner, repo, githubType: githubType === 'issue' ? 'issue' : 'pr', githubId, }); } }); } this.page.withoutTransact(() => { if (!this.model.description && !this.model.title) this.refreshData(); else this.refreshStatus(); }); this.disposables.add( this.model.propsUpdated.on(({ key }) => { this.requestUpdate(); if (key === 'url') this.refreshData(); }) ); this.disposables.add( this.selection.slots.changed.on(sels => { this._isSelected = sels.some(sel => PathFinder.equals(sel.path, this.path) ); }) ); if (this.isInSurface) { const surface = this.surface; assertExists(surface); this.disposables.add( this.model.propsUpdated.on(() => { this.requestUpdate(); }) ); } } private _whenHover = new HoverController(this, ({ abortController }) => { const selection = this.host.selection; const textSelection = selection.find('text'); if ( !!textSelection && (!!textSelection.to || !!textSelection.from.length) ) { return null; } const blockSelections = selection.filter('block'); if ( blockSelections.length > 1 || (blockSelections.length === 1 && blockSelections[0].path !== this.path) ) { return null; } return { template: html` `, computePosition: { referenceElement: this, placement: 'top-end', middleware: [flip(), offset(4)], autoUpdate: true, }, }; }); override renderBlock() { const { title = 'GitHub', githubType, status, statusReason, owner, repo, createdAt, assignees, description, image, style, } = this.model; this._cardStyle = style; this._width = EMBED_CARD_WIDTH[this._cardStyle]; this._height = EMBED_CARD_HEIGHT[this._cardStyle]; const loading = this.loading; const { LoadingIcon, EmbedCardBannerIcon } = getEmbedCardIcons(); const titleIcon = loading ? LoadingIcon : GithubIcon; const statusIcon = status ? getGithubStatusIcon(githubType, status, statusReason) : nothing; const statusText = loading ? '' : status; const titleText = loading ? 'Loading...' : title; const descriptionText = loading ? '' : description; const bannerImage = !loading && image ? html` ${EmbedCardBannerIcon} ` : EmbedCardBannerIcon; let dateText = ''; if (createdAt) { const date = new Date(createdAt); dateText = date.toLocaleDateString('en-US', { month: 'short', day: 'numeric', }); const day = date.getDate(); const suffix = ['th', 'st', 'nd', 'rd'][((day / 10) | 0) !== 1 ? day % 10 : 4] || 'th'; dateText = dateText.replace(/\d+/, `${day}${suffix}`); } return this.renderEmbed( () => html`
${titleIcon}
${status && statusText ? html`
${statusIcon} ${statusText}
` : nothing}
${titleText}
${descriptionText}
${githubType === 'issue' && assignees ? html`
Assignees
${assignees.length === 0 ? html`No one` : repeat( assignees, assignee => assignee, (assignee, index) => html` this._handleAssigneeClick(assignee)} >${`@${assignee}`} ${index === assignees.length - 1 ? '' : `, `}` )}
` : nothing}
${`${owner}/${repo} |`} ${createdAt ? html`` : nothing} github.com
${OpenIcon}
${bannerImage}
{ if (!this.model.caption) this.showCaption = false; }} > ${this.selected?.is('block') ? html`` : nothing}
` ); } } declare global { interface HTMLElementTagNameMap { 'affine-embed-github-block': EmbedGithubBlockComponent; } }