feat(editor): edgeless page block toolbar (#9707)
Close [BS-2315](https://linear.app/affine-design/issue/BS-2315/page-block-header) ### What Changes - Add header toolbar to page block (the first note in canvas) - Add e2e tests - Add some edgeless e2e test utils. **The package `@blocksuite/affine` was added to `"@affine-test/kit"`**
This commit is contained in:
14
blocksuite/affine/block-note/src/config.ts
Normal file
14
blocksuite/affine/block-note/src/config.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import type { NoteBlockModel } from '@blocksuite/affine-model';
|
||||||
|
import { type BlockStdScope, ConfigExtension } from '@blocksuite/block-std';
|
||||||
|
import type { TemplateResult } from 'lit';
|
||||||
|
|
||||||
|
export type NoteConfig = {
|
||||||
|
edgelessNoteHeader: (context: {
|
||||||
|
note: NoteBlockModel;
|
||||||
|
std: BlockStdScope;
|
||||||
|
}) => TemplateResult;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function NoteConfigExtension(config: NoteConfig) {
|
||||||
|
return ConfigExtension('affine:note', config);
|
||||||
|
}
|
||||||
@@ -12,6 +12,7 @@ import type { indentBlock } from './commands/indent-block';
|
|||||||
import type { indentBlocks } from './commands/indent-blocks';
|
import type { indentBlocks } from './commands/indent-blocks';
|
||||||
import type { selectBlock } from './commands/select-block';
|
import type { selectBlock } from './commands/select-block';
|
||||||
import type { selectBlocksBetween } from './commands/select-blocks-between';
|
import type { selectBlocksBetween } from './commands/select-blocks-between';
|
||||||
|
import type { NoteConfig } from './config';
|
||||||
import { NoteBlockComponent } from './note-block';
|
import { NoteBlockComponent } from './note-block';
|
||||||
import {
|
import {
|
||||||
EdgelessNoteBlockComponent,
|
EdgelessNoteBlockComponent,
|
||||||
@@ -48,5 +49,8 @@ declare global {
|
|||||||
interface BlockServices {
|
interface BlockServices {
|
||||||
'affine:note': NoteBlockService;
|
'affine:note': NoteBlockService;
|
||||||
}
|
}
|
||||||
|
interface BlockConfigs {
|
||||||
|
'affine:note': NoteConfig;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
export * from './adapters';
|
export * from './adapters';
|
||||||
export * from './commands';
|
export * from './commands';
|
||||||
|
export * from './config';
|
||||||
export * from './note-block';
|
export * from './note-block';
|
||||||
export * from './note-edgeless-block';
|
export * from './note-edgeless-block';
|
||||||
export * from './note-service';
|
export * from './note-service';
|
||||||
|
|||||||
@@ -7,7 +7,10 @@ import {
|
|||||||
StrokeStyle,
|
StrokeStyle,
|
||||||
} from '@blocksuite/affine-model';
|
} from '@blocksuite/affine-model';
|
||||||
import { EDGELESS_BLOCK_CHILD_PADDING } from '@blocksuite/affine-shared/consts';
|
import { EDGELESS_BLOCK_CHILD_PADDING } from '@blocksuite/affine-shared/consts';
|
||||||
import { ThemeProvider } from '@blocksuite/affine-shared/services';
|
import {
|
||||||
|
FeatureFlagService,
|
||||||
|
ThemeProvider,
|
||||||
|
} from '@blocksuite/affine-shared/services';
|
||||||
import {
|
import {
|
||||||
getClosestBlockComponentByPoint,
|
getClosestBlockComponentByPoint,
|
||||||
handleNativeRangeAtPoint,
|
handleNativeRangeAtPoint,
|
||||||
@@ -77,7 +80,7 @@ export class EdgelessNoteMask extends WithDisposable(ShadowlessElement) {
|
|||||||
bottom: `${-extra}px`,
|
bottom: `${-extra}px`,
|
||||||
right: `${-extra}px`,
|
right: `${-extra}px`,
|
||||||
zIndex: '1',
|
zIndex: '1',
|
||||||
pointerEvents: this.display ? 'auto' : 'none',
|
pointerEvents: this.editing ? 'none' : 'auto',
|
||||||
borderRadius: `${
|
borderRadius: `${
|
||||||
this.model.edgeless.style.borderRadius * this.zoom
|
this.model.edgeless.style.borderRadius * this.zoom
|
||||||
}px`,
|
}px`,
|
||||||
@@ -86,9 +89,6 @@ export class EdgelessNoteMask extends WithDisposable(ShadowlessElement) {
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@property({ attribute: false })
|
|
||||||
accessor display!: boolean;
|
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
accessor editing!: boolean;
|
accessor editing!: boolean;
|
||||||
|
|
||||||
@@ -128,9 +128,6 @@ export class EdgelessNoteBlockComponent extends toGfxBlockComponent(
|
|||||||
.edgeless-note-collapse-button.flip {
|
.edgeless-note-collapse-button.flip {
|
||||||
transform: translateX(-50%) rotate(180deg);
|
transform: translateX(-50%) rotate(180deg);
|
||||||
}
|
}
|
||||||
.edgeless-note-collapse-button.hide {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.edgeless-note-container:has(.affine-embed-synced-doc-container.editing)
|
.edgeless-note-container:has(.affine-embed-synced-doc-container.editing)
|
||||||
> .note-background {
|
> .note-background {
|
||||||
@@ -156,8 +153,16 @@ export class EdgelessNoteBlockComponent extends toGfxBlockComponent(
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
private get _enablePageHeader() {
|
||||||
|
return this.std.get(FeatureFlagService).getFlag('enable_page_block_header');
|
||||||
|
}
|
||||||
|
|
||||||
private get _isShowCollapsedContent() {
|
private get _isShowCollapsedContent() {
|
||||||
return this.model.edgeless.collapse && (this._isResizing || this._isHover);
|
return (
|
||||||
|
this.model.edgeless.collapse &&
|
||||||
|
this.gfx.selection.has(this.model.id) &&
|
||||||
|
(this._isResizing || this._isHover)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
get _zoom() {
|
get _zoom() {
|
||||||
@@ -240,12 +245,28 @@ export class EdgelessNoteBlockComponent extends toGfxBlockComponent(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _isFirstNote() {
|
||||||
|
return (
|
||||||
|
this.model.parent?.children.find(child =>
|
||||||
|
matchFlavours(child, ['affine:note'])
|
||||||
|
) === this.model
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
private _leaved() {
|
private _leaved() {
|
||||||
if (this._isHover) {
|
if (this._isHover) {
|
||||||
this._isHover = false;
|
this._isHover = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _renderHeader() {
|
||||||
|
const header = this.host.std
|
||||||
|
.getConfig('affine:note')
|
||||||
|
?.edgelessNoteHeader({ note: this.model, std: this.std });
|
||||||
|
|
||||||
|
return header;
|
||||||
|
}
|
||||||
|
|
||||||
private _setCollapse(event: MouseEvent) {
|
private _setCollapse(event: MouseEvent) {
|
||||||
event.stopImmediatePropagation();
|
event.stopImmediatePropagation();
|
||||||
|
|
||||||
@@ -466,7 +487,9 @@ export class EdgelessNoteBlockComponent extends toGfxBlockComponent(
|
|||||||
style=${styleMap(backgroundStyle)}
|
style=${styleMap(backgroundStyle)}
|
||||||
@pointerdown=${stopPropagation}
|
@pointerdown=${stopPropagation}
|
||||||
@click=${this._handleClickAtBackground}
|
@click=${this._handleClickAtBackground}
|
||||||
></div>
|
>
|
||||||
|
${this._enablePageHeader ? this._renderHeader() : nothing}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="edgeless-note-page-content"
|
class="edgeless-note-page-content"
|
||||||
@@ -479,12 +502,18 @@ export class EdgelessNoteBlockComponent extends toGfxBlockComponent(
|
|||||||
${this.renderPageContent()}
|
${this.renderPageContent()}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
${isCollapsable
|
<edgeless-note-mask
|
||||||
|
.model=${this.model}
|
||||||
|
.host=${this.host}
|
||||||
|
.zoom=${this.gfx.viewport.zoom ?? 1}
|
||||||
|
.editing=${this._editing}
|
||||||
|
></edgeless-note-mask>
|
||||||
|
|
||||||
|
${isCollapsable && (!this._isFirstNote() || !this._enablePageHeader)
|
||||||
? html`<div
|
? html`<div
|
||||||
class="${classMap({
|
class="${classMap({
|
||||||
'edgeless-note-collapse-button': true,
|
'edgeless-note-collapse-button': true,
|
||||||
flip: isCollapseArrowUp,
|
flip: isCollapseArrowUp,
|
||||||
hide: this._isSelected,
|
|
||||||
})}"
|
})}"
|
||||||
style=${styleMap({
|
style=${styleMap({
|
||||||
bottom: this._editing ? `${-extra}px` : '0',
|
bottom: this._editing ? `${-extra}px` : '0',
|
||||||
@@ -497,14 +526,6 @@ export class EdgelessNoteBlockComponent extends toGfxBlockComponent(
|
|||||||
</div>`
|
</div>`
|
||||||
: nothing}
|
: nothing}
|
||||||
${this._collapsedContent()}
|
${this._collapsedContent()}
|
||||||
|
|
||||||
<edgeless-note-mask
|
|
||||||
.model=${this.model}
|
|
||||||
.display=${!this._editing}
|
|
||||||
.host=${this.host}
|
|
||||||
.zoom=${this.gfx.viewport.zoom ?? 1}
|
|
||||||
.editing=${this._editing}
|
|
||||||
></edgeless-note-mask>
|
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@@ -518,9 +539,6 @@ export class EdgelessNoteBlockComponent extends toGfxBlockComponent(
|
|||||||
@state()
|
@state()
|
||||||
private accessor _isResizing = false;
|
private accessor _isResizing = false;
|
||||||
|
|
||||||
@state()
|
|
||||||
private accessor _isSelected = false;
|
|
||||||
|
|
||||||
@state()
|
@state()
|
||||||
private accessor _noteFullHeight = 0;
|
private accessor _noteFullHeight = 0;
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ export interface BlockSuiteFlags {
|
|||||||
enable_shape_shadow_blur: boolean;
|
enable_shape_shadow_blur: boolean;
|
||||||
enable_mobile_keyboard_toolbar: boolean;
|
enable_mobile_keyboard_toolbar: boolean;
|
||||||
enable_mobile_linked_doc_menu: boolean;
|
enable_mobile_linked_doc_menu: boolean;
|
||||||
|
enable_page_block_header: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class FeatureFlagService extends StoreExtension {
|
export class FeatureFlagService extends StoreExtension {
|
||||||
@@ -40,6 +41,7 @@ export class FeatureFlagService extends StoreExtension {
|
|||||||
enable_shape_shadow_blur: false,
|
enable_shape_shadow_blur: false,
|
||||||
enable_mobile_keyboard_toolbar: false,
|
enable_mobile_keyboard_toolbar: false,
|
||||||
enable_mobile_linked_doc_menu: false,
|
enable_mobile_linked_doc_menu: false,
|
||||||
|
enable_page_block_header: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
setFlag(key: keyof BlockSuiteFlags, value: boolean) {
|
setFlag(key: keyof BlockSuiteFlags, value: boolean) {
|
||||||
|
|||||||
@@ -145,6 +145,18 @@ export class EdgelessChangeNoteButton extends WithDisposable(LitElement) {
|
|||||||
return this.edgeless.doc;
|
return this.edgeless.doc;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private get _enableAutoHeight() {
|
||||||
|
return !(
|
||||||
|
this.edgeless.doc
|
||||||
|
.get(FeatureFlagService)
|
||||||
|
.getFlag('enable_page_block_header') &&
|
||||||
|
this.notes.length === 1 &&
|
||||||
|
this.notes[0].parent?.children.find(child =>
|
||||||
|
matchFlavours(child, ['affine:note'])
|
||||||
|
) === this.notes[0]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
private _getScaleLabel(scale: number) {
|
private _getScaleLabel(scale: number) {
|
||||||
return Math.round(scale * 100) + '%';
|
return Math.round(scale * 100) + '%';
|
||||||
}
|
}
|
||||||
@@ -434,15 +446,18 @@ export class EdgelessChangeNoteButton extends WithDisposable(LitElement) {
|
|||||||
|
|
||||||
onlyOne ? this.quickConnectButton : nothing,
|
onlyOne ? this.quickConnectButton : nothing,
|
||||||
|
|
||||||
html`
|
this._enableAutoHeight
|
||||||
<editor-icon-button
|
? html`<editor-icon-button
|
||||||
aria-label="Size"
|
aria-label="Size"
|
||||||
.tooltip=${collapse ? 'Auto height' : 'Customized height'}
|
data-testid="edgeless-note-auto-height"
|
||||||
@click=${() => this._setCollapse()}
|
.tooltip=${collapse ? 'Auto height' : 'Customized height'}
|
||||||
>
|
@click=${() => this._setCollapse()}
|
||||||
${collapse ? ExpandIcon : ShrinkIcon}
|
>
|
||||||
</editor-icon-button>
|
${collapse ? ExpandIcon : ShrinkIcon}
|
||||||
|
</editor-icon-button>`
|
||||||
|
: nothing,
|
||||||
|
|
||||||
|
html`
|
||||||
<editor-menu-button
|
<editor-menu-button
|
||||||
${ref(this._scalePanelRef)}
|
${ref(this._scalePanelRef)}
|
||||||
.contentPadding=${'8px'}
|
.contentPadding=${'8px'}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
export { EDGELESS_TOOLBAR_WIDGET } from '../edgeless/components/toolbar/edgeless-toolbar.js';
|
||||||
export {
|
export {
|
||||||
AFFINE_AI_PANEL_WIDGET,
|
AFFINE_AI_PANEL_WIDGET,
|
||||||
AffineAIPanelWidget,
|
AffineAIPanelWidget,
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ import {
|
|||||||
patchEdgelessClipboard,
|
patchEdgelessClipboard,
|
||||||
patchForAttachmentEmbedViews,
|
patchForAttachmentEmbedViews,
|
||||||
patchForClipboardInElectron,
|
patchForClipboardInElectron,
|
||||||
|
patchForEdgelessNoteConfig,
|
||||||
patchForMobile,
|
patchForMobile,
|
||||||
patchForSharedPage,
|
patchForSharedPage,
|
||||||
patchGenerateDocUrlExtension,
|
patchGenerateDocUrlExtension,
|
||||||
@@ -159,6 +160,7 @@ const usePatchSpecs = (shared: boolean, mode: DocMode) => {
|
|||||||
patched = patched.concat(patchForAttachmentEmbedViews(reactToLit));
|
patched = patched.concat(patchForAttachmentEmbedViews(reactToLit));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
patched = patched.concat(patchForEdgelessNoteConfig(reactToLit));
|
||||||
patched = patched.concat(patchNotificationService(confirmModal));
|
patched = patched.concat(patchNotificationService(confirmModal));
|
||||||
patched = patched.concat(patchPeekViewService(peekViewService));
|
patched = patched.concat(patchPeekViewService(peekViewService));
|
||||||
patched = patched.concat(patchOpenDocExtension());
|
patched = patched.concat(patchOpenDocExtension());
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ import {
|
|||||||
GenerateDocUrlExtension,
|
GenerateDocUrlExtension,
|
||||||
MobileSpecsPatches,
|
MobileSpecsPatches,
|
||||||
NativeClipboardExtension,
|
NativeClipboardExtension,
|
||||||
|
NoteConfigExtension,
|
||||||
NotificationExtension,
|
NotificationExtension,
|
||||||
OpenDocExtension,
|
OpenDocExtension,
|
||||||
ParseDocUrlExtension,
|
ParseDocUrlExtension,
|
||||||
@@ -83,6 +84,7 @@ import { pick } from 'lodash-es';
|
|||||||
import type { DocProps } from '../../../../../blocksuite/initialization';
|
import type { DocProps } from '../../../../../blocksuite/initialization';
|
||||||
import { AttachmentEmbedPreview } from '../../../../attachment-viewer/pdf-viewer-embedded';
|
import { AttachmentEmbedPreview } from '../../../../attachment-viewer/pdf-viewer-embedded';
|
||||||
import { generateUrl } from '../../../../hooks/affine/use-share-url';
|
import { generateUrl } from '../../../../hooks/affine/use-share-url';
|
||||||
|
import { EdgelessNoteHeader } from './widgets/edgeless-note-header';
|
||||||
import { createKeyboardToolbarConfig } from './widgets/keyboard-toolbar';
|
import { createKeyboardToolbarConfig } from './widgets/keyboard-toolbar';
|
||||||
|
|
||||||
export type ReferenceReactRenderer = (
|
export type ReferenceReactRenderer = (
|
||||||
@@ -654,3 +656,12 @@ export function patchForClipboardInElectron(framework: FrameworkProvider) {
|
|||||||
copyAsPNG: desktopApi.handler.clipboard.copyAsPNG,
|
copyAsPNG: desktopApi.handler.clipboard.copyAsPNG,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function patchForEdgelessNoteConfig(
|
||||||
|
reactToLit: (element: ElementOrFactory) => TemplateResult
|
||||||
|
) {
|
||||||
|
return NoteConfigExtension({
|
||||||
|
edgelessNoteHeader: ({ note }) =>
|
||||||
|
reactToLit(<EdgelessNoteHeader note={note} />),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||||
|
import { style } from '@vanilla-extract/css';
|
||||||
|
|
||||||
|
const headerPadding = 8;
|
||||||
|
export const header = style({
|
||||||
|
position: 'relative',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 4,
|
||||||
|
padding: headerPadding,
|
||||||
|
zIndex: 2, // should have higher z-index than the note mask
|
||||||
|
pointerEvents: 'none',
|
||||||
|
});
|
||||||
|
|
||||||
|
export const title = style({
|
||||||
|
flex: 1,
|
||||||
|
color: cssVarV2('text/primary'),
|
||||||
|
fontFamily: 'Inter',
|
||||||
|
fontWeight: 600,
|
||||||
|
lineHeight: '30px',
|
||||||
|
});
|
||||||
|
|
||||||
|
export const iconSize = 24;
|
||||||
|
const buttonPadding = 4;
|
||||||
|
export const button = style({
|
||||||
|
padding: buttonPadding,
|
||||||
|
pointerEvents: 'auto',
|
||||||
|
});
|
||||||
|
|
||||||
|
export const headerHeight = 2 * headerPadding + iconSize + 2 * buttonPadding;
|
||||||
@@ -0,0 +1,178 @@
|
|||||||
|
import { IconButton } from '@affine/component';
|
||||||
|
import { useSharingUrl } from '@affine/core/components/hooks/affine/use-share-url';
|
||||||
|
import { WorkspaceDialogService } from '@affine/core/modules/dialogs';
|
||||||
|
import { EditorService } from '@affine/core/modules/editor';
|
||||||
|
import { FeatureFlagService } from '@affine/core/modules/feature-flag';
|
||||||
|
import { useInsidePeekView } from '@affine/core/modules/peek-view/view/modal-container';
|
||||||
|
import { WorkspaceService } from '@affine/core/modules/workspace';
|
||||||
|
import { useI18n } from '@affine/i18n';
|
||||||
|
import { track } from '@affine/track';
|
||||||
|
import { matchFlavours, type NoteBlockModel } from '@blocksuite/affine/blocks';
|
||||||
|
import { Bound } from '@blocksuite/affine/global/utils';
|
||||||
|
import { GfxControllerIdentifier } from '@blocksuite/block-std/gfx';
|
||||||
|
import {
|
||||||
|
ExpandFullIcon,
|
||||||
|
InformationIcon,
|
||||||
|
LinkIcon,
|
||||||
|
ToggleDownIcon,
|
||||||
|
ToggleRightIcon,
|
||||||
|
} from '@blocksuite/icons/rc';
|
||||||
|
import { useLiveData, useService, useServices } from '@toeverything/infra';
|
||||||
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
import * as styles from './edgeless-note-header.css';
|
||||||
|
|
||||||
|
const EdgelessNoteToggleButton = ({ note }: { note: NoteBlockModel }) => {
|
||||||
|
const t = useI18n();
|
||||||
|
const [collapsed, setCollapsed] = useState(note.edgeless.collapse);
|
||||||
|
const editor = useService(EditorService).editor;
|
||||||
|
const editorContainer = useLiveData(editor.editorContainer$);
|
||||||
|
const gfx = editorContainer?.std.get(GfxControllerIdentifier);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setCollapsed(note.edgeless.collapse);
|
||||||
|
}, [note.edgeless.collapse]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!gfx) return;
|
||||||
|
|
||||||
|
const { selection } = gfx;
|
||||||
|
|
||||||
|
const dispose = selection.slots.updated.on(() => {
|
||||||
|
if (selection.has(note.id) && selection.editing) {
|
||||||
|
note.doc.transact(() => {
|
||||||
|
note.edgeless.collapse = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => dispose.dispose();
|
||||||
|
}, [gfx, note]);
|
||||||
|
|
||||||
|
const toggle = useCallback(() => {
|
||||||
|
note.doc.transact(() => {
|
||||||
|
if (collapsed) {
|
||||||
|
note.edgeless.collapse = false;
|
||||||
|
} else {
|
||||||
|
const bound = Bound.deserialize(note.xywh);
|
||||||
|
bound.h = styles.headerHeight * (note.edgeless.scale ?? 1);
|
||||||
|
note.xywh = bound.serialize();
|
||||||
|
note.edgeless.collapse = true;
|
||||||
|
note.edgeless.collapsedHeight = styles.headerHeight;
|
||||||
|
gfx?.selection.clear();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, [collapsed, gfx, note]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<IconButton
|
||||||
|
className={styles.button}
|
||||||
|
size={styles.iconSize}
|
||||||
|
tooltip={t['com.affine.editor.edgeless-note-header.fold-page-block']()}
|
||||||
|
data-testid="edgeless-note-toggle-button"
|
||||||
|
onClick={toggle}
|
||||||
|
>
|
||||||
|
{collapsed ? <ToggleRightIcon /> : <ToggleDownIcon />}
|
||||||
|
</IconButton>
|
||||||
|
<div className={styles.title} data-testid="edgeless-note-title">
|
||||||
|
{collapsed && (note.doc.meta?.title ?? 'Untitled')}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ExpandFullButton = () => {
|
||||||
|
const t = useI18n();
|
||||||
|
const editor = useService(EditorService).editor;
|
||||||
|
|
||||||
|
const expand = useCallback(() => {
|
||||||
|
editor.setMode('page');
|
||||||
|
}, [editor]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<IconButton
|
||||||
|
className={styles.button}
|
||||||
|
size={styles.iconSize}
|
||||||
|
tooltip={t['com.affine.editor.edgeless-note-header.view-in-page']()}
|
||||||
|
data-testid="edgeless-note-expand-button"
|
||||||
|
onClick={expand}
|
||||||
|
>
|
||||||
|
<ExpandFullIcon />
|
||||||
|
</IconButton>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const InfoButton = ({ note }: { note: NoteBlockModel }) => {
|
||||||
|
const t = useI18n();
|
||||||
|
const workspaceDialogService = useService(WorkspaceDialogService);
|
||||||
|
|
||||||
|
const onOpenInfoModal = useCallback(() => {
|
||||||
|
track.doc.editor.pageBlockHeader.openDocInfo();
|
||||||
|
workspaceDialogService.open('doc-info', { docId: note.doc.id });
|
||||||
|
}, [note.doc.id, workspaceDialogService]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<IconButton
|
||||||
|
className={styles.button}
|
||||||
|
size={styles.iconSize}
|
||||||
|
tooltip={t['com.affine.page-properties.page-info.view']()}
|
||||||
|
data-testid="edgeless-note-info-button"
|
||||||
|
onClick={onOpenInfoModal}
|
||||||
|
>
|
||||||
|
<InformationIcon />
|
||||||
|
</IconButton>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const LinkButton = ({ note }: { note: NoteBlockModel }) => {
|
||||||
|
const t = useI18n();
|
||||||
|
const { workspaceService, editorService } = useServices({
|
||||||
|
WorkspaceService,
|
||||||
|
EditorService,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { onClickCopyLink } = useSharingUrl({
|
||||||
|
workspaceId: workspaceService.workspace.id,
|
||||||
|
pageId: editorService.editor.doc.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
const copyLink = useCallback(() => {
|
||||||
|
onClickCopyLink('edgeless', [note.id]);
|
||||||
|
}, [note.id, onClickCopyLink]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<IconButton
|
||||||
|
className={styles.button}
|
||||||
|
size={styles.iconSize}
|
||||||
|
tooltip={t['com.affine.share-menu.copy']()}
|
||||||
|
data-testid="edgeless-note-link-button"
|
||||||
|
onClick={copyLink}
|
||||||
|
>
|
||||||
|
<LinkIcon />
|
||||||
|
</IconButton>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const EdgelessNoteHeader = ({ note }: { note: NoteBlockModel }) => {
|
||||||
|
const flags = useService(FeatureFlagService).flags;
|
||||||
|
const insidePeekView = useInsidePeekView();
|
||||||
|
|
||||||
|
if (!flags.enable_page_block_header) return null;
|
||||||
|
|
||||||
|
const isFirstNote =
|
||||||
|
note.parent?.children.find(child =>
|
||||||
|
matchFlavours(child, ['affine:note'])
|
||||||
|
) === note;
|
||||||
|
|
||||||
|
if (!isFirstNote) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.header} data-testid="edgeless-page-block-header">
|
||||||
|
<EdgelessNoteToggleButton note={note} />
|
||||||
|
<ExpandFullButton />
|
||||||
|
{!insidePeekView && <InfoButton note={note} />}
|
||||||
|
<LinkButton note={note} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -239,6 +239,17 @@ export const AFFINE_FLAGS = {
|
|||||||
configurable: !isMobile,
|
configurable: !isMobile,
|
||||||
defaultState: isCanaryBuild,
|
defaultState: isCanaryBuild,
|
||||||
},
|
},
|
||||||
|
// TODO(@L-Sun): remove this flag when ready
|
||||||
|
enable_page_block_header: {
|
||||||
|
category: 'blocksuite',
|
||||||
|
bsFlag: 'enable_page_block_header',
|
||||||
|
displayName:
|
||||||
|
'com.affine.settings.workspace.experimental-features.enable-page-block-header.name',
|
||||||
|
description:
|
||||||
|
'com.affine.settings.workspace.experimental-features.enable-page-block-header.description',
|
||||||
|
configurable: isCanaryBuild,
|
||||||
|
defaultState: isCanaryBuild,
|
||||||
|
},
|
||||||
} satisfies { [key in string]: FlagInfo };
|
} satisfies { [key in string]: FlagInfo };
|
||||||
|
|
||||||
// oxlint-disable-next-line no-redeclare
|
// oxlint-disable-next-line no-redeclare
|
||||||
|
|||||||
@@ -1332,6 +1332,8 @@
|
|||||||
"com.affine.settings.workspace.experimental-features.enable-mobile-edgeless-editing.description": "Once enabled, users can edit edgeless canvas.",
|
"com.affine.settings.workspace.experimental-features.enable-mobile-edgeless-editing.description": "Once enabled, users can edit edgeless canvas.",
|
||||||
"com.affine.settings.workspace.experimental-features.enable-pdf-embed-preview.name": "PDF embed preview",
|
"com.affine.settings.workspace.experimental-features.enable-pdf-embed-preview.name": "PDF embed preview",
|
||||||
"com.affine.settings.workspace.experimental-features.enable-pdf-embed-preview.description": "Once enabled, you can preview PDF in embed view.",
|
"com.affine.settings.workspace.experimental-features.enable-pdf-embed-preview.description": "Once enabled, you can preview PDF in embed view.",
|
||||||
|
"com.affine.settings.workspace.experimental-features.enable-page-block-header.name": "Page Block Header",
|
||||||
|
"com.affine.settings.workspace.experimental-features.enable-page-block-header.description": "Once enabled, the header of page block will be displayed.",
|
||||||
"com.affine.settings.workspace.not-owner": "Only an owner can edit the workspace avatar and name. Changes will be shown for everyone.",
|
"com.affine.settings.workspace.not-owner": "Only an owner can edit the workspace avatar and name. Changes will be shown for everyone.",
|
||||||
"com.affine.settings.workspace.preferences": "Preference",
|
"com.affine.settings.workspace.preferences": "Preference",
|
||||||
"com.affine.settings.workspace.billing": "Billing",
|
"com.affine.settings.workspace.billing": "Billing",
|
||||||
@@ -1590,6 +1592,8 @@
|
|||||||
"com.affine.editor.at-menu.date-picker": "Select a specific date",
|
"com.affine.editor.at-menu.date-picker": "Select a specific date",
|
||||||
"com.affine.editor.bi-directional-link-panel.show": "Show",
|
"com.affine.editor.bi-directional-link-panel.show": "Show",
|
||||||
"com.affine.editor.bi-directional-link-panel.hide": "Hide",
|
"com.affine.editor.bi-directional-link-panel.hide": "Hide",
|
||||||
|
"com.affine.editor.edgeless-note-header.fold-page-block": "Fold page block",
|
||||||
|
"com.affine.editor.edgeless-note-header.view-in-page": "View in page",
|
||||||
"com.affine.upgrade-to-team-page.title": "Empower Your Team with Seamless Collaboration",
|
"com.affine.upgrade-to-team-page.title": "Empower Your Team with Seamless Collaboration",
|
||||||
"com.affine.upgrade-to-team-page.workspace-selector.placeholder": "Select an existing workspace or create a new one",
|
"com.affine.upgrade-to-team-page.workspace-selector.placeholder": "Select an existing workspace or create a new one",
|
||||||
"com.affine.upgrade-to-team-page.workspace-selector.create-workspace": "Create Workspace",
|
"com.affine.upgrade-to-team-page.workspace-selector.create-workspace": "Create Workspace",
|
||||||
|
|||||||
@@ -314,6 +314,7 @@ const PageEvents = {
|
|||||||
pageRef: ['navigate'],
|
pageRef: ['navigate'],
|
||||||
toolbar: ['copyBlockToLink'],
|
toolbar: ['copyBlockToLink'],
|
||||||
aiActions: ['requestSignIn'],
|
aiActions: ['requestSignIn'],
|
||||||
|
pageBlockHeader: ['openDocInfo'],
|
||||||
},
|
},
|
||||||
inlineDocInfo: {
|
inlineDocInfo: {
|
||||||
$: ['toggle'],
|
$: ['toggle'],
|
||||||
|
|||||||
156
tests/affine-local/e2e/blocksuite/edgeless/note.spec.ts
Normal file
156
tests/affine-local/e2e/blocksuite/edgeless/note.spec.ts
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
import { test } from '@affine-test/kit/playwright';
|
||||||
|
import {
|
||||||
|
clickEdgelessModeButton,
|
||||||
|
createEdgelessNoteBlock,
|
||||||
|
getEdgelessSelectedIds,
|
||||||
|
getPageMode,
|
||||||
|
locateEditorContainer,
|
||||||
|
locateElementToolbar,
|
||||||
|
} from '@affine-test/kit/utils/editor';
|
||||||
|
import {
|
||||||
|
pasteByKeyboard,
|
||||||
|
selectAllByKeyboard,
|
||||||
|
} from '@affine-test/kit/utils/keyboard';
|
||||||
|
import { openHomePage } from '@affine-test/kit/utils/load-page';
|
||||||
|
import {
|
||||||
|
clickNewPageButton,
|
||||||
|
waitForEditorLoad,
|
||||||
|
} from '@affine-test/kit/utils/page-logic';
|
||||||
|
import { expect, type Page } from '@playwright/test';
|
||||||
|
|
||||||
|
const title = 'Edgeless Note Header Test';
|
||||||
|
|
||||||
|
test.beforeEach(async ({ page }) => {
|
||||||
|
await openHomePage(page);
|
||||||
|
await waitForEditorLoad(page);
|
||||||
|
await clickNewPageButton(page, title);
|
||||||
|
await page.keyboard.press('Enter');
|
||||||
|
await page.keyboard.type('Hello');
|
||||||
|
await page.keyboard.press('Enter');
|
||||||
|
await page.keyboard.type('World');
|
||||||
|
await clickEdgelessModeButton(page);
|
||||||
|
const container = locateEditorContainer(page);
|
||||||
|
await container.click();
|
||||||
|
});
|
||||||
|
|
||||||
|
test.describe('edgeless page header toolbar', () => {
|
||||||
|
const locateHeaderToolbar = (page: Page) =>
|
||||||
|
page.getByTestId('edgeless-page-block-header');
|
||||||
|
|
||||||
|
test('only first note block has header toolbar and its element toolbar', async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
const toolbar = locateHeaderToolbar(page);
|
||||||
|
await expect(toolbar).toHaveCount(1);
|
||||||
|
await expect(toolbar).toBeVisible();
|
||||||
|
|
||||||
|
await createEdgelessNoteBlock(page, [100, 100]);
|
||||||
|
|
||||||
|
await expect(toolbar).toHaveCount(1);
|
||||||
|
await expect(toolbar).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should shrink note block when clicking on the toggle button', async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
const toolbar = locateHeaderToolbar(page);
|
||||||
|
const toolBox = await toolbar.boundingBox();
|
||||||
|
const noteBox = await page.locator('affine-edgeless-note').boundingBox();
|
||||||
|
if (!noteBox || !toolBox) throw new Error('Bounding box not found');
|
||||||
|
expect(noteBox.height).toBeGreaterThan(toolBox.height);
|
||||||
|
|
||||||
|
const toggleButton = toolbar.getByTestId('edgeless-note-toggle-button');
|
||||||
|
await toggleButton.click();
|
||||||
|
|
||||||
|
const newNoteBox = await page.locator('affine-edgeless-note').boundingBox();
|
||||||
|
if (!newNoteBox) throw new Error('Bounding box not found');
|
||||||
|
expect(newNoteBox.height).toBe(toolBox.height);
|
||||||
|
|
||||||
|
await toggleButton.click();
|
||||||
|
const newNoteBox2 = await page
|
||||||
|
.locator('affine-edgeless-note')
|
||||||
|
.boundingBox();
|
||||||
|
if (!newNoteBox2) throw new Error('Bounding box not found');
|
||||||
|
expect(newNoteBox2).toEqual(noteBox);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('page title should be displayed when page block is collapsed and hidden when page block is not collapsed', async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
const toolbar = locateHeaderToolbar(page);
|
||||||
|
const toolbarTitle = toolbar.getByTestId('edgeless-note-title');
|
||||||
|
await expect(toolbarTitle).toHaveText('');
|
||||||
|
|
||||||
|
const toggleButton = toolbar.getByTestId('edgeless-note-toggle-button');
|
||||||
|
await toggleButton.click();
|
||||||
|
await expect(toolbarTitle).toHaveText(title);
|
||||||
|
|
||||||
|
await toggleButton.click();
|
||||||
|
await expect(toolbarTitle).toHaveText('');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should switch to page mode when expand button is clicked', async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
const toolbar = locateHeaderToolbar(page);
|
||||||
|
const expandButton = toolbar.getByTestId('edgeless-note-expand-button');
|
||||||
|
await expandButton.click();
|
||||||
|
|
||||||
|
expect(await getPageMode(page)).toBe('page');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should open doc properties dialog when info button is clicked', async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
const toolbar = locateHeaderToolbar(page);
|
||||||
|
const infoButton = toolbar.getByTestId('edgeless-note-info-button');
|
||||||
|
await infoButton.click();
|
||||||
|
const infoModal = page.getByTestId('info-modal');
|
||||||
|
await expect(infoModal).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should copy note edgeless link to clipboard when link button is clicked', async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
const toolbar = locateHeaderToolbar(page);
|
||||||
|
await selectAllByKeyboard(page);
|
||||||
|
const noteId = (await getEdgelessSelectedIds(page))[0];
|
||||||
|
|
||||||
|
const linkButton = toolbar.getByTestId('edgeless-note-link-button');
|
||||||
|
await linkButton.click();
|
||||||
|
|
||||||
|
const url = page.url();
|
||||||
|
const link = await page.evaluate(() => navigator.clipboard.readText());
|
||||||
|
expect(link).toBe(`${url}&blockIds=${noteId}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('info button should hidden in peek view', async ({ page }) => {
|
||||||
|
const url = page.url();
|
||||||
|
await page.evaluate(url => navigator.clipboard.writeText(url), url);
|
||||||
|
|
||||||
|
await clickNewPageButton(page);
|
||||||
|
await page.keyboard.press('Enter');
|
||||||
|
await pasteByKeyboard(page);
|
||||||
|
const reference = page.locator('affine-reference');
|
||||||
|
await reference.click({ modifiers: ['Shift'] });
|
||||||
|
|
||||||
|
const toolbar = locateHeaderToolbar(page);
|
||||||
|
const infoButton = toolbar.getByTestId('edgeless-note-info-button');
|
||||||
|
|
||||||
|
await expect(toolbar).toBeVisible();
|
||||||
|
await expect(infoButton).toBeHidden();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.describe('edgeless note element toolbar', () => {
|
||||||
|
test('the toolbar of page block should not contains auto-height', async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
await selectAllByKeyboard(page);
|
||||||
|
const toolbar = locateElementToolbar(page);
|
||||||
|
const autoHeight = toolbar.getByTestId('edgeless-note-auto-height');
|
||||||
|
|
||||||
|
await expect(toolbar).toBeVisible();
|
||||||
|
await expect(autoHeight).toHaveCount(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -11,6 +11,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@affine-tools/utils": "workspace:*",
|
"@affine-tools/utils": "workspace:*",
|
||||||
|
"@blocksuite/affine": "workspace:*",
|
||||||
"@node-rs/argon2": "^2.0.2",
|
"@node-rs/argon2": "^2.0.2",
|
||||||
"@playwright/test": "=1.49.1",
|
"@playwright/test": "=1.49.1",
|
||||||
"express": "^4.21.2",
|
"express": "^4.21.2",
|
||||||
|
|||||||
@@ -1,4 +1,10 @@
|
|||||||
import { expect, type Page } from '@playwright/test';
|
import type { AffineEditorContainer } from '@blocksuite/affine/presets';
|
||||||
|
import {
|
||||||
|
EDGELESS_ELEMENT_TOOLBAR_WIDGET,
|
||||||
|
EDGELESS_TOOLBAR_WIDGET,
|
||||||
|
} from '@blocksuite/blocks';
|
||||||
|
import type { IVec, XYWH } from '@blocksuite/global/utils';
|
||||||
|
import { expect, type Locator, type Page } from '@playwright/test';
|
||||||
|
|
||||||
export function locateModeSwitchButton(
|
export function locateModeSwitchButton(
|
||||||
page: Page,
|
page: Page,
|
||||||
@@ -41,3 +47,270 @@ export async function getPageMode(page: Page): Promise<'page' | 'edgeless'> {
|
|||||||
}
|
}
|
||||||
throw new Error('Unknown mode');
|
throw new Error('Unknown mode');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function locateEditorContainer(page: Page, editorIndex = 0) {
|
||||||
|
return page.locator('[data-affine-editor-container]').nth(editorIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ================== Edgeless ==================
|
||||||
|
|
||||||
|
export async function getEdgelessSelectedIds(page: Page, editorIndex = 0) {
|
||||||
|
const container = locateEditorContainer(page, editorIndex);
|
||||||
|
return container.evaluate((container: AffineEditorContainer) => {
|
||||||
|
const root = container.querySelector('affine-edgeless-root');
|
||||||
|
if (!root) {
|
||||||
|
throw new Error('Edgeless root not found');
|
||||||
|
}
|
||||||
|
return root.gfx.selection.selectedIds;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert a canvas point to view coordinate
|
||||||
|
* @param point the coordinate on the canvas
|
||||||
|
*/
|
||||||
|
export async function toViewCoord(page: Page, point: IVec, editorIndex = 0) {
|
||||||
|
const container = locateEditorContainer(page, editorIndex);
|
||||||
|
return container.evaluate((container: AffineEditorContainer, point) => {
|
||||||
|
const root = container.querySelector('affine-edgeless-root');
|
||||||
|
if (!root) {
|
||||||
|
throw new Error('Edgeless root not found');
|
||||||
|
}
|
||||||
|
return root.gfx.viewport.toViewCoord(point[0], point[1]);
|
||||||
|
}, point);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Click a point on the canvas
|
||||||
|
* @param point the coordinate on the canvas
|
||||||
|
*/
|
||||||
|
export async function clickView(page: Page, point: IVec, editorIndex = 0) {
|
||||||
|
const [x, y] = await toViewCoord(page, point, editorIndex);
|
||||||
|
await page.mouse.click(x, y);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Double click a point on the canvas
|
||||||
|
* @param point the coordinate on the canvas
|
||||||
|
*/
|
||||||
|
export async function dblclickView(page: Page, point: IVec, editorIndex = 0) {
|
||||||
|
const [x, y] = await toViewCoord(page, point, editorIndex);
|
||||||
|
await page.mouse.dblclick(x, y);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function dragView(
|
||||||
|
page: Page,
|
||||||
|
from: IVec,
|
||||||
|
to: IVec,
|
||||||
|
editorIndex = 0
|
||||||
|
) {
|
||||||
|
const [x1, y1] = await toViewCoord(page, from, editorIndex);
|
||||||
|
const [x2, y2] = await toViewCoord(page, to, editorIndex);
|
||||||
|
await page.mouse.move(x1, y1);
|
||||||
|
await page.mouse.down();
|
||||||
|
await page.mouse.move(x2, y2);
|
||||||
|
await page.mouse.up();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function locateEdgelessToolbar(page: Page, editorIndex = 0) {
|
||||||
|
return locateEditorContainer(page, editorIndex).locator(
|
||||||
|
EDGELESS_TOOLBAR_WIDGET
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
type EdgelessTool =
|
||||||
|
| 'default'
|
||||||
|
| 'pan'
|
||||||
|
| 'note'
|
||||||
|
| 'shape'
|
||||||
|
| 'brush'
|
||||||
|
| 'eraser'
|
||||||
|
| 'text'
|
||||||
|
| 'connector'
|
||||||
|
| 'frame'
|
||||||
|
| 'frameNavigator'
|
||||||
|
| 'lasso';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param type the type of the tool in the toolbar
|
||||||
|
* @param innerContainer the button may have an inner container
|
||||||
|
*/
|
||||||
|
export async function locateEdgelessToolButton(
|
||||||
|
page: Page,
|
||||||
|
type: EdgelessTool,
|
||||||
|
innerContainer = true,
|
||||||
|
editorIndex = 0
|
||||||
|
) {
|
||||||
|
const toolbar = locateEdgelessToolbar(page, editorIndex);
|
||||||
|
|
||||||
|
const selector = {
|
||||||
|
default: '.edgeless-default-button',
|
||||||
|
pan: '.edgeless-default-button',
|
||||||
|
shape: '.edgeless-shape-button',
|
||||||
|
brush: '.edgeless-brush-button',
|
||||||
|
eraser: '.edgeless-eraser-button',
|
||||||
|
text: '.edgeless-mindmap-button',
|
||||||
|
connector: '.edgeless-connector-button',
|
||||||
|
note: '.edgeless-note-button',
|
||||||
|
frame: '.edgeless-frame-button',
|
||||||
|
frameNavigator: '.edgeless-frame-navigator-button',
|
||||||
|
lasso: '.edgeless-lasso-button',
|
||||||
|
}[type];
|
||||||
|
|
||||||
|
let buttonType;
|
||||||
|
switch (type) {
|
||||||
|
case 'brush':
|
||||||
|
case 'text':
|
||||||
|
case 'eraser':
|
||||||
|
case 'shape':
|
||||||
|
case 'note':
|
||||||
|
buttonType = 'edgeless-toolbar-button';
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
buttonType = 'edgeless-tool-icon-button';
|
||||||
|
}
|
||||||
|
|
||||||
|
const locateEdgelessToolButtonSenior = async (
|
||||||
|
selector: string
|
||||||
|
): Promise<Locator> => {
|
||||||
|
const target = toolbar.locator(selector);
|
||||||
|
const visible = await target.isVisible();
|
||||||
|
if (visible) return target;
|
||||||
|
// try to click next page
|
||||||
|
const nextButton = toolbar.locator(
|
||||||
|
'.senior-nav-button-wrapper.next > icon-button'
|
||||||
|
);
|
||||||
|
const nextExists = await nextButton.count();
|
||||||
|
const isDisabled =
|
||||||
|
// oxlint-disable-next-line unicorn/prefer-dom-node-dataset
|
||||||
|
(await nextButton.getAttribute('data-test-disabled')) === 'true';
|
||||||
|
if (!nextExists || isDisabled) return target;
|
||||||
|
await nextButton.click();
|
||||||
|
await page.waitForTimeout(200);
|
||||||
|
return locateEdgelessToolButtonSenior(selector);
|
||||||
|
};
|
||||||
|
|
||||||
|
const button = await locateEdgelessToolButtonSenior(
|
||||||
|
`${buttonType}${selector}`
|
||||||
|
);
|
||||||
|
|
||||||
|
return innerContainer ? button.locator('.icon-container') : button;
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum Shape {
|
||||||
|
Diamond = 'Diamond',
|
||||||
|
Ellipse = 'Ellipse',
|
||||||
|
'Rounded rectangle' = 'Rounded rectangle',
|
||||||
|
Square = 'Square',
|
||||||
|
Triangle = 'Triangle',
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set edgeless tool by clicking button in edgeless toolbar
|
||||||
|
*/
|
||||||
|
export async function setEdgelessTool(
|
||||||
|
page: Page,
|
||||||
|
tool: EdgelessTool,
|
||||||
|
shape = Shape.Square,
|
||||||
|
editorIndex = 0
|
||||||
|
) {
|
||||||
|
const toolbar = locateEdgelessToolbar(page, editorIndex);
|
||||||
|
|
||||||
|
switch (tool) {
|
||||||
|
// text tool is removed, use shortcut to trigger
|
||||||
|
case 'text':
|
||||||
|
await page.keyboard.press('t', { delay: 100 });
|
||||||
|
break;
|
||||||
|
case 'default': {
|
||||||
|
const button = await locateEdgelessToolButton(
|
||||||
|
page,
|
||||||
|
'default',
|
||||||
|
false,
|
||||||
|
editorIndex
|
||||||
|
);
|
||||||
|
const classes = (await button.getAttribute('class'))?.split(' ');
|
||||||
|
if (!classes?.includes('default')) {
|
||||||
|
await button.click();
|
||||||
|
await page.waitForTimeout(100);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'pan': {
|
||||||
|
const button = await locateEdgelessToolButton(
|
||||||
|
page,
|
||||||
|
'default',
|
||||||
|
false,
|
||||||
|
editorIndex
|
||||||
|
);
|
||||||
|
const classes = (await button.getAttribute('class'))?.split(' ');
|
||||||
|
if (classes?.includes('default')) {
|
||||||
|
await button.click();
|
||||||
|
await page.waitForTimeout(100);
|
||||||
|
} else if (classes?.includes('pan')) {
|
||||||
|
await button.click(); // change to default
|
||||||
|
await page.waitForTimeout(100);
|
||||||
|
await button.click(); // change to pan
|
||||||
|
await page.waitForTimeout(100);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'lasso':
|
||||||
|
case 'note':
|
||||||
|
case 'brush':
|
||||||
|
case 'eraser':
|
||||||
|
case 'frame':
|
||||||
|
case 'connector': {
|
||||||
|
const button = await locateEdgelessToolButton(
|
||||||
|
page,
|
||||||
|
tool,
|
||||||
|
false,
|
||||||
|
editorIndex
|
||||||
|
);
|
||||||
|
await button.click();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'shape': {
|
||||||
|
const shapeToolButton = await locateEdgelessToolButton(
|
||||||
|
page,
|
||||||
|
'shape',
|
||||||
|
false,
|
||||||
|
editorIndex
|
||||||
|
);
|
||||||
|
// Avoid clicking on the shape-element (will trigger dragging mode)
|
||||||
|
await shapeToolButton.click({ position: { x: 5, y: 5 } });
|
||||||
|
|
||||||
|
const squareShapeButton = toolbar
|
||||||
|
.locator('edgeless-slide-menu edgeless-tool-icon-button')
|
||||||
|
.filter({ hasText: shape });
|
||||||
|
await squareShapeButton.click();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function locateElementToolbar(page: Page, editorIndex = 0) {
|
||||||
|
return locateEditorContainer(page, editorIndex).locator(
|
||||||
|
EDGELESS_ELEMENT_TOOLBAR_WIDGET
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a not block in canvas
|
||||||
|
* @param position the position or xwyh of the note block in canvas
|
||||||
|
*/
|
||||||
|
export async function createEdgelessNoteBlock(
|
||||||
|
page: Page,
|
||||||
|
position: IVec | XYWH,
|
||||||
|
editorIndex = 0
|
||||||
|
) {
|
||||||
|
await setEdgelessTool(page, 'note', undefined, editorIndex);
|
||||||
|
if (position.length === 4) {
|
||||||
|
dragView(
|
||||||
|
page,
|
||||||
|
[position[0], position[1]],
|
||||||
|
[position[0] + position[2], position[1] + position[3]]
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
await clickView(page, position, editorIndex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ export async function importImage(page: Page, pathInFixtures: string) {
|
|||||||
await page.evaluate(() => {
|
await page.evaluate(() => {
|
||||||
// Force fallback to input[type=file] in tests
|
// Force fallback to input[type=file] in tests
|
||||||
// See https://github.com/microsoft/playwright/issues/8850
|
// See https://github.com/microsoft/playwright/issues/8850
|
||||||
// @ts-expect-error allow
|
|
||||||
window.showOpenFilePicker = undefined;
|
window.showOpenFilePicker = undefined;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -6,5 +6,8 @@
|
|||||||
"tsBuildInfoFile": "./dist/tsconfig.tsbuildinfo"
|
"tsBuildInfoFile": "./dist/tsconfig.tsbuildinfo"
|
||||||
},
|
},
|
||||||
"include": ["./src"],
|
"include": ["./src"],
|
||||||
"references": [{ "path": "../../tools/utils" }]
|
"references": [
|
||||||
|
{ "path": "../../tools/utils" },
|
||||||
|
{ "path": "../../blocksuite/affine/all" }
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -661,7 +661,7 @@ export const PackageList = [
|
|||||||
{
|
{
|
||||||
location: 'tests/kit',
|
location: 'tests/kit',
|
||||||
name: '@affine-test/kit',
|
name: '@affine-test/kit',
|
||||||
workspaceDependencies: ['tools/utils'],
|
workspaceDependencies: ['tools/utils', 'blocksuite/affine/all'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
location: 'tools/@types/build-config',
|
location: 'tools/@types/build-config',
|
||||||
|
|||||||
@@ -82,6 +82,7 @@ __metadata:
|
|||||||
resolution: "@affine-test/kit@workspace:tests/kit"
|
resolution: "@affine-test/kit@workspace:tests/kit"
|
||||||
dependencies:
|
dependencies:
|
||||||
"@affine-tools/utils": "workspace:*"
|
"@affine-tools/utils": "workspace:*"
|
||||||
|
"@blocksuite/affine": "workspace:*"
|
||||||
"@node-rs/argon2": "npm:^2.0.2"
|
"@node-rs/argon2": "npm:^2.0.2"
|
||||||
"@playwright/test": "npm:=1.49.1"
|
"@playwright/test": "npm:=1.49.1"
|
||||||
express: "npm:^4.21.2"
|
express: "npm:^4.21.2"
|
||||||
|
|||||||
Reference in New Issue
Block a user