From 2b57f1d4365bc23a2682986f9b9d381aa357a5d1 Mon Sep 17 00:00:00 2001 From: fundon Date: Mon, 9 Dec 2024 03:45:15 +0000 Subject: [PATCH] feat(blocks): add event tracking for linked doc (#8876) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes: [BS-2052](https://linear.app/affine-design/issue/BS-2052/增加埋点) --- .../nodes/link-node/link-popup/link-popup.ts | 62 +++- .../reference-node/reference-alias-popup.ts | 29 +- .../nodes/reference-node/reference-popup.ts | 140 +++++--- .../components/src/toolbar/menu-button.ts | 5 +- .../src/services/telemetry-service/index.ts | 1 + .../src/services/telemetry-service/link.ts | 16 + .../telemetry-service/telemetry-service.ts | 14 +- .../embed-card/modal/embed-card-edit-modal.ts | 66 +++- .../components/color-picker/button.ts | 4 +- .../change-embed-card-button.ts | 184 ++++++++--- .../embed-card-toolbar/embed-card-toolbar.ts | 298 +++++++++++------- 11 files changed, 600 insertions(+), 219 deletions(-) create mode 100644 packages/affine/shared/src/services/telemetry-service/link.ts diff --git a/packages/affine/components/src/rich-text/inline/presets/nodes/link-node/link-popup/link-popup.ts b/packages/affine/components/src/rich-text/inline/presets/nodes/link-node/link-popup/link-popup.ts index 4acfcec756b4..d10a7ef3cca7 100644 --- a/packages/affine/components/src/rich-text/inline/presets/nodes/link-node/link-popup/link-popup.ts +++ b/packages/affine/components/src/rich-text/inline/presets/nodes/link-node/link-popup/link-popup.ts @@ -1,14 +1,23 @@ import type { EmbedOptions } from '@blocksuite/affine-shared/types'; import type { InlineRange } from '@blocksuite/inline/types'; -import { EmbedOptionProvider } from '@blocksuite/affine-shared/services'; +import { + EmbedOptionProvider, + type LinkEventType, + type TelemetryEvent, + TelemetryProvider, +} from '@blocksuite/affine-shared/services'; import { getHostName, isValidUrl, normalizeUrl, stopPropagation, } from '@blocksuite/affine-shared/utils'; -import { BLOCK_ID_ATTR, type BlockComponent } from '@blocksuite/block-std'; +import { + BLOCK_ID_ATTR, + type BlockComponent, + type BlockStdScope, +} from '@blocksuite/block-std'; import { WithDisposable } from '@blocksuite/global/utils'; import { computePosition, inline, offset, shift } from '@floating-ui/dom'; import { html, LitElement, nothing } from 'lit'; @@ -75,7 +84,11 @@ export class LinkPopup extends WithDisposable(LitElement) { }; private _edit = () => { + if (!this.host) return; + this.type = 'edit'; + + track(this.host.std, 'OpenedAliasPopup', { control: 'edit' }); }; private _editTemplate = () => { @@ -152,6 +165,24 @@ export class LinkPopup extends WithDisposable(LitElement) { this.abortController.abort(); }; + private _toggleViewSelector = (e: Event) => { + if (!this.host) return; + + const opened = (e as CustomEvent).detail; + if (!opened) return; + + track(this.host.std, 'OpenedViewSelector', { control: 'switch view' }); + }; + + private _trackViewSelected = (type: string) => { + if (!this.host) return; + + track(this.host.std, 'SelectedView', { + control: 'select view', + type: `${type} view`, + }); + }; + private _viewTemplate = () => { if (!this.currentLink) return; @@ -191,7 +222,7 @@ export class LinkPopup extends WithDisposable(LitElement) { `, - this._viewToggleMenu(), + this._viewSelector(), html` `} + @toggle=${this._toggleViewSelector} >
${repeat( @@ -497,7 +531,10 @@ export class LinkPopup extends WithDisposable(LitElement) { data-testid=${`link-to-${type}`} ?data-selected=${type === 'inline'} ?disabled=${type === 'inline'} - @click=${action} + @click=${() => { + action?.(); + this._trackViewSelected(type); + }} > ${label} @@ -637,3 +674,18 @@ export class LinkPopup extends WithDisposable(LitElement) { @property() accessor type: 'create' | 'edit' | 'view' = 'create'; } + +function track( + std: BlockStdScope, + event: LinkEventType, + props: Partial +) { + std.getOptional(TelemetryProvider)?.track(event, { + segment: 'toolbar', + page: 'doc editor', + module: 'link toolbar', + type: 'inline view', + category: 'link', + ...props, + }); +} diff --git a/packages/affine/components/src/rich-text/inline/presets/nodes/reference-node/reference-alias-popup.ts b/packages/affine/components/src/rich-text/inline/presets/nodes/reference-node/reference-alias-popup.ts index d301780534b1..33afb6a1bc33 100644 --- a/packages/affine/components/src/rich-text/inline/presets/nodes/reference-node/reference-alias-popup.ts +++ b/packages/affine/components/src/rich-text/inline/presets/nodes/reference-node/reference-alias-popup.ts @@ -2,8 +2,13 @@ import type { ReferenceInfo } from '@blocksuite/affine-model'; import type { AffineTextAttributes } from '@blocksuite/affine-shared/types'; import type { DeltaInsert, InlineRange } from '@blocksuite/inline'; +import { + type LinkEventType, + type TelemetryEvent, + TelemetryProvider, +} from '@blocksuite/affine-shared/services'; import { FONT_XS, PANEL_BASE } from '@blocksuite/affine-shared/styles'; -import { ShadowlessElement } from '@blocksuite/block-std'; +import { type BlockStdScope, ShadowlessElement } from '@blocksuite/block-std'; import { assertExists, SignalWatcher, @@ -95,6 +100,8 @@ export class ReferenceAliasPopup extends SignalWatcher( this._setTitle(title); + track(this.std, 'SavedAlias', { control: 'save' }); + this.remove(); }; @@ -124,6 +131,8 @@ export class ReferenceAliasPopup extends SignalWatcher( this._setTitle(); + track(this.std, 'ResetedAlias', { control: 'reset' }); + this.remove(); } @@ -255,5 +264,23 @@ export class ReferenceAliasPopup extends SignalWatcher( @query('editor-icon-button.save') accessor saveButton!: EditorIconButton; + @property({ attribute: false }) + accessor std!: BlockStdScope; + accessor title$ = signal(''); } + +function track( + std: BlockStdScope, + event: LinkEventType, + props: Partial +) { + std.getOptional(TelemetryProvider)?.track(event, { + segment: 'toolbar', + page: 'doc editor', + module: 'reference edit popup', + type: 'inline view', + category: 'linked doc', + ...props, + }); +} diff --git a/packages/affine/components/src/rich-text/inline/presets/nodes/reference-node/reference-popup.ts b/packages/affine/components/src/rich-text/inline/presets/nodes/reference-node/reference-popup.ts index 42b7b146893e..e2dabbe0b206 100644 --- a/packages/affine/components/src/rich-text/inline/presets/nodes/reference-node/reference-popup.ts +++ b/packages/affine/components/src/rich-text/inline/presets/nodes/reference-node/reference-popup.ts @@ -1,9 +1,18 @@ import type { ReferenceInfo } from '@blocksuite/affine-model'; import type { InlineRange } from '@blocksuite/inline'; -import { GenerateDocUrlProvider } from '@blocksuite/affine-shared/services'; +import { + GenerateDocUrlProvider, + type LinkEventType, + type TelemetryEvent, + TelemetryProvider, +} from '@blocksuite/affine-shared/services'; import { isInsideBlockByFlavour } from '@blocksuite/affine-shared/utils'; -import { BLOCK_ID_ATTR, type BlockComponent } from '@blocksuite/block-std'; +import { + BLOCK_ID_ATTR, + type BlockComponent, + type BlockStdScope, +} from '@blocksuite/block-std'; import { assertExists, WithDisposable } from '@blocksuite/global/utils'; import { computePosition, inline, offset, shift } from '@floating-ui/dom'; import { effect } from '@preact/signals-core'; @@ -41,6 +50,68 @@ import { cloneReferenceInfoWithoutAliases } from './utils.js'; export class ReferencePopup extends WithDisposable(LitElement) { static override styles = styles; + private _copyLink = () => { + const url = this.std + .getOptional(GenerateDocUrlProvider) + ?.generateDocUrl(this.referenceInfo.pageId, this.referenceInfo.params); + + if (url) { + navigator.clipboard.writeText(url).catch(console.error); + toast(this.std.host, 'Copied link to clipboard'); + } + + this.abortController.abort(); + + track(this.std, 'CopiedLink', { control: 'copy link' }); + }; + + private _openDoc = () => { + this.std + .getOptional(RefNodeSlotsProvider) + ?.docLinkClicked.emit(this.referenceInfo); + }; + + private _openEditPopup = (e: MouseEvent) => { + e.stopPropagation(); + + const { + std, + docTitle, + referenceInfo, + inlineEditor, + targetInlineRange, + abortController, + } = this; + + const aliasPopup = new ReferenceAliasPopup(); + + aliasPopup.std = std; + aliasPopup.docTitle = docTitle; + aliasPopup.referenceInfo = referenceInfo; + aliasPopup.inlineEditor = inlineEditor; + aliasPopup.inlineRange = targetInlineRange; + + document.body.append(aliasPopup); + + abortController.abort(); + + track(std, 'OpenedAliasPopup', { control: 'edit' }); + }; + + private _toggleViewSelector = (e: Event) => { + const opened = (e as CustomEvent).detail; + if (!opened) return; + + track(this.std, 'OpenedViewSelector', { control: 'switch view' }); + }; + + private _trackViewSelected = (type: string) => { + track(this.std, 'SelectedView', { + control: 'select view', + type: `${type} view`, + }); + }; + get _embedViewButtonDisabled() { if ( this.block.doc.readonly || @@ -148,19 +219,6 @@ export class ReferencePopup extends WithDisposable(LitElement) { this.abortController.abort(); } - private _copyLink() { - const url = this.std - .getOptional(GenerateDocUrlProvider) - ?.generateDocUrl(this.referenceInfo.pageId, this.referenceInfo.params); - - if (url) { - navigator.clipboard.writeText(url).catch(console.error); - toast(this.std.host, 'Copied link to clipboard'); - } - - this.abortController.abort(); - } - private _delete() { if (this.inlineEditor.isValidInlineRange(this.targetInlineRange)) { this.inlineEditor.deleteText(this.targetInlineRange); @@ -182,19 +240,13 @@ export class ReferencePopup extends WithDisposable(LitElement) { ]); } - private _openDoc() { - this.std - .getOptional(RefNodeSlotsProvider) - ?.docLinkClicked.emit(this.referenceInfo); - } - private _openMenuButton() { const buttons: MenuItem[] = [ { label: 'Open this doc', type: 'open-this-doc', icon: ExpandFullSmallIcon, - action: () => this._openDoc(), + action: this._openDoc, disabled: this._openButtonDisabled, }, ]; @@ -248,20 +300,7 @@ export class ReferencePopup extends WithDisposable(LitElement) { `; } - private _showAliasPopup() { - const aliasPopup = new ReferenceAliasPopup(); - - aliasPopup.docTitle = this.docTitle; - aliasPopup.referenceInfo = this.referenceInfo; - aliasPopup.inlineEditor = this.inlineEditor; - aliasPopup.inlineRange = this.targetInlineRange; - - document.body.append(aliasPopup); - - this.abortController.abort(); - } - - private _viewToggleMenu() { + private _viewSelector() { // synced doc entry controlled by awareness flag const isSyncedDocEnabled = this.doc.awarenessStore.getFlag( 'enable_synced_doc_block' @@ -306,6 +345,7 @@ export class ReferencePopup extends WithDisposable(LitElement) { ${SmallArrowDownIcon} `} + @toggle=${this._toggleViewSelector} >
${repeat( @@ -317,7 +357,10 @@ export class ReferencePopup extends WithDisposable(LitElement) { data-testid=${`link-to-${type}`} ?data-selected=${type === 'inline'} ?disabled=${disabled || type === 'inline'} - @click=${action} + @click=${() => { + action?.(); + this._trackViewSelected(type); + }} > ${label} @@ -356,7 +399,7 @@ export class ReferencePopup extends WithDisposable(LitElement) { .hover=${false} .labelHeight=${'20px'} .tooltip=${'Original linked doc title'} - @click=${() => this._openDoc()} + @click=${this._openDoc} > ${this.docTitle} @@ -373,7 +416,7 @@ export class ReferencePopup extends WithDisposable(LitElement) { aria-label="Copy link" data-testid="copy-link" .tooltip=${'Copy link'} - @click=${() => this._copyLink()} + @click=${this._copyLink} > ${CopyIcon} @@ -383,13 +426,13 @@ export class ReferencePopup extends WithDisposable(LitElement) { data-testid="edit" .tooltip=${'Edit'} ?disabled=${this.doc.readonly} - @click=${() => this._showAliasPopup()} + @click=${this._openEditPopup} > ${EditIcon} `, - this._viewToggleMenu(), + this._viewSelector(), html` +) { + std.getOptional(TelemetryProvider)?.track(event, { + segment: 'toolbar', + page: 'doc editor', + module: 'reference toolbar', + type: 'inline view', + category: 'linked doc', + ...props, + }); +} diff --git a/packages/affine/components/src/toolbar/menu-button.ts b/packages/affine/components/src/toolbar/menu-button.ts index 972f564f65a7..2d70f92978b6 100644 --- a/packages/affine/components/src/toolbar/menu-button.ts +++ b/packages/affine/components/src/toolbar/menu-button.ts @@ -29,11 +29,12 @@ export class EditorMenuButton extends WithDisposable(LitElement) { this._trigger, this._content, ({ display }) => { - this._trigger.showTooltip = display === 'hidden'; + const opened = display === 'show'; + this._trigger.showTooltip = !opened; this.dispatchEvent( new CustomEvent('toggle', { - detail: display, + detail: opened, bubbles: false, cancelable: false, composed: true, diff --git a/packages/affine/shared/src/services/telemetry-service/index.ts b/packages/affine/shared/src/services/telemetry-service/index.ts index f01af45e4777..1c6241ff04de 100644 --- a/packages/affine/shared/src/services/telemetry-service/index.ts +++ b/packages/affine/shared/src/services/telemetry-service/index.ts @@ -1,3 +1,4 @@ export * from './database.js'; +export * from './link.js'; export * from './telemetry-service.js'; export * from './types.js'; diff --git a/packages/affine/shared/src/services/telemetry-service/link.ts b/packages/affine/shared/src/services/telemetry-service/link.ts new file mode 100644 index 000000000000..7eb14d532d0a --- /dev/null +++ b/packages/affine/shared/src/services/telemetry-service/link.ts @@ -0,0 +1,16 @@ +import type { TelemetryEvent } from './types.js'; + +export type LinkEventType = + | 'CopiedLink' + | 'OpenedAliasPopup' + | 'SavedAlias' + | 'ResetedAlias' + | 'OpenedViewSelector' + | 'SelectedView' + | 'OpenedCaptionEditor' + | 'OpenedCardStyleSelector' + | 'SelectedCardStyle' + | 'OpenedCardScaleSelector' + | 'SelectedCardScale'; + +export type LinkToolbarEvents = Record; diff --git a/packages/affine/shared/src/services/telemetry-service/telemetry-service.ts b/packages/affine/shared/src/services/telemetry-service/telemetry-service.ts index d5ad259bdede..75f7e75673dd 100644 --- a/packages/affine/shared/src/services/telemetry-service/telemetry-service.ts +++ b/packages/affine/shared/src/services/telemetry-service/telemetry-service.ts @@ -1,18 +1,20 @@ import { createIdentifier } from '@blocksuite/global/di'; import type { OutDatabaseAllEvents } from './database.js'; +import type { LinkToolbarEvents } from './link.js'; import type { DocCreatedEvent, ElementCreationEvent, TelemetryEvent, } from './types.js'; -export type TelemetryEventMap = OutDatabaseAllEvents & { - DocCreated: DocCreatedEvent; - LinkedDocCreated: TelemetryEvent; - SplitNote: TelemetryEvent; - CanvasElementAdded: ElementCreationEvent; -}; +export type TelemetryEventMap = OutDatabaseAllEvents & + LinkToolbarEvents & { + DocCreated: DocCreatedEvent; + LinkedDocCreated: TelemetryEvent; + SplitNote: TelemetryEvent; + CanvasElementAdded: ElementCreationEvent; + }; export interface TelemetryService { track( diff --git a/packages/blocks/src/_common/components/embed-card/modal/embed-card-edit-modal.ts b/packages/blocks/src/_common/components/embed-card/modal/embed-card-edit-modal.ts index 7434bc177e62..f5f9b8d7f1ef 100644 --- a/packages/blocks/src/_common/components/embed-card/modal/embed-card-edit-modal.ts +++ b/packages/blocks/src/_common/components/embed-card/modal/embed-card-edit-modal.ts @@ -1,5 +1,9 @@ import type { AliasInfo } from '@blocksuite/affine-model'; -import type { BlockComponent, EditorHost } from '@blocksuite/block-std'; +import type { + BlockComponent, + BlockStdScope, + EditorHost, +} from '@blocksuite/block-std'; import { EmbedLinkedDocBlockComponent, @@ -14,6 +18,11 @@ import { EmbedLinkedDocModel, EmbedSyncedDocModel, } from '@blocksuite/affine-model'; +import { + type LinkEventType, + type TelemetryEvent, + TelemetryProvider, +} from '@blocksuite/affine-shared/services'; import { FONT_SM, FONT_XS } from '@blocksuite/affine-shared/styles'; import { unsafeCSSVarV2 } from '@blocksuite/affine-shared/theme'; import { @@ -150,52 +159,67 @@ export class EmbedCardEditModal extends SignalWatcher( }; private _onReset = () => { + const blockComponent = this._blockComponent; + + if (!blockComponent) { + this.remove(); + return; + } + + const std = blockComponent.std; + this.model.doc.updateBlock(this.model, { title: null, description: null }); - const blockComponent = this._blockComponent; if ( this.isEmbedLinkedDocModel && blockComponent instanceof EmbedLinkedDocBlockComponent ) { - const std = blockComponent.std; - blockComponent.refreshData(); notifyLinkedDocClearedAliases(std); } - blockComponent?.requestUpdate(); + blockComponent.requestUpdate(); + + track(std, this.model, this.viewType, 'ResetedAlias', { control: 'reset' }); this.remove(); }; private _onSave = () => { + const blockComponent = this._blockComponent; + + if (!blockComponent) { + this.remove(); + return; + } + const title = this.title$.value.trim(); if (title.length === 0) { toast(this.host, 'Title can not be empty'); return; } + const std = blockComponent.std; + const description = this.description$.value.trim(); const props: AliasInfo = { title }; if (description) props.description = description; - const blockComponent = this._blockComponent; - if ( this.isEmbedSyncedDocModel && blockComponent instanceof EmbedSyncedDocBlockComponent ) { - const std = blockComponent.std; - blockComponent.convertToCard(props); notifyLinkedDocSwitchedToCard(std); } else { this.model.doc.updateBlock(this.model, props); - blockComponent?.requestUpdate(); + blockComponent.requestUpdate(); } + track(std, this.model, this.viewType, 'SavedAlias', { control: 'save' }); + this.remove(); }; @@ -380,11 +404,15 @@ export class EmbedCardEditModal extends SignalWatcher( @query('.input.title') accessor titleInput!: HTMLInputElement; + + @property({ attribute: false }) + accessor viewType!: string; } export function toggleEmbedCardEditModal( host: EditorHost, embedCardModel: LinkableEmbedModel, + viewType: string, originalDocInfo?: AliasInfo ) { document.body.querySelector('embed-card-edit-modal')?.remove(); @@ -392,6 +420,7 @@ export function toggleEmbedCardEditModal( const embedCardEditModal = new EmbedCardEditModal(); embedCardEditModal.model = embedCardModel; embedCardEditModal.host = host; + embedCardEditModal.viewType = viewType; embedCardEditModal.originalDocInfo = originalDocInfo; document.body.append(embedCardEditModal); } @@ -401,3 +430,20 @@ declare global { 'embed-card-edit-modal': EmbedCardEditModal; } } + +function track( + std: BlockStdScope, + model: LinkableEmbedModel, + viewType: string, + event: LinkEventType, + props: Partial +) { + std.getOptional(TelemetryProvider)?.track(event, { + segment: 'toolbar', + page: 'doc editor', + module: 'embed card edit popup', + type: `${viewType} view`, + category: isInternalEmbedModel(model) ? 'linked doc' : 'link', + ...props, + }); +} diff --git a/packages/blocks/src/root-block/edgeless/components/color-picker/button.ts b/packages/blocks/src/root-block/edgeless/components/color-picker/button.ts index e185f7776903..235c8a267a6e 100644 --- a/packages/blocks/src/root-block/edgeless/components/color-picker/button.ts +++ b/packages/blocks/src/root-block/edgeless/components/color-picker/button.ts @@ -63,8 +63,8 @@ export class EdgelessColorPickerButton extends WithDisposable(LitElement) { override firstUpdated() { this.disposables.addFromEvent(this.menuButton, 'toggle', (e: Event) => { - const newState = (e as CustomEvent).detail; - if (newState === 'hidden' && this.tabType !== 'normal') { + const opened = (e as CustomEvent).detail; + if (!opened && this.tabType !== 'normal') { this.tabType = 'normal'; } }); diff --git a/packages/blocks/src/root-block/widgets/element-toolbar/change-embed-card-button.ts b/packages/blocks/src/root-block/widgets/element-toolbar/change-embed-card-button.ts index b43ccc3c6f17..fb92d7ab4bcd 100644 --- a/packages/blocks/src/root-block/widgets/element-toolbar/change-embed-card-button.ts +++ b/packages/blocks/src/root-block/widgets/element-toolbar/change-embed-card-button.ts @@ -1,3 +1,5 @@ +import type { BlockStdScope } from '@blocksuite/block-std'; + import { getDocContentWithMaxLength } from '@blocksuite/affine-block-embed'; import { CaptionIcon, @@ -23,6 +25,9 @@ import { type EmbedOptions, GenerateDocUrlProvider, type GenerateDocUrlService, + type LinkEventType, + type TelemetryEvent, + TelemetryProvider, ThemeProvider, } from '@blocksuite/affine-shared/services'; import { getHostName } from '@blocksuite/affine-shared/utils'; @@ -231,6 +236,10 @@ export class EdgelessChangeEmbedCardButton extends WithDisposable(LitElement) { navigator.clipboard.writeText(url).catch(console.error); toast(this.std.host, 'Copied link to clipboard'); this.edgeless.service.selection.clear(); + + track(this.std, this.model, this._viewType, 'CopiedLink', { + control: 'copy link', + }); }; private _embedOptions: EmbedOptions | null = null; @@ -250,11 +259,104 @@ export class EdgelessChangeEmbedCardButton extends WithDisposable(LitElement) { this._blockComponent?.open(); }; + private _openEditPopup = (e: MouseEvent) => { + e.stopPropagation(); + + if (isEmbedHtmlBlock(this.model)) return; + + this.std.selection.clear(); + + const originalDocInfo = this._originalDocInfo; + + toggleEmbedCardEditModal( + this.std.host, + this.model, + this._viewType, + originalDocInfo + ); + + track(this.std, this.model, this._viewType, 'OpenedAliasPopup', { + control: 'edit', + }); + }; + private _peek = () => { if (!this._blockComponent) return; peek(this._blockComponent); }; + private _setCardStyle = (style: EmbedCardStyle) => { + const bounds = Bound.deserialize(this.model.xywh); + bounds.w = EMBED_CARD_WIDTH[style]; + bounds.h = EMBED_CARD_HEIGHT[style]; + const xywh = bounds.serialize(); + this.model.doc.updateBlock(this.model, { style, xywh }); + + track(this.std, this.model, this._viewType, 'SelectedCardStyle', { + control: 'select card style', + type: style, + }); + }; + + private _setEmbedScale = (scale: number) => { + if (isEmbedHtmlBlock(this.model)) return; + + const bound = Bound.deserialize(this.model.xywh); + if ('scale' in this.model) { + const oldScale = this.model.scale ?? 1; + const ratio = scale / oldScale; + bound.w *= ratio; + bound.h *= ratio; + const xywh = bound.serialize(); + this.model.doc.updateBlock(this.model, { scale, xywh }); + } else { + bound.h = EMBED_CARD_HEIGHT[this.model.style] * scale; + bound.w = EMBED_CARD_WIDTH[this.model.style] * scale; + const xywh = bound.serialize(); + this.model.doc.updateBlock(this.model, { xywh }); + } + this._embedScale = scale; + + track(this.std, this.model, this._viewType, 'SelectedCardScale', { + control: 'select card scale', + type: `${scale}`, + }); + }; + + private _toggleCardScaleSelector = (e: Event) => { + const opened = (e as CustomEvent).detail; + if (!opened) return; + + track(this.std, this.model, this._viewType, 'OpenedCardScaleSelector', { + control: 'switch card scale', + }); + }; + + private _toggleCardStyleSelector = (e: Event) => { + const opened = (e as CustomEvent).detail; + if (!opened) return; + + track(this.std, this.model, this._viewType, 'OpenedCardStyleSelector', { + control: 'switch card style', + }); + }; + + private _toggleViewSelector = (e: Event) => { + const opened = (e as CustomEvent).detail; + if (!opened) return; + + track(this.std, this.model, this._viewType, 'OpenedViewSelector', { + control: 'switch view', + }); + }; + + private _trackViewSelected = (type: string) => { + track(this.std, this.model, this._viewType, 'SelectedView', { + control: 'select view', + type: `${type} view`, + }); + }; + private get _blockComponent() { const blockSelection = this.edgeless.service.selection.surfaceSelections.filter(sel => @@ -492,51 +594,27 @@ export class EdgelessChangeEmbedCardButton extends WithDisposable(LitElement) { `; } - private _setCardStyle(style: EmbedCardStyle) { - const bounds = Bound.deserialize(this.model.xywh); - bounds.w = EMBED_CARD_WIDTH[style]; - bounds.h = EMBED_CARD_HEIGHT[style]; - const xywh = bounds.serialize(); - this.model.doc.updateBlock(this.model, { style, xywh }); - } - - private _setEmbedScale(scale: number) { - if (isEmbedHtmlBlock(this.model)) return; - - const bound = Bound.deserialize(this.model.xywh); - if ('scale' in this.model) { - const oldScale = this.model.scale ?? 1; - const ratio = scale / oldScale; - bound.w *= ratio; - bound.h *= ratio; - const xywh = bound.serialize(); - this.model.doc.updateBlock(this.model, { scale, xywh }); - } else { - bound.h = EMBED_CARD_HEIGHT[this.model.style] * scale; - bound.w = EMBED_CARD_WIDTH[this.model.style] * scale; - const xywh = bound.serialize(); - this.model.doc.updateBlock(this.model, { xywh }); - } - this._embedScale = scale; - } - private _showCaption() { this._blockComponent?.captionEditor?.show(); + + track(this.std, this.model, this._viewType, 'OpenedCaptionEditor', { + control: 'add caption', + }); } - private _viewToggleMenu() { + private _viewSelector() { if (this._canConvertToEmbedView || this._isEmbedView) { const buttons = [ { type: 'card', label: 'Card view', - handler: () => this._convertToCardView(), + action: () => this._convertToCardView(), disabled: this.model.doc.readonly, }, { type: 'embed', label: 'Embed view', - handler: () => this._convertToEmbedView(), + action: () => this._convertToEmbedView(), disabled: this.model.doc.readonly || this._embedViewButtonDisabled, }, ]; @@ -560,18 +638,22 @@ export class EdgelessChangeEmbedCardButton extends WithDisposable(LitElement) { ${SmallArrowDownIcon} `} + @toggle=${this._toggleViewSelector} >
${repeat( buttons, button => button.type, - ({ type, label, handler, disabled }) => html` + ({ type, label, action, disabled }) => html` { + action(); + this._trackViewSelected(type); + }} > ${label} @@ -653,21 +735,13 @@ export class EdgelessChangeEmbedCardButton extends WithDisposable(LitElement) { .tooltip=${'Edit'} class="change-embed-card-button edit" ?disabled=${this._doc.readonly} - @click=${(e: MouseEvent) => { - e.stopPropagation(); - - this.std.selection.clear(); - - const originalDocInfo = this._originalDocInfo; - - toggleEmbedCardEditModal(this.std.host, model, originalDocInfo); - }} + @click=${this._openEditPopup} > ${EditIcon} `, - this._viewToggleMenu(), + this._viewSelector(), 'style' in model && this._canShowCardStylePanel ? html` @@ -681,12 +755,12 @@ export class EdgelessChangeEmbedCardButton extends WithDisposable(LitElement) { ${PaletteIcon} `} + @toggle=${this._toggleCardStyleSelector} > - this._setCardStyle(value)} + .onSelect=${this._setCardStyle} > @@ -728,11 +802,12 @@ export class EdgelessChangeEmbedCardButton extends WithDisposable(LitElement) { ${SmallArrowDownIcon} `} + @toggle=${this._toggleCardScaleSelector} > this._setEmbedScale(scale)} + .onSelect=${this._setEmbedScale} > `, @@ -772,3 +847,20 @@ export function renderEmbedButton( > `; } + +function track( + std: BlockStdScope, + model: EmbedModel, + viewType: string, + event: LinkEventType, + props: Partial +) { + std.getOptional(TelemetryProvider)?.track(event, { + segment: 'toolbar', + page: 'whiteboard editor', + module: 'element toolbar', + type: `${viewType} view`, + category: isInternalEmbedModel(model) ? 'linked doc' : 'link', + ...props, + }); +} diff --git a/packages/blocks/src/root-block/widgets/embed-card-toolbar/embed-card-toolbar.ts b/packages/blocks/src/root-block/widgets/embed-card-toolbar/embed-card-toolbar.ts index 4251b5ddaebd..d3f1764e1a59 100644 --- a/packages/blocks/src/root-block/widgets/embed-card-toolbar/embed-card-toolbar.ts +++ b/packages/blocks/src/root-block/widgets/embed-card-toolbar/embed-card-toolbar.ts @@ -34,10 +34,13 @@ import { type EmbedOptions, GenerateDocUrlProvider, type GenerateDocUrlService, + type LinkEventType, + type TelemetryEvent, + TelemetryProvider, ThemeProvider, } from '@blocksuite/affine-shared/services'; import { getHostName } from '@blocksuite/affine-shared/utils'; -import { WidgetComponent } from '@blocksuite/block-std'; +import { type BlockStdScope, WidgetComponent } from '@blocksuite/block-std'; import { type BlockModel, DocCollection } from '@blocksuite/store'; import { autoUpdate, computePosition, flip, offset } from '@floating-ui/dom'; import { html, nothing, type TemplateResult } from 'lit'; @@ -81,13 +84,109 @@ export class EmbedCardToolbar extends WidgetComponent< private _abortController = new AbortController(); + private _copyUrl = () => { + const model = this.focusModel; + if (!model) return; + + let url!: ReturnType; + const isInternal = isInternalEmbedModel(model); + + if ('url' in model) { + url = model.url; + } else if (isInternal) { + url = this.std + .getOptional(GenerateDocUrlProvider) + ?.generateDocUrl(model.pageId, model.params); + } + + if (!url) return; + + navigator.clipboard.writeText(url).catch(console.error); + toast(this.std.host, 'Copied link to clipboard'); + + track(this.std, model, this._viewType, 'CopiedLink', { + control: 'copy link', + }); + }; + private _embedOptions: EmbedOptions | null = null; + private _openEditPopup = (e: MouseEvent) => { + e.stopPropagation(); + + const model = this.focusModel; + if (!model || isEmbedHtmlBlock(model)) return; + + const originalDocInfo = this._originalDocInfo; + + this._hide(); + + toggleEmbedCardEditModal(this.host, model, this._viewType, originalDocInfo); + + track(this.std, model, this._viewType, 'OpenedAliasPopup', { + control: 'edit', + }); + }; + private _resetAbortController = () => { this._abortController.abort(); this._abortController = new AbortController(); }; + private _showCaption = () => { + const focusBlock = this.focusBlock; + if (!focusBlock) { + return; + } + try { + focusBlock.captionEditor?.show(); + } catch (_) { + toggleEmbedCardCaptionEditModal(focusBlock); + } + this._resetAbortController(); + + const model = this.focusModel; + if (!model) return; + + track(this.std, model, this._viewType, 'OpenedCaptionEditor', { + control: 'add caption', + }); + }; + + private _toggleCardStyleSelector = (e: Event) => { + const opened = (e as CustomEvent).detail; + if (!opened) return; + + const model = this.focusModel; + if (!model) return; + + track(this.std, model, this._viewType, 'OpenedCardStyleSelector', { + control: 'switch card style', + }); + }; + + private _toggleViewSelector = (e: Event) => { + const opened = (e as CustomEvent).detail; + if (!opened) return; + + const model = this.focusModel; + if (!model) return; + + track(this.std, model, this._viewType, 'OpenedViewSelector', { + control: 'switch view', + }); + }; + + private _trackViewSelected = (type: string) => { + const model = this.focusModel; + if (!model) return; + + track(this.std, model, this._viewType, 'SelectedView', { + control: 'selected view', + type: `${type} view`, + }); + }; + /* * Caches the more menu items. * Currently only supports configuring more menu. @@ -207,67 +306,66 @@ export class EmbedCardToolbar extends WidgetComponent< ); } - private _cardStyleMenuButton() { + private _cardStyleSelector() { + const model = this.focusModel; + + if (!model) return nothing; + if (!this._canShowCardStylePanel(model)) return nothing; + const theme = this.std.get(ThemeProvider).theme; - if (this.focusModel && this._canShowCardStylePanel(this.focusModel)) { - const { EmbedCardHorizontalIcon, EmbedCardListIcon } = - getEmbedCardIcons(theme); - - const buttons = [ - { - type: 'horizontal', - label: 'Large horizontal style', - icon: EmbedCardHorizontalIcon, - }, - { - type: 'list', - label: 'Small horizontal style', - icon: EmbedCardListIcon, - }, - ] as { - type: EmbedCardStyle; - label: string; - icon: TemplateResult<1>; - }[]; - - return html` - - ${PaletteIcon} - - `} - > -
- ${repeat( - buttons, - button => button.type, - ({ type, label, icon }) => html` - this._setEmbedCardStyle(type)} - > - ${icon} - ${label} - - ` - )} -
-
- `; - } + const { EmbedCardHorizontalIcon, EmbedCardListIcon } = + getEmbedCardIcons(theme); + + const buttons = [ + { + type: 'horizontal', + label: 'Large horizontal style', + icon: EmbedCardHorizontalIcon, + }, + { + type: 'list', + label: 'Small horizontal style', + icon: EmbedCardListIcon, + }, + ] as { + type: EmbedCardStyle; + label: string; + icon: TemplateResult<1>; + }[]; - return nothing; + return html` + + ${PaletteIcon} + + `} + @toggle=${this._toggleCardStyleSelector} + > +
+ ${repeat( + buttons, + button => button.type, + ({ type, label, icon }) => html` + this._setEmbedCardStyle(type)} + > + ${icon} + ${label} + + ` + )} +
+
+ `; } private _convertToCardView() { @@ -371,25 +469,6 @@ export class EmbedCardToolbar extends WidgetComponent< doc.deleteBlock(targetModel); } - private _copyUrl() { - if (!this.focusModel) return; - - let url!: ReturnType; - - if ('url' in this.focusModel) { - url = this.focusModel.url; - } else if (isInternalEmbedModel(this.focusModel)) { - url = this.std - .getOptional(GenerateDocUrlProvider) - ?.generateDocUrl(this.focusModel.pageId, this.focusModel.params); - } - - if (!url) return; - - navigator.clipboard.writeText(url).catch(console.error); - toast(this.std.host, 'Copied link to clipboard'); - } - private _hide() { this._resetAbortController(); this.focusBlock = null; @@ -472,9 +551,17 @@ export class EmbedCardToolbar extends WidgetComponent< } private _setEmbedCardStyle(style: EmbedCardStyle) { - this.focusModel?.doc.updateBlock(this.focusModel, { style }); + const model = this.focusModel; + if (!model) return; + + model.doc.updateBlock(model, { style }); this.requestUpdate(); this._abortController.abort(); + + track(this.std, model, this._viewType, 'SelectedCardStyle', { + control: 'select card style', + type: style, + }); } private _show() { @@ -501,19 +588,6 @@ export class EmbedCardToolbar extends WidgetComponent< ); } - private _showCaption() { - const focusBlock = this.focusBlock; - if (!focusBlock) { - return; - } - try { - focusBlock.captionEditor?.show(); - } catch (_) { - toggleEmbedCardCaptionEditModal(focusBlock); - } - this._resetAbortController(); - } - private _turnIntoInlineView() { if (this.focusBlock && 'covertToInline' in this.focusBlock) { this.focusBlock.covertToInline(); @@ -546,7 +620,7 @@ export class EmbedCardToolbar extends WidgetComponent< doc.deleteBlock(targetModel); } - private _viewToggleMenu() { + private _viewSelector() { const buttons = []; buttons.push({ @@ -589,6 +663,7 @@ export class EmbedCardToolbar extends WidgetComponent< ${SmallArrowDownIcon} `} + @toggle=${this._toggleViewSelector} >
${repeat( @@ -602,6 +677,7 @@ export class EmbedCardToolbar extends WidgetComponent< ?disabled=${disabled || this._viewType === type} @click=${() => { action(); + this._trackViewSelected(type); this._hide(); }} > @@ -700,7 +776,7 @@ export class EmbedCardToolbar extends WidgetComponent< aria-label="Copy link" data-testid="copy-link" .tooltip=${'Copy link'} - @click=${() => this._copyUrl()} + @click=${this._copyUrl} > ${CopyIcon} @@ -710,29 +786,22 @@ export class EmbedCardToolbar extends WidgetComponent< data-testid="edit" .tooltip=${'Edit'} ?disabled=${this.doc.readonly} - @click=${(e: MouseEvent) => { - e.stopPropagation(); - const originalDocInfo = this._originalDocInfo; - - this._hide(); - - toggleEmbedCardEditModal(this.host, model, originalDocInfo); - }} + @click=${this._openEditPopup} > ${EditIcon} `, - this._viewToggleMenu(), + this._viewSelector(), - this._cardStyleMenuButton(), + this._cardStyleSelector(), html` this._showCaption()} + @click=${this._showCaption} > ${CaptionIcon} @@ -785,3 +854,20 @@ declare global { [AFFINE_EMBED_CARD_TOOLBAR_WIDGET]: EmbedCardToolbar; } } + +function track( + std: BlockStdScope, + model: EmbedModel, + viewType: string, + event: LinkEventType, + props: Partial +) { + std.getOptional(TelemetryProvider)?.track(event, { + segment: 'toolbar', + page: 'doc editor', + module: 'embed card toolbar', + type: `${viewType} view`, + category: isInternalEmbedModel(model) ? 'linked doc' : 'link', + ...props, + }); +}