feat(core): save recent open mode of internal links (#11086)

Closes: [BS-2865](https://linear.app/affine-design/issue/BS-2865/internal-links-保存用户最近的打开方式)

Added `openDocMode` in settings.

https://github.com/user-attachments/assets/a452da73-83e4-4ef5-9b57-58291fc22785
This commit is contained in:
fundon
2025-03-24 06:12:47 +00:00
parent 4bacfbd640
commit 63762b75a1
13 changed files with 256 additions and 128 deletions

View File

@@ -57,7 +57,7 @@ export class EditorIconButton extends LitElement {
padding: 0 4px; padding: 0 4px;
overflow: hidden; overflow: hidden;
white-space: nowrap; white-space: nowrap;
line-height: var(--label-height, inherit); line-height: var(--label-height, var(--icon-size, 20px));
} }
::slotted(.label.padding0) { ::slotted(.label.padding0) {
padding: 0; padding: 0;

View File

@@ -232,7 +232,7 @@ export function renderToolbar(
`${flavour}:${key}`, `${flavour}:${key}`,
html` html`
<editor-menu-button <editor-menu-button
aria-label="more-menu" aria-label="More menu"
.contentPadding="${'8px'}" .contentPadding="${'8px'}"
.button=${html` .button=${html`
<editor-icon-button aria-label="More" .tooltip="${'More'}"> <editor-icon-button aria-label="More" .tooltip="${'More'}">

View File

@@ -29,6 +29,6 @@ export function getEditorConfigExtension(
}), }),
ToolbarMoreMenuConfigExtension(createToolbarMoreMenuConfig(framework)), ToolbarMoreMenuConfigExtension(createToolbarMoreMenuConfig(framework)),
createCustomToolbarExtension(baseUrl), createCustomToolbarExtension(editorSettingService.editorSetting, baseUrl),
].flat(); ].flat();
} }

View File

@@ -5,6 +5,7 @@ import {
} from '@affine/core/components/hooks/affine/use-share-url'; } from '@affine/core/components/hooks/affine/use-share-url';
import { WorkspaceServerService } from '@affine/core/modules/cloud'; import { WorkspaceServerService } from '@affine/core/modules/cloud';
import { EditorService } from '@affine/core/modules/editor'; import { EditorService } from '@affine/core/modules/editor';
import type { EditorSettingExt } from '@affine/core/modules/editor-setting/entities/editor-setting';
import { copyLinkToBlockStdScopeClipboard } from '@affine/core/utils/clipboard'; import { copyLinkToBlockStdScopeClipboard } from '@affine/core/utils/clipboard';
import { I18n, i18nTime } from '@affine/i18n'; import { I18n, i18nTime } from '@affine/i18n';
import { track } from '@affine/track'; import { track } from '@affine/track';
@@ -57,7 +58,6 @@ import {
GenerateDocUrlProvider, GenerateDocUrlProvider,
isRemovedUserInfo, isRemovedUserInfo,
OpenDocExtensionIdentifier, OpenDocExtensionIdentifier,
type OpenDocMode,
type ToolbarAction, type ToolbarAction,
type ToolbarActionGenerator, type ToolbarActionGenerator,
type ToolbarActionGroupGenerator, type ToolbarActionGroupGenerator,
@@ -69,14 +69,11 @@ import {
import { matchModels } from '@blocksuite/affine/shared/utils'; import { matchModels } from '@blocksuite/affine/shared/utils';
import type { ExtensionType } from '@blocksuite/affine/store'; import type { ExtensionType } from '@blocksuite/affine/store';
import { import {
CenterPeekIcon,
CopyAsImgaeIcon, CopyAsImgaeIcon,
CopyIcon, CopyIcon,
EditIcon, EditIcon,
ExpandFullIcon,
LinkIcon, LinkIcon,
OpenInNewIcon, OpenInNewIcon,
SplitViewIcon,
} from '@blocksuite/icons/lit'; } from '@blocksuite/icons/lit';
import { computed } from '@preact/signals-core'; import { computed } from '@preact/signals-core';
import type { FrameworkProvider } from '@toeverything/infra'; import type { FrameworkProvider } from '@toeverything/infra';
@@ -86,6 +83,7 @@ import { keyed } from 'lit/directives/keyed.js';
import { repeat } from 'lit/directives/repeat.js'; import { repeat } from 'lit/directives/repeat.js';
import { styleMap } from 'lit/directives/style-map.js'; import { styleMap } from 'lit/directives/style-map.js';
import { openDocActions } from '../../open-doc';
import { createCopyAsPngMenuItem } from './copy-as-image'; import { createCopyAsPngMenuItem } from './copy-as-image';
export function createToolbarMoreMenuConfig(framework: FrameworkProvider) { export function createToolbarMoreMenuConfig(framework: FrameworkProvider) {
@@ -477,39 +475,6 @@ function createExternalLinkableToolbarConfig(
} as const satisfies ToolbarModuleConfig; } as const satisfies ToolbarModuleConfig;
} }
const openDocActions = [
{
mode: 'open-in-active-view',
id: 'a.open-in-active-view',
label: I18n['com.affine.peek-view-controls.open-doc'](),
icon: ExpandFullIcon(),
when: true,
},
{
mode: 'open-in-new-view',
id: 'b.open-in-new-view',
label: I18n['com.affine.peek-view-controls.open-doc-in-split-view'](),
icon: SplitViewIcon(),
when: BUILD_CONFIG.isElectron,
},
{
mode: 'open-in-new-tab',
id: 'c.open-in-new-tab',
label: I18n['com.affine.peek-view-controls.open-doc-in-new-tab'](),
icon: OpenInNewIcon(),
when: true,
},
{
mode: 'open-in-center-peek',
id: 'd.open-in-center-peek',
label: I18n['com.affine.peek-view-controls.open-doc-in-center-peek'](),
icon: CenterPeekIcon(),
when: true,
},
] as const satisfies (Pick<ToolbarAction, 'id' | 'label' | 'icon' | 'when'> & {
mode: OpenDocMode;
})[];
function createOpenDocActions( function createOpenDocActions(
ctx: ToolbarContext, ctx: ToolbarContext,
target: target:
@@ -517,7 +482,15 @@ function createOpenDocActions(
| EmbedSyncedDocBlockComponent | EmbedSyncedDocBlockComponent
| AffineReference, | AffineReference,
isSameDoc: boolean, isSameDoc: boolean,
actions = openDocActions actions = openDocActions.map(
({ type: mode, label, icon, enabled: when }, i) => ({
mode,
id: `${i}.${mode}`,
label,
icon,
when,
})
)
) { ) {
return actions return actions
.filter(action => action.when) .filter(action => action.when)
@@ -552,7 +525,8 @@ function createOpenDocActions(
function createOpenDocActionGroup( function createOpenDocActionGroup(
klass: klass:
| typeof EmbedLinkedDocBlockComponent | typeof EmbedLinkedDocBlockComponent
| typeof EmbedSyncedDocBlockComponent | typeof EmbedSyncedDocBlockComponent,
settings: EditorSettingExt
): ToolbarAction { ): ToolbarAction {
return { return {
placement: ActionPlacement.Start, placement: ActionPlacement.Start,
@@ -562,6 +536,7 @@ function createOpenDocActionGroup(
if (!block) return null; if (!block) return null;
return renderOpenDocMenu( return renderOpenDocMenu(
settings,
ctx, ctx,
block, block,
block.model.props.pageId === ctx.store.id block.model.props.pageId === ctx.store.id
@@ -594,6 +569,7 @@ function createEdgelessOpenDocActionGroup(
} }
function renderOpenDocMenu( function renderOpenDocMenu(
settings: EditorSettingExt,
ctx: ToolbarContext, ctx: ToolbarContext,
target: target:
| EmbedLinkedDocBlockComponent | EmbedLinkedDocBlockComponent
@@ -607,42 +583,64 @@ function renderOpenDocMenu(
})); }));
if (!actions.length) return null; if (!actions.length) return null;
return html` const currentOpenMode =
${keyed( settings.settingSignal.value.openDocMode ?? 'open-in-active-view';
target, const currentIcon =
html` openDocActions.find(a => a.type === currentOpenMode)?.icon ??
<editor-menu-button OpenInNewIcon();
.contentPadding="${'8px'}" const currentAction = actions.find(a => a.icon === currentIcon) ?? actions[0];
.button=${html`
<editor-icon-button aria-label="Open doc" .tooltip=${'Open doc'}> return html`${keyed(
${OpenInNewIcon()} ${EditorChevronDown} target,
</editor-icon-button> html`
`} <editor-icon-button
> aria-label="${currentAction.label}"
<div data-size="small" data-orientation="vertical"> .tooltip="${currentAction.label}"
${repeat( @click=${() => currentAction.run?.(ctx)}
actions, >
action => action.id, ${currentAction.icon} <span class="label">Open</span>
({ label, icon, run, disabled }) => html` </editor-icon-button>
<editor-menu-action <editor-menu-button
aria-label=${ifDefined(label)} aria-label="Open doc menu"
?disabled=${ifDefined(disabled)} .contentPadding="${'8px'}"
@click=${() => run?.(ctx)} .button=${html`
> <editor-icon-button
${icon}<span class="label">${label}</span> aria-label="Open doc"
</editor-menu-action> .tooltip="${'Open doc'}"
` .iconContainerPadding="${'4'}"
)} >
</div> ${EditorChevronDown}
</editor-menu-button> </editor-icon-button>
` `}
)} >
`; <div data-size="small" data-orientation="vertical">
${repeat(
actions,
action => action.id,
({ label, icon, run, disabled }) => html`
<editor-menu-action
aria-label=${ifDefined(label)}
?disabled=${ifDefined(disabled)}
@click=${() => {
run?.(ctx);
settings.openDocMode.set(
openDocActions.find(a => a.icon === icon)?.type ??
'open-in-active-view'
);
}}
>
${icon}<span class="label">${label}</span>
</editor-menu-action>
`
)}
</div>
</editor-menu-button>
`
)}`;
} }
const embedLinkedDocToolbarConfig = { const embedLinkedDocToolbarConfig = {
actions: [ actions: [
createOpenDocActionGroup(EmbedLinkedDocBlockComponent),
{ {
id: 'a.doc-title.after.copy-link-and-edit', id: 'a.doc-title.after.copy-link-and-edit',
actions: [ actions: [
@@ -724,7 +722,6 @@ const embedLinkedDocToolbarConfig = {
const embedSyncedDocToolbarConfig = { const embedSyncedDocToolbarConfig = {
actions: [ actions: [
createOpenDocActionGroup(EmbedSyncedDocBlockComponent),
{ {
placement: ActionPlacement.Start, placement: ActionPlacement.Start,
id: 'B.copy-link-and-edit', id: 'B.copy-link-and-edit',
@@ -800,20 +797,6 @@ const embedSyncedDocToolbarConfig = {
const inlineReferenceToolbarConfig = { const inlineReferenceToolbarConfig = {
actions: [ actions: [
{
placement: ActionPlacement.Start,
id: 'A.open-doc',
content(ctx) {
const target = ctx.message$.peek()?.element;
if (!(target instanceof AffineReference)) return null;
return renderOpenDocMenu(
ctx,
target,
target.referenceInfo.pageId === ctx.store.id
);
},
},
{ {
id: 'b.copy-link-and-edit', id: 'b.copy-link-and-edit',
actions: [ actions: [
@@ -957,6 +940,7 @@ const embedIframeToolbarConfig = {
} as const satisfies ToolbarModuleConfig; } as const satisfies ToolbarModuleConfig;
export const createCustomToolbarExtension = ( export const createCustomToolbarExtension = (
settings: EditorSettingExt,
baseUrl: string baseUrl: string
): ExtensionType[] => { ): ExtensionType[] => {
return [ return [
@@ -1017,7 +1001,12 @@ export const createCustomToolbarExtension = (
ToolbarModuleExtension({ ToolbarModuleExtension({
id: BlockFlavourIdentifier('custom:affine:embed-linked-doc'), id: BlockFlavourIdentifier('custom:affine:embed-linked-doc'),
config: embedLinkedDocToolbarConfig, config: {
actions: [
embedLinkedDocToolbarConfig.actions,
createOpenDocActionGroup(EmbedLinkedDocBlockComponent, settings),
].flat(),
},
}), }),
ToolbarModuleExtension({ ToolbarModuleExtension({
@@ -1025,6 +1014,7 @@ export const createCustomToolbarExtension = (
config: { config: {
actions: [ actions: [
embedLinkedDocToolbarConfig.actions, embedLinkedDocToolbarConfig.actions,
createOpenDocActionGroup(EmbedLinkedDocBlockComponent, settings),
createEdgelessOpenDocActionGroup(EmbedLinkedDocBlockComponent), createEdgelessOpenDocActionGroup(EmbedLinkedDocBlockComponent),
].flat(), ].flat(),
@@ -1034,7 +1024,13 @@ export const createCustomToolbarExtension = (
ToolbarModuleExtension({ ToolbarModuleExtension({
id: BlockFlavourIdentifier('custom:affine:embed-synced-doc'), id: BlockFlavourIdentifier('custom:affine:embed-synced-doc'),
config: embedSyncedDocToolbarConfig, config: {
actions: [
embedSyncedDocToolbarConfig.actions,
createOpenDocActionGroup(EmbedSyncedDocBlockComponent, settings),
createEdgelessOpenDocActionGroup(EmbedSyncedDocBlockComponent),
].flat(),
},
}), }),
ToolbarModuleExtension({ ToolbarModuleExtension({
@@ -1042,6 +1038,7 @@ export const createCustomToolbarExtension = (
config: { config: {
actions: [ actions: [
embedSyncedDocToolbarConfig.actions, embedSyncedDocToolbarConfig.actions,
createOpenDocActionGroup(EmbedSyncedDocBlockComponent, settings),
createEdgelessOpenDocActionGroup(EmbedSyncedDocBlockComponent), createEdgelessOpenDocActionGroup(EmbedSyncedDocBlockComponent),
].flat(), ].flat(),
@@ -1051,7 +1048,26 @@ export const createCustomToolbarExtension = (
ToolbarModuleExtension({ ToolbarModuleExtension({
id: BlockFlavourIdentifier('custom:affine:reference'), id: BlockFlavourIdentifier('custom:affine:reference'),
config: inlineReferenceToolbarConfig, config: {
actions: [
{
placement: ActionPlacement.Start,
id: 'A.open-doc',
content(ctx) {
const target = ctx.message$.peek()?.element;
if (!(target instanceof AffineReference)) return null;
return renderOpenDocMenu(
settings,
ctx,
target,
target.referenceInfo.pageId === ctx.store.id
);
},
} as const satisfies ToolbarAction,
inlineReferenceToolbarConfig.actions,
].flat(),
},
}), }),
ToolbarModuleExtension({ ToolbarModuleExtension({

View File

@@ -1,3 +1,4 @@
import { EditorSettingSchema } from '@affine/core/modules/editor-setting';
import { I18n } from '@affine/i18n'; import { I18n } from '@affine/i18n';
import { import {
type OpenDocConfig, type OpenDocConfig,
@@ -11,33 +12,45 @@ import {
SplitViewIcon, SplitViewIcon,
} from '@blocksuite/icons/lit'; } from '@blocksuite/icons/lit';
type OpenDocAction = OpenDocConfigItem & { enabled: boolean };
export const openDocActions: Array<OpenDocAction> = [
{
type: 'open-in-active-view',
label: I18n['com.affine.peek-view-controls.open-doc'](),
icon: ExpandFullIcon(),
enabled: true,
},
{
type: 'open-in-new-view',
label: I18n['com.affine.peek-view-controls.open-doc-in-split-view'](),
icon: SplitViewIcon(),
enabled: BUILD_CONFIG.isElectron,
},
{
type: 'open-in-new-tab',
label: I18n['com.affine.peek-view-controls.open-doc-in-new-tab'](),
icon: OpenInNewIcon(),
enabled: true,
},
{
type: 'open-in-center-peek',
label: I18n['com.affine.peek-view-controls.open-doc-in-center-peek'](),
icon: CenterPeekIcon(),
enabled: true,
},
].filter(
(a): a is OpenDocAction =>
a.enabled && EditorSettingSchema.shape.openDocMode.safeParse(a.type).success
);
export function patchOpenDocExtension() { export function patchOpenDocExtension() {
const openDocConfig: OpenDocConfig = { const openDocConfig: OpenDocConfig = {
items: [ items: openDocActions.map<OpenDocConfigItem>(({ type, label, icon }) => ({
{ type,
type: 'open-in-active-view', label,
label: I18n['com.affine.peek-view-controls.open-doc'](), icon,
icon: ExpandFullIcon(), })),
},
BUILD_CONFIG.isElectron
? {
type: 'open-in-new-view',
label:
I18n['com.affine.peek-view-controls.open-doc-in-split-view'](),
icon: SplitViewIcon(),
}
: null,
{
type: 'open-in-new-tab',
label: I18n['com.affine.peek-view-controls.open-doc-in-new-tab'](),
icon: OpenInNewIcon(),
},
{
type: 'open-in-center-peek',
label: I18n['com.affine.peek-view-controls.open-doc-in-center-peek'](),
icon: CenterPeekIcon(),
},
].filter((item): item is OpenDocConfigItem => item !== null),
}; };
return OpenDocExtension(openDocConfig); return OpenDocExtension(openDocConfig);
} }

View File

@@ -26,6 +26,14 @@ const AffineEditorSettingSchema = z.object({
edgelessDefaultTheme: z edgelessDefaultTheme: z
.enum(['specified', 'dark', 'light', 'auto']) .enum(['specified', 'dark', 'light', 'auto'])
.default('specified'), .default('specified'),
openDocMode: z
.enum([
'open-in-active-view',
'open-in-new-view',
'open-in-new-tab',
'open-in-center-peek',
])
.default('open-in-active-view'),
}); });
export const EditorSettingSchema = BSEditorSettingSchema.merge( export const EditorSettingSchema = BSEditorSettingSchema.merge(

View File

@@ -47,7 +47,7 @@ test('should update zindex of element when moving it into frame', async ({
await createEdgelessNoteBlock(page, [500, 500]); await createEdgelessNoteBlock(page, [500, 500]);
await clickView(page, [0, 100]); await clickView(page, [0, 100]);
await clickView(page, [500, 500]); await clickView(page, [500, 500]);
await toolbar.getByLabel('more-menu').click(); await toolbar.getByLabel('More menu').click();
await toolbar.getByLabel('Send to Back').click(); await toolbar.getByLabel('Send to Back').click();
await pressEscape(page); await pressEscape(page);

View File

@@ -190,8 +190,11 @@ test('outline viewer should be useable in doc peek preview', async ({
const toolbar = page.locator('affine-toolbar-widget editor-toolbar'); const toolbar = page.locator('affine-toolbar-widget editor-toolbar');
await expect(toolbar).toBeVisible(); await expect(toolbar).toBeVisible();
await toolbar.getByLabel('Open doc').click(); await toolbar.getByLabel(/^Open doc$/).click();
await toolbar.getByLabel('Open in center peek').click(); await toolbar
.getByLabel('Open doc menu')
.getByLabel('Open in center peek')
.click();
const peekView = page.getByTestId('peek-view-modal'); const peekView = page.getByTestId('peek-view-modal');
await expect(peekView).toBeVisible(); await expect(peekView).toBeVisible();

View File

@@ -156,7 +156,7 @@ test('not allowed to switch to embed view when linking to block', async ({
await cardLink.click(); await cardLink.click();
await toolbar.getByLabel('more-menu').click(); await toolbar.getByLabel('More menu').click();
await toolbar.getByLabel('Copy link to block').click(); await toolbar.getByLabel('Copy link to block').click();
await page.keyboard.press('Enter'); await page.keyboard.press('Enter');
@@ -1035,3 +1035,89 @@ test.describe('Customize linked doc title and description', () => {
await expect(a.locator('.affine-reference-title')).toContainText(year); await expect(a.locator('.affine-reference-title')).toContainText(year);
}); });
}); });
test('should save open doc mode of internal links', async ({ page }) => {
await waitForEditorLoad(page);
await enableEmojiDocIcon(page);
await clickNewPageButton(page);
const title = getBlockSuiteEditorTitle(page);
await title.click();
await page.keyboard.press('Enter');
await createLinkedPage(page, 'Test Page');
const { toolbar, switchViewBtn, inlineViewBtn, cardViewBtn, embedViewBtn } =
toolbarButtons(page);
const inlineLink = page.locator('affine-reference');
await inlineLink.hover();
const recentOpenModeBtn = toolbar.getByLabel(/^Open/).nth(0);
await expect(recentOpenModeBtn).toHaveAttribute(
'aria-label',
'Open this doc'
);
await expect(
recentOpenModeBtn.locator('span.label:has-text("Open")')
).toBeVisible();
const openDocBtn = toolbar.getByLabel(/^Open doc$/);
await openDocBtn.click();
const openDocMenu = toolbar.getByLabel('Open doc menu');
const openThisDocBtn = openDocMenu.getByLabel('Open this doc');
// In Desktop
const openInSplitViewBtn = openDocMenu.getByLabel('Open in split view');
const openInNewTabBtn = openDocMenu.getByLabel('Open in new tab');
const openInCenterPeekBtn = openDocMenu.getByLabel('Open in center peek');
await expect(openThisDocBtn).toBeVisible();
await expect(openInSplitViewBtn).toBeHidden();
await expect(openInNewTabBtn).toBeVisible();
await expect(openInCenterPeekBtn).toBeVisible();
await openInCenterPeekBtn.click();
await page.keyboard.press('Escape');
await inlineLink.hover();
await expect(toolbar).toBeVisible();
await expect(recentOpenModeBtn).toHaveAttribute(
'aria-label',
'Open in center peek'
);
await switchViewBtn.click();
await cardViewBtn.click();
await expect(toolbar).toBeVisible();
await expect(recentOpenModeBtn).toHaveAttribute(
'aria-label',
'Open in center peek'
);
await switchViewBtn.click();
await embedViewBtn.click();
await expect(toolbar).toBeVisible();
await expect(recentOpenModeBtn).toHaveAttribute(
'aria-label',
'Open in center peek'
);
await switchViewBtn.click();
await inlineViewBtn.click();
await inlineLink.hover();
await page.waitForTimeout(250);
await expect(toolbar).toBeVisible();
await expect(recentOpenModeBtn).toHaveAttribute(
'aria-label',
'Open in center peek'
);
});

View File

@@ -26,9 +26,11 @@ test('can open peek view via link popover', async ({ page }) => {
const toolbar = page.locator('affine-toolbar-widget editor-toolbar'); const toolbar = page.locator('affine-toolbar-widget editor-toolbar');
await expect(toolbar).toBeVisible(); await expect(toolbar).toBeVisible();
// click more button await toolbar.getByLabel(/^Open doc$/).click();
await toolbar.getByLabel('Open doc').click(); await toolbar
await toolbar.getByLabel('Open in center peek').click(); .getByLabel('Open doc menu')
.getByLabel('Open in center peek')
.click();
// verify peek view is opened // verify peek view is opened
await expect(page.getByTestId('peek-view-modal')).toBeVisible(); await expect(page.getByTestId('peek-view-modal')).toBeVisible();

View File

@@ -108,7 +108,7 @@ test.describe('frame copy and paste', () => {
await frameTitles.nth(0).click(); await frameTitles.nth(0).click();
const moreMenu = page.getByLabel('more-menu'); const moreMenu = page.getByLabel('More menu');
await moreMenu.click(); await moreMenu.click();
await moreMenu await moreMenu

View File

@@ -262,7 +262,7 @@ test('duplicate note should work correctly', async ({ page }) => {
await triggerComponentToolbarAction(page, 'duplicate'); await triggerComponentToolbarAction(page, 'duplicate');
await waitNextFrame(page, 200); // wait viewport fit animation await waitNextFrame(page, 200); // wait viewport fit animation
const moreActionsContainer = page.getByLabel('more-menu').getByRole('menu'); const moreActionsContainer = page.getByLabel('More menu').getByRole('menu');
await expect(moreActionsContainer).toBeHidden(); await expect(moreActionsContainer).toBeHidden();
const noteLocator = page.locator('affine-edgeless-note'); const noteLocator = page.locator('affine-edgeless-note');

View File

@@ -797,7 +797,7 @@ export async function updateExistedBrushElementSize(
export async function openComponentToolbarMoreMenu(page: Page) { export async function openComponentToolbarMoreMenu(page: Page) {
const btn = page const btn = page
.locator('affine-toolbar-widget editor-toolbar') .locator('affine-toolbar-widget editor-toolbar')
.getByLabel('more-menu'); .getByLabel('More menu');
await btn.click(); await btn.click();
} }
@@ -1023,7 +1023,7 @@ export function locatorComponentToolbar(page: Page) {
} }
export function locatorComponentToolbarMoreButton(page: Page) { export function locatorComponentToolbarMoreButton(page: Page) {
const moreButton = locatorComponentToolbar(page).getByLabel('more-menu'); const moreButton = locatorComponentToolbar(page).getByLabel('More menu');
return moreButton; return moreButton;
} }
type Action = type Action =