Peng Xiao 2024-02-22 09:37:55 +00:00
parent bb8e601f82
commit 372b4da884
No known key found for this signature in database
GPG Key ID: 23F23D9E8B3971ED
19 changed files with 574 additions and 114 deletions

View File

@ -0,0 +1,58 @@
import { ConfirmModal } from '@affine/component';
import { WorkspacePropertiesAdapter } from '@affine/core/modules/workspace';
import type { PageInfoCustomPropertyMeta } from '@affine/core/modules/workspace/properties/schema';
import { Trans } from '@affine/i18n';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { useService } from '@toeverything/infra/di';
import { useMemo } from 'react';
import { PagePropertiesMetaManager } from './page-properties-manager';
export const ConfirmDeletePropertyModal = ({
onConfirm,
onCancel,
property,
show,
}: {
property: PageInfoCustomPropertyMeta;
show: boolean;
onConfirm: () => void;
onCancel: () => void;
}) => {
const t = useAFFiNEI18N();
const adapter = useService(WorkspacePropertiesAdapter);
const count = useMemo(() => {
const manager = new PagePropertiesMetaManager(adapter);
return manager.getPropertyRelatedPages(property.id)?.size || 0;
}, [adapter, property.id]);
return (
<ConfirmModal
open={show}
closeButtonOptions={{
onClick: onCancel,
}}
title={t['com.affine.settings.workspace.properties.delete-property']()}
description={
<Trans
values={{
name: property.name,
count,
}}
i18nKey="com.affine.settings.workspace.properties.delete-property-prompt"
>
The <strong>{{ name: property.name } as any}</strong> property will be
removed from count doc(s). This action cannot be undone.
</Trans>
}
onConfirm={onConfirm}
cancelButtonOptions={{
onClick: onCancel,
}}
confirmButtonOptions={{
type: 'error',
children: t['Confirm'](),
}}
/>
);
};

View File

@ -34,7 +34,7 @@ export const IconsSelectorPanel = ({
const t = useAFFiNEI18N(); const t = useAFFiNEI18N();
return ( return (
<Scrollable.Root> <Scrollable.Root>
<div className={styles.menuHeader}> <div role="heading" className={styles.menuHeader}>
{t['com.affine.page-properties.icons']()} {t['com.affine.page-properties.icons']()}
</div> </div>
<Scrollable.Viewport className={styles.iconsContainerScrollable}> <Scrollable.Viewport className={styles.iconsContainerScrollable}>

View File

@ -90,7 +90,9 @@ export const EditPropertyNameMenuItem = ({
const iconName = getSafeIconName(property.icon, property.type); const iconName = getSafeIconName(property.icon, property.type);
const onKeyDown: KeyboardEventHandler<HTMLInputElement> = useCallback( const onKeyDown: KeyboardEventHandler<HTMLInputElement> = useCallback(
e => { e => {
if (e.key !== 'Escape') {
e.stopPropagation(); e.stopPropagation();
}
if (e.key === 'Enter') { if (e.key === 'Enter') {
e.preventDefault(); e.preventDefault();
onBlur(e.currentTarget.value); onBlur(e.currentTarget.value);

View File

@ -132,6 +132,10 @@ export class PagePropertiesMetaManager {
} }
return mapping; return mapping;
} }
getPropertyRelatedPages(id: string) {
return this.getPropertyStatistics().get(id);
}
} }
export class PagePropertiesManager { export class PagePropertiesManager {

View File

@ -212,7 +212,7 @@ export const propertyRowCell = style({
position: 'relative', position: 'relative',
borderRadius: 4, borderRadius: 4,
fontSize: cssVar('fontSm'), fontSize: cssVar('fontSm'),
lineHeight: '20px', lineHeight: '22px',
userSelect: 'none', userSelect: 'none',
':focus-visible': { ':focus-visible': {
outline: 'none', outline: 'none',

View File

@ -60,6 +60,7 @@ import {
import { AffinePageReference } from '../reference-link'; import { AffinePageReference } from '../reference-link';
import { managerContext, pageInfoCollapsedAtom } from './common'; import { managerContext, pageInfoCollapsedAtom } from './common';
import { ConfirmDeletePropertyModal } from './confirm-delete-property-modal';
import { import {
getDefaultIconName, getDefaultIconName,
nameToIcon, nameToIcon,
@ -281,7 +282,11 @@ const VisibilityModeSelector = ({
}, },
}} }}
> >
<div data-required={required} className={styles.selectorButton}> <div
role="button"
data-required={required}
className={styles.selectorButton}
>
{required ? ( {required ? (
t['com.affine.page-properties.property.required']() t['com.affine.page-properties.property.required']()
) : ( ) : (
@ -305,7 +310,11 @@ export const PagePropertiesSettingsPopup = ({
const menuItems = useMemo(() => { const menuItems = useMemo(() => {
const options: MenuItemOption[] = []; const options: MenuItemOption[] = [];
options.push( options.push(
<div className={styles.menuHeader} style={{ minWidth: 320 }}> <div
role="heading"
className={styles.menuHeader}
style={{ minWidth: 320 }}
>
{t['com.affine.page-properties.settings.title']()} {t['com.affine.page-properties.settings.title']()}
</div> </div>
); );
@ -327,7 +336,12 @@ export const PagePropertiesSettingsPopup = ({
<MenuIcon> <MenuIcon>
<Icon /> <Icon />
</MenuIcon> </MenuIcon>
<div className={styles.propertyRowName}>{name}</div> <div
data-testid="page-property-setting-row-name"
className={styles.propertyRowName}
>
{name}
</div>
<VisibilityModeSelector property={property} /> <VisibilityModeSelector property={property} />
</SortablePropertyRow> </SortablePropertyRow>
); );
@ -407,6 +421,8 @@ export const PagePropertyRowNameMenu = ({
const [localProperty, setLocalProperty] = useState(() => ({ ...property })); const [localProperty, setLocalProperty] = useState(() => ({ ...property }));
const nextVisibility = rotateVisibility(localProperty.visibility); const nextVisibility = rotateVisibility(localProperty.visibility);
const [showDeleteModal, setShowDeleteModal] = useState(false);
const handleFinishEditing = useCallback(() => { const handleFinishEditing = useCallback(() => {
onFinishEditing(); onFinishEditing();
manager.updateCustomPropertyMeta(meta.id, localPropertyMeta); manager.updateCustomPropertyMeta(meta.id, localPropertyMeta);
@ -445,14 +461,9 @@ export const PagePropertyRowNameMenu = ({
}, },
[nextVisibility] [nextVisibility]
); );
const handleDelete = useCallback( const handleDelete = useCallback(() => {
(e: MouseEvent) => {
e.stopPropagation();
e.preventDefault();
manager.removeCustomProperty(property.id); manager.removeCustomProperty(property.id);
}, }, [manager, property.id]);
[manager, property.id]
);
const handleIconChange = useCallback( const handleIconChange = useCallback(
(icon: PagePropertyIcon) => { (icon: PagePropertyIcon) => {
@ -496,12 +507,11 @@ export const PagePropertyRowNameMenu = ({
type: 'danger', type: 'danger',
icon: <DeleteIcon />, icon: <DeleteIcon />,
text: t['com.affine.page-properties.property.remove-property'](), text: t['com.affine.page-properties.property.remove-property'](),
onClick: handleDelete, onClick: () => setShowDeleteModal(true),
}); });
} }
return renderMenuItemOptions(options); return renderMenuItemOptions(options);
}, [ }, [
handleDelete,
handleIconChange, handleIconChange,
handleNameBlur, handleNameBlur,
handleNameChange, handleNameChange,
@ -513,6 +523,7 @@ export const PagePropertyRowNameMenu = ({
]); ]);
return ( return (
<>
<Menu <Menu
rootOptions={{ rootOptions={{
open: editing, open: editing,
@ -522,11 +533,26 @@ export const PagePropertyRowNameMenu = ({
onClick(e) { onClick(e) {
e.stopPropagation(); e.stopPropagation();
}, },
onKeyDown(e) {
if (e.key === 'Escape') {
handleFinishEditing();
}
},
}} }}
items={menuItems} items={menuItems}
> >
{children} {children}
</Menu> </Menu>
<ConfirmDeletePropertyModal
onConfirm={() => {
setShowDeleteModal(false);
handleDelete();
}}
onCancel={() => setShowDeleteModal(false)}
show={showDeleteModal}
property={meta}
/>
</>
); );
}; };
@ -607,7 +633,11 @@ export const PagePropertiesTableHeader = ({
</div> </div>
{properties.length === 0 || manager.readonly ? null : ( {properties.length === 0 || manager.readonly ? null : (
<PagePropertiesSettingsPopup> <PagePropertiesSettingsPopup>
<IconButton type="plain" icon={<MoreHorizontalIcon />} /> <IconButton
data-testid="page-info-show-more"
type="plain"
icon={<MoreHorizontalIcon />}
/>
</PagePropertiesSettingsPopup> </PagePropertiesSettingsPopup>
)} )}
<div className={styles.spacer} /> <div className={styles.spacer} />
@ -802,7 +832,7 @@ export const PagePropertiesCreatePropertyMenuItems = ({
return useMemo(() => { return useMemo(() => {
const options: MenuItemOption[] = []; const options: MenuItemOption[] = [];
options.push( options.push(
<div className={styles.menuHeader}> <div role="heading" className={styles.menuHeader}>
{t['com.affine.page-properties.create-property.menu.header']()} {t['com.affine.page-properties.create-property.menu.header']()}
</div> </div>
); );
@ -864,7 +894,7 @@ const PagePropertiesAddPropertyMenuItems = ({
const menuItems = useMemo(() => { const menuItems = useMemo(() => {
const options: MenuItemOption[] = []; const options: MenuItemOption[] = [];
options.push( options.push(
<div className={styles.menuHeader}> <div role="heading" className={styles.menuHeader}>
{t['com.affine.page-properties.add-property.menu.header']()} {t['com.affine.page-properties.add-property.menu.header']()}
</div> </div>
); );

View File

@ -47,7 +47,7 @@ const InlineTagsList = ({
children, children,
}: PropsWithChildren<InlineTagsListProps>) => { }: PropsWithChildren<InlineTagsListProps>) => {
return ( return (
<div className={styles.inlineTagsContainer}> <div className={styles.inlineTagsContainer} data-testid="inline-tags-list">
{value.map((tagId, idx) => { {value.map((tagId, idx) => {
const tag = options.find(t => t.id === tagId); const tag = options.find(t => t.id === tagId);
if (!tag) { if (!tag) {
@ -251,7 +251,7 @@ export const TagsEditor = ({
); );
return ( return (
<div className={styles.tagsEditorRoot}> <div data-testid="tags-editor-popup" className={styles.tagsEditorRoot}>
<div className={styles.tagsEditorSelectedTags}> <div className={styles.tagsEditorSelectedTags}>
<InlineTagsList <InlineTagsList
options={options} options={options}
@ -282,6 +282,9 @@ export const TagsEditor = ({
<div <div
key={tag.id} key={tag.id}
className={styles.tagSelectorItem} className={styles.tagSelectorItem}
data-testid="tag-selector-item"
data-tag-id={tag.id}
data-tag-value={tag.value}
onClick={() => { onClick={() => {
onAddTag(tag.id); onAddTag(tag.id);
}} }}
@ -296,6 +299,7 @@ export const TagsEditor = ({
})} })}
{exactMatch || !inputValue ? null : ( {exactMatch || !inputValue ? null : (
<div <div
data-testid="tag-selector-item"
className={styles.tagSelectorItem} className={styles.tagSelectorItem}
onClick={() => { onClick={() => {
setInputValue(''); setInputValue('');

View File

@ -269,6 +269,7 @@ const WorkspaceListItem = ({
.map(({ key, title }) => { .map(({ key, title }) => {
return ( return (
<div <div
data-testid={`workspace-list-item-${key}`}
onClick={() => { onClick={() => {
onClick(key); onClick(key);
}} }}

View File

@ -1,4 +1,4 @@
import { Button, ConfirmModal, IconButton, Menu } from '@affine/component'; import { Button, IconButton, Menu } from '@affine/component';
import { SettingHeader } from '@affine/component/setting-components'; import { SettingHeader } from '@affine/component/setting-components';
import { useWorkspacePropertiesAdapter } from '@affine/core/hooks/use-affine-adapter'; import { useWorkspacePropertiesAdapter } from '@affine/core/hooks/use-affine-adapter';
import { useWorkspace } from '@affine/core/hooks/use-workspace'; import { useWorkspace } from '@affine/core/hooks/use-workspace';
@ -28,6 +28,7 @@ import {
PagePropertiesMetaManager, PagePropertiesMetaManager,
type PagePropertyIcon, type PagePropertyIcon,
} from '../../../page-properties'; } from '../../../page-properties';
import { ConfirmDeletePropertyModal } from '../../../page-properties/confirm-delete-property-modal';
import { import {
EditPropertyNameMenuItem, EditPropertyNameMenuItem,
type MenuItemOption, type MenuItemOption,
@ -54,57 +55,9 @@ const Divider = () => {
return <div className={styles.divider} />; return <div className={styles.divider} />;
}; };
const ConfirmDeletePropertyModal = ({
onConfirm,
onCancel,
property,
count,
show,
}: {
property: PageInfoCustomPropertyMeta;
count: number;
show: boolean;
onConfirm: () => void;
onCancel: () => void;
}) => {
const t = useAFFiNEI18N();
return (
<ConfirmModal
open={show}
closeButtonOptions={{
onClick: onCancel,
}}
title={t['com.affine.settings.workspace.properties.delete-property']()}
description={
<Trans
values={{
name: property.name,
count,
}}
i18nKey="com.affine.settings.workspace.properties.delete-property-prompt"
>
The <strong>{{ name: property.name } as any}</strong> property will be
removed from count doc(s). This action cannot be undone.
</Trans>
}
onConfirm={onConfirm}
cancelButtonOptions={{
onClick: onCancel,
}}
confirmButtonOptions={{
type: 'error',
children: t['Confirm'](),
}}
/>
);
};
const EditPropertyButton = ({ const EditPropertyButton = ({
property, property,
count,
}: { }: {
count: number;
property: PageInfoCustomPropertyMeta; property: PageInfoCustomPropertyMeta;
}) => { }) => {
const t = useAFFiNEI18N(); const t = useAFFiNEI18N();
@ -234,7 +187,6 @@ const EditPropertyButton = ({
onCancel={() => setShowDeleteModal(false)} onCancel={() => setShowDeleteModal(false)}
show={showDeleteModal} show={showDeleteModal}
property={property} property={property}
count={count}
/> />
</> </>
); );
@ -281,7 +233,7 @@ const CustomPropertyRow = ({
{t['com.affine.page-properties.property.required']()} {t['com.affine.page-properties.property.required']()}
</div> </div>
) : null} ) : null}
<EditPropertyButton property={property} count={relatedPages.length} /> <EditPropertyButton property={property} />
</div> </div>
); );
}; };

View File

@ -44,6 +44,8 @@ export const TagItem = ({
data-testid="page-tag" data-testid="page-tag"
className={styles.tag} className={styles.tag}
data-idx={idx} data-idx={idx}
data-tag-id={tag.id}
data-tag-value={tag.value}
title={tag.value} title={tag.value}
style={style} style={style}
> >
@ -59,7 +61,11 @@ export const TagItem = ({
/> />
<div className={styles.tagLabel}>{tag.value}</div> <div className={styles.tagLabel}>{tag.value}</div>
{onRemoved ? ( {onRemoved ? (
<div className={styles.tagRemove} onClick={handleRemove}> <div
data-testid="remove-tag-button"
className={styles.tagRemove}
onClick={handleRemove}
>
<CloseIcon /> <CloseIcon />
</div> </div>
) : null} ) : null}

View File

@ -1107,7 +1107,7 @@
"com.affine.page-properties.backlinks": "Backlinks", "com.affine.page-properties.backlinks": "Backlinks",
"com.affine.page-properties.page-info": "Page Info", "com.affine.page-properties.page-info": "Page Info",
"com.affine.page-properties.settings.title": "customize properties", "com.affine.page-properties.settings.title": "customize properties",
"com.affine.page-properties.add-property": "Add a property", "com.affine.page-properties.add-property": "Add property",
"com.affine.page-properties.add-property.menu.header": "Properties", "com.affine.page-properties.add-property.menu.header": "Properties",
"com.affine.page-properties.create-property.menu.header": "Type", "com.affine.page-properties.create-property.menu.header": "Type",
"com.affine.page-properties.add-property.menu.create": "Create property", "com.affine.page-properties.add-property.menu.create": "Create property",

View File

@ -106,7 +106,7 @@ test('use monthpicker to modify the month of datepicker', async ({ page }) => {
await checkDatePickerMonth(page, nextMonth); await checkDatePickerMonth(page, nextMonth);
}); });
test.skip('allow creation of filters by tags', async ({ page }) => { test('allow creation of filters by tags', async ({ page }) => {
await openHomePage(page); await openHomePage(page);
await waitForEditorLoad(page); await waitForEditorLoad(page);
await clickSideBarAllPageButton(page); await clickSideBarAllPageButton(page);

View File

@ -137,27 +137,6 @@ test('edit collection and change filter date', async ({ page }) => {
expect(await first.textContent()).toBe('123'); expect(await first.textContent()).toBe('123');
}); });
test.skip('create temporary filter by click tag', async ({ page }) => {
await clickNewPageButton(page);
await getBlockSuiteEditorTitle(page).click();
await getBlockSuiteEditorTitle(page).fill('test page');
await page.locator('page-meta-tags').click();
await page.locator('.add-tag').click();
await page.keyboard.type('TODO Tag');
await page.keyboard.press('Enter');
await page.keyboard.press('Escape');
await page.locator('.tag', { hasText: 'TODO Tag' }).click();
const cell = page.getByTestId('page-list-item-title').getByText('test page');
await expect(cell).toBeVisible();
expect(await page.getByTestId('page-list-item').count()).toBe(1);
await page.getByTestId('filter-arg').click();
await page.getByTestId('multi-select-TODO Tag').click();
expect(
await page.getByTestId('page-list-item').count()
).toBeGreaterThanOrEqual(2);
});
test('add collection from sidebar', async ({ page }) => { test('add collection from sidebar', async ({ page }) => {
await removeOnboardingPages(page); await removeOnboardingPages(page);
await clickNewPageButton(page); await clickNewPageButton(page);

View File

@ -48,6 +48,7 @@ test('page delete -> create new page -> refresh page -> new page should be appea
page, page,
workspace, workspace,
}) => { }) => {
test.slow();
await openHomePage(page); await openHomePage(page);
await waitForEditorLoad(page); await waitForEditorLoad(page);
await clickNewPageButton(page); await clickNewPageButton(page);

View File

@ -1,4 +1,5 @@
import { test } from '@affine-test/kit/playwright'; import { test } from '@affine-test/kit/playwright';
import { clickPageModeButton } from '@affine-test/kit/utils/editor';
import { openHomePage } from '@affine-test/kit/utils/load-page'; import { openHomePage } from '@affine-test/kit/utils/load-page';
import { import {
clickNewPageButton, clickNewPageButton,
@ -135,8 +136,9 @@ test("Deleted page's reference will not be shown in sidebar", async ({
test('Add new favorite page via sidebar', async ({ page }) => { test('Add new favorite page via sidebar', async ({ page }) => {
await openHomePage(page); await openHomePage(page);
await waitForEditorLoad(page); await waitForEditorLoad(page);
await clickNewPageButton(page);
await page.getByTestId('slider-bar-add-favorite-button').first().click(); await page.getByTestId('slider-bar-add-favorite-button').first().click();
await clickPageModeButton(page);
await waitForEmptyEditor(page); await waitForEmptyEditor(page);
// enter random page title // enter random page title

View File

@ -0,0 +1,243 @@
/* eslint-disable unicorn/prefer-dom-node-dataset */
import { test } from '@affine-test/kit/playwright';
import { clickPageModeButton } from '@affine-test/kit/utils/editor';
import { openHomePage } from '@affine-test/kit/utils/load-page';
import { dragTo, waitForEditorLoad } from '@affine-test/kit/utils/page-logic';
import {
addCustomProperty,
changePropertyVisibility,
clickPropertyValue,
closeTagsEditor,
expectTagsVisible,
getPropertyValueLocator,
openTagsEditor,
openWorkspaceProperties,
removeSelectedTag,
searchAndCreateTag,
} from '@affine-test/kit/utils/properties';
import { expect } from '@playwright/test';
test.beforeEach(async ({ page }) => {
await openHomePage(page);
await clickPageModeButton(page);
await waitForEditorLoad(page);
});
test('allow create tag', async ({ page }) => {
await openTagsEditor(page);
await searchAndCreateTag(page, 'Test1');
await searchAndCreateTag(page, 'Test2');
await closeTagsEditor(page);
await expectTagsVisible(page, ['Test1', 'Test2']);
await openTagsEditor(page);
await removeSelectedTag(page, 'Test1');
await closeTagsEditor(page);
await expectTagsVisible(page, ['Test2']);
});
test('add custom property', async ({ page }) => {
await addCustomProperty(page, 'Text');
await addCustomProperty(page, 'Number');
await addCustomProperty(page, 'Date');
await addCustomProperty(page, 'Checkbox');
});
test('add custom property & edit', async ({ page }) => {
await addCustomProperty(page, 'Checkbox');
await expect(
getPropertyValueLocator(page, 'Checkbox').locator('input')
).not.toBeChecked();
await clickPropertyValue(page, 'Checkbox');
await expect(
getPropertyValueLocator(page, 'Checkbox').locator('input')
).toBeChecked();
});
test('property table reordering', async ({ page }) => {
await addCustomProperty(page, 'Text');
await addCustomProperty(page, 'Number');
await addCustomProperty(page, 'Date');
await addCustomProperty(page, 'Checkbox');
await dragTo(
page,
page.locator('[data-testid="page-property-row-name"]:has-text("Text")'),
page.locator(
'[data-testid="page-property-row-name"]:has-text("Checkbox") + div'
)
);
// new order should be (Tags), Number, Date, Checkbox, Text
for (const [index, property] of [
'Tags',
'Number',
'Date',
'Checkbox',
'Text',
].entries()) {
await expect(
page
.getByTestId('page-property-row')
.nth(index)
.getByTestId('page-property-row-name')
).toHaveText(property);
}
});
test('page info show more will not should by default when there is no properties', async ({
page,
}) => {
// by default page info show more should not show
await expect(page.getByTestId('page-info-show-more')).not.toBeVisible();
});
test('page info show more will show all properties', async ({ page }) => {
await addCustomProperty(page, 'Text');
await addCustomProperty(page, 'Number');
await addCustomProperty(page, 'Date');
await addCustomProperty(page, 'Checkbox');
await expect(page.getByTestId('page-info-show-more')).toBeVisible();
await page.click('[data-testid="page-info-show-more"]');
await expect(
page.getByRole('heading', {
name: 'customize properties',
})
).toBeVisible();
// new order should be (Tags), Number, Date, Checkbox, Text
for (const [index, property] of [
'Text',
'Number',
'Date',
'Checkbox',
].entries()) {
await expect(
page
.getByTestId('page-properties-settings-menu-item')
.nth(index)
.getByTestId('page-property-setting-row-name')
).toHaveText(property);
}
});
test('change page properties visibility', async ({ page }) => {
await addCustomProperty(page, 'Text');
await addCustomProperty(page, 'Number');
await addCustomProperty(page, 'Date');
await addCustomProperty(page, 'Checkbox');
// add some number to number property
await clickPropertyValue(page, 'Number');
await page.locator('input[type=number]').fill('123');
await changePropertyVisibility(page, 'Text', 'Hide in view');
await changePropertyVisibility(page, 'Number', 'Hide in view when empty');
// text property should not be visible
await expect(
page.locator('[data-testid="page-property-row-name"]:has-text("Text")')
).not.toBeVisible();
// number property should be visible
await expect(
page.locator('[data-testid="page-property-row-name"]:has-text("Number")')
).toBeVisible();
});
test('check if added property is also in workspace settings', async ({
page,
}) => {
await addCustomProperty(page, 'Text');
await openWorkspaceProperties(page);
await expect(
page.locator('[data-testid=custom-property-row]:has-text("Text")')
).toBeVisible();
});
test('edit property name', async ({ page }) => {
await addCustomProperty(page, 'Text');
await page
.locator('[data-testid="page-property-row-name"]:has-text("Text")')
.click();
await expect(page.locator('[data-radix-menu-content]')).toBeVisible();
await expect(page.locator('[data-radix-menu-content] input')).toHaveValue(
'Text'
);
await page.locator('[data-radix-menu-content] input').fill('New Text');
await page.keyboard.press('Enter');
await expect(page.locator('[data-radix-menu-content] input')).toHaveValue(
'New Text'
);
await page.keyboard.press('Escape');
// check if the property name is also updated in workspace settings
await openWorkspaceProperties(page);
await expect(
page.locator('[data-testid=custom-property-row]:has-text("New Text")')
).toBeVisible();
});
test('delete property via property popup', async ({ page }) => {
await addCustomProperty(page, 'Text');
await page
.locator('[data-testid="page-property-row-name"]:has-text("Text")')
.click();
await expect(page.locator('[data-radix-menu-content]')).toBeVisible();
await page
.locator('[data-radix-menu-content]')
.getByRole('menuitem', {
name: 'Remove property',
})
.click();
// confirm delete dialog should show
await expect(page.getByRole('dialog')).toContainText(
`The "Text" property will be remove from 1 doc(s). This action cannot be undone.`
);
await page
.getByRole('button', {
name: 'Confirm',
})
.click();
// check if the property is removed
await expect(
page.locator('[data-testid="page-property-row-name"]:has-text("Text")')
).not.toBeVisible();
});
test('create a required property', async ({ page }) => {
await openWorkspaceProperties(page);
await addCustomProperty(page, 'Text', true);
await page
.locator('[data-testid="custom-property-row"]:has-text("Text")')
.getByRole('button')
.click();
await page
.getByRole('menuitem', {
name: 'Set as required property',
})
.click();
await expect(
page.locator('[data-testid="custom-property-row"]:has-text("Text")')
).toContainText('Required');
// close workspace settings
await page.keyboard.press('Escape');
// check if the property is also required in page properties
await expect(
page.locator('[data-testid="page-property-row-name"]:has-text("Text")')
).toBeVisible();
// check if the required property is also listed in the show more menu
await page.click('[data-testid="page-info-show-more"]');
await expect(
page.locator(
'[data-testid="page-properties-settings-menu-item"]:has-text("Text")'
)
).toContainText('Required');
});

View File

@ -126,8 +126,9 @@ export const createPageWithTag = async (
await clickNewPageButton(page); await clickNewPageButton(page);
await getBlockSuiteEditorTitle(page).click(); await getBlockSuiteEditorTitle(page).click();
await getBlockSuiteEditorTitle(page).fill('test page'); await getBlockSuiteEditorTitle(page).fill('test page');
await page.locator('page-meta-tags').click(); await page
await page.locator('.add-tag').click(); .locator('[data-testid="page-property-row"][data-property="tags"]')
.click();
for (const name of options.tags) { for (const name of options.tags) {
await createTag(page, name); await createTag(page, name);
} }

View File

@ -26,7 +26,6 @@ export async function clickNewPageButton(page: Page) {
await page.getByTestId('sidebar-new-page-button').click({ await page.getByTestId('sidebar-new-page-button').click({
delay: 100, delay: 100,
}); });
await expect(page.locator('.doc-title-container-empty')).toBeVisible();
await waitForEmptyEditor(page); await waitForEmptyEditor(page);
} }

View File

@ -0,0 +1,178 @@
import { expect, type Page } from '@playwright/test';
export const getPropertyValueLocator = (page: Page, property: string) => {
return page.locator(
`[data-testid="page-property-row-name"]:has-text("${property}") + *`
);
};
export const clickPropertyValue = async (page: Page, property: string) => {
await getPropertyValueLocator(page, property).click();
};
export const openTagsEditor = async (page: Page) => {
await clickPropertyValue(page, 'tags');
await expect(page.getByTestId('tags-editor-popup')).toBeVisible();
};
export const closeTagsEditor = async (page: Page) => {
await page.keyboard.press('Escape');
await expect(page.getByTestId('tags-editor-popup')).not.toBeVisible();
};
export const clickTagFromSelector = async (page: Page, name: string) => {
// assume that the tags editor is already open
await page
.locator(`[data-testid="tag-selector-item"][data-tag-value="${name}"]`)
.click();
};
export const removeSelectedTag = async (page: Page, name: string) => {
await page
.locator(
`[data-testid="tags-editor-popup"] [data-testid="inline-tags-list"] [data-tag-value="${name}"] [data-testid="remove-tag-button"]`
)
.click();
};
export const filterTags = async (page: Page, filter: string) => {
await page
.locator(
'[data-testid="tags-editor-popup"] [data-testid="inline-tags-list"] input'
)
.fill(filter);
};
export const searchAndCreateTag = async (page: Page, name: string) => {
await filterTags(page, name);
await page
.locator(
'[data-testid="tags-editor-popup"] [data-testid="tag-selector-item"]:has-text("Create ")'
)
.click();
};
export const expectTagsVisible = async (page: Page, tags: string[]) => {
const tagListPanel = page
.getByTestId('page-property-row')
.getByTestId('inline-tags-list');
expect(await tagListPanel.locator('[data-tag-value]').count()).toBe(
tags.length
);
for (const tag of tags) {
await expect(
tagListPanel.locator(`[data-tag-value="${tag}"]`)
).toBeVisible();
}
};
export const clickAddPropertyButton = async (page: Page) => {
await page
.getByRole('button', {
name: 'Add property',
})
.click();
};
export const addCustomProperty = async (
page: Page,
type: string,
inSettings?: boolean
) => {
await clickAddPropertyButton(page);
if (!inSettings) {
await expect(
page.getByRole('heading', {
name: 'Properties',
})
).toBeVisible();
await page
.getByRole('menuitem', {
name: 'Create property',
})
.click();
}
await expect(
page.getByRole('heading', {
name: 'Type',
})
).toBeVisible();
await page
.getByRole('menuitem', {
name: type,
})
.click();
if (!inSettings) {
await expect(
page
.getByRole('menuitem', {
name: type,
})
.locator('.selected')
).toBeVisible();
await page.keyboard.press('Escape', {
delay: 100,
});
}
};
export const expectPropertyOrdering = async (
page: Page,
properties: string[]
) => {
for (let i = 0; i < properties.length; i++) {
await expect(
page.locator(`[data-testid="page-property-row-name"])`).nth(i)
).toHaveText(properties[i]);
}
};
export const openWorkspaceProperties = async (page: Page) => {
await page.getByTestId('slider-bar-workspace-setting-button').click();
await page
.locator('[data-testid="workspace-list-item"] .setting-name')
.click();
await page.getByTestId('workspace-list-item-properties').click();
};
export const selectVisibilitySelector = async (
page: Page,
name: string,
option: string
) => {
await page
.getByRole('menu')
.locator(
`[data-testid="page-properties-settings-menu-item"]:has-text("${name}")`
)
.getByRole('button')
.click();
await page
.getByRole('menu')
.last()
.getByRole('menuitem', {
name: option,
exact: true,
})
.click();
await page.keyboard.press('Escape');
};
export const changePropertyVisibility = async (
page: Page,
name: string,
option: string
) => {
await expect(page.getByTestId('page-info-show-more')).toBeVisible();
await page.click('[data-testid="page-info-show-more"]');
await expect(
page.getByRole('heading', {
name: 'customize properties',
})
).toBeVisible();
await selectVisibilitySelector(page, name, option);
};