diff --git a/packages/frontend/component/src/ui/button/button.css.ts b/packages/frontend/component/src/ui/button/button.css.ts index 21d9349c6d..d113bfc2d1 100644 --- a/packages/frontend/component/src/ui/button/button.css.ts +++ b/packages/frontend/component/src/ui/button/button.css.ts @@ -9,7 +9,7 @@ export const button = style({ flexShrink: 0, outline: '0', border: '1px solid', - padding: '0 18px', + padding: '0 8px', borderRadius: '8px', fontSize: cssVar('fontXs'), fontWeight: 500, diff --git a/packages/frontend/component/src/ui/input/style.css.ts b/packages/frontend/component/src/ui/input/style.css.ts index fe474a2a22..254e993ef8 100644 --- a/packages/frontend/component/src/ui/input/style.css.ts +++ b/packages/frontend/component/src/ui/input/style.css.ts @@ -4,7 +4,7 @@ export const inputWrapper = style({ width: '100%', height: 28, lineHeight: '22px', - padding: '0 10px', + gap: '10px', color: cssVar('textPrimaryColor'), border: '1px solid', backgroundColor: cssVar('white'), @@ -53,6 +53,7 @@ export const input = style({ width: '0', flex: 1, boxSizing: 'border-box', + padding: '0 12px', // prevent default style WebkitAppearance: 'none', WebkitTapHighlightColor: 'transparent', diff --git a/packages/frontend/component/src/ui/menu/styles.css.ts b/packages/frontend/component/src/ui/menu/styles.css.ts index df06dbdfa8..1e7b498256 100644 --- a/packages/frontend/component/src/ui/menu/styles.css.ts +++ b/packages/frontend/component/src/ui/menu/styles.css.ts @@ -13,7 +13,6 @@ export const menuContent = style({ userSelect: 'none', }); export const menuItem = style({ - maxWidth: '296px', display: 'flex', alignItems: 'center', justifyContent: 'space-between', @@ -50,6 +49,9 @@ export const menuItem = style({ color: cssVar('warningColor'), backgroundColor: cssVar('backgroundWarningColor'), }, + '&.checked': { + color: cssVar('primaryColor'), + }, }, }); export const menuSpan = style({ diff --git a/packages/frontend/core/src/components/affine/page-properties/icons-mapping.tsx b/packages/frontend/core/src/components/affine/page-properties/icons-mapping.tsx index abd5c5ab5f..a0258476c7 100644 --- a/packages/frontend/core/src/components/affine/page-properties/icons-mapping.tsx +++ b/packages/frontend/core/src/components/affine/page-properties/icons-mapping.tsx @@ -1,40 +1,47 @@ -import type { PagePropertyType } from '@affine/core/modules/workspace/properties/schema'; +import { PagePropertyType } from '@affine/core/modules/workspace/properties/schema'; import * as icons from '@blocksuite/icons'; import type { SVGProps } from 'react'; type IconType = (props: SVGProps) => JSX.Element; // todo: this breaks tree-shaking, and we should fix it (using dynamic imports?) -const IconsMapping = { - text: icons.TextIcon, - tag: icons.TagIcon, - dateTime: icons.DateTimeIcon, - progress: icons.ProgressIcon, - checkbox: icons.CheckBoxCheckLinearIcon, - number: icons.NumberIcon, - // todo: add more icons -} satisfies Record; - +const IconsMapping = icons; export type PagePropertyIcon = keyof typeof IconsMapping; +const excludedIcons: PagePropertyIcon[] = [ + 'YoutubeDuotoneIcon', + 'LinearLogoIcon', + 'RedditDuotoneIcon', + 'Logo2Icon', + 'Logo3Icon', + 'Logo4Icon', + 'InstagramDuotoneIcon', + 'TelegramDuotoneIcon', + 'TextBackgroundDuotoneIcon', +]; + +export const iconNames = Object.keys(IconsMapping).filter( + icon => !excludedIcons.includes(icon as PagePropertyIcon) +) as PagePropertyIcon[]; + export const getDefaultIconName = ( type: PagePropertyType ): PagePropertyIcon => { switch (type) { case 'text': - return 'text'; + return 'TextIcon'; case 'tags': - return 'tag'; + return 'TagIcon'; case 'date': - return 'dateTime'; + return 'DateTimeIcon'; case 'progress': - return 'progress'; + return 'ProgressIcon'; case 'checkbox': - return 'checkbox'; + return 'CheckBoxCheckLinearIcon'; case 'number': - return 'number'; + return 'NumberIcon'; default: - return 'text'; + return 'TextIcon'; } }; @@ -46,12 +53,18 @@ export const IconToIconName = (icon: IconType) => { return iconKey; }; +export const getSafeIconName = ( + iconName: string, + type?: PagePropertyType +): PagePropertyIcon => { + return Object.hasOwn(IconsMapping, iconName) + ? (iconName as PagePropertyIcon) + : getDefaultIconName(type || PagePropertyType.Text); +}; + export const nameToIcon = ( iconName: string, - type: PagePropertyType + type?: PagePropertyType ): IconType => { - return ( - IconsMapping[iconName as keyof typeof IconsMapping] ?? - getDefaultIconName(type) - ); + return IconsMapping[getSafeIconName(iconName, type)]; }; diff --git a/packages/frontend/core/src/components/affine/page-properties/icons-selector.css.ts b/packages/frontend/core/src/components/affine/page-properties/icons-selector.css.ts new file mode 100644 index 0000000000..189ad976bb --- /dev/null +++ b/packages/frontend/core/src/components/affine/page-properties/icons-selector.css.ts @@ -0,0 +1,79 @@ +import { cssVar } from '@toeverything/theme'; +import { style } from '@vanilla-extract/css'; + +export const iconsContainer = style({ + display: 'flex', + flexDirection: 'column', + gap: '2px', +}); + +export const iconsRow = style({ + display: 'flex', + flexDirection: 'row', + alignItems: 'center', + color: cssVar('iconColor'), + fontSize: cssVar('fontSm'), + fontWeight: 500, + padding: '0 6px', + gap: '8px', +}); + +export const iconButton = style({ + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + fontSize: 20, + borderRadius: '4px', + width: 28, + height: 28, + cursor: 'pointer', + transition: 'background-color 0.2s', + ':hover': { + backgroundColor: cssVar('hoverColor'), + }, + selectors: { + '&[data-active=true]': { + color: cssVar('primaryColor'), + }, + }, +}); + +export const iconSelectorButton = style({ + fontSize: cssVar('fontH5'), + borderRadius: 4, + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + width: 32, + height: 32, + flexShrink: 0, + border: `1px solid ${cssVar('borderColor')}`, + background: cssVar('backgroundSecondaryColor'), + cursor: 'pointer', + ':hover': { + backgroundColor: cssVar('backgroundTertiaryColor'), + }, +}); + +export const iconsContainerScrollable = style({ + maxHeight: 320, + display: 'flex', + flexDirection: 'column', +}); + +export const iconsContainerScroller = style({ + transform: 'translateX(4px)', +}); + +export const menuHeader = style({ + display: 'flex', + flexDirection: 'row', + alignItems: 'center', + gap: '8px', + fontSize: cssVar('fontXs'), + fontWeight: 500, + color: cssVar('textSecondaryColor'), + padding: '8px 12px', + minWidth: 200, + textTransform: 'uppercase', +}); diff --git a/packages/frontend/core/src/components/affine/page-properties/icons-selector.tsx b/packages/frontend/core/src/components/affine/page-properties/icons-selector.tsx new file mode 100644 index 0000000000..bdc231ab85 --- /dev/null +++ b/packages/frontend/core/src/components/affine/page-properties/icons-selector.tsx @@ -0,0 +1,93 @@ +import { Menu, Scrollable } from '@affine/component'; +import { useAFFiNEI18N } from '@affine/i18n/hooks'; +import { chunk } from 'lodash-es'; +import { useEffect, useRef } from 'react'; + +import { iconNames, nameToIcon, type PagePropertyIcon } from './icons-mapping'; +import * as styles from './icons-selector.css'; + +const iconsPerRow = 10; + +const iconRows = chunk(iconNames, iconsPerRow); + +export const IconsSelectorPanel = ({ + selected, + onSelectedChange, +}: { + selected: PagePropertyIcon; + onSelectedChange: (icon: PagePropertyIcon) => void; +}) => { + const ref = useRef(null); + useEffect(() => { + if (!ref.current) { + return; + } + const iconButton = ref.current.querySelector( + `[data-name="${selected}"]` + ) as HTMLDivElement; + if (!iconButton) { + return; + } + iconButton.scrollIntoView({ block: 'center' }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + const t = useAFFiNEI18N(); + return ( + +
+ {t['com.affine.page-properties.icons']()} +
+ +
+ {iconRows.map((iconRow, index) => { + return ( +
+ {iconRow.map(iconName => { + const Icon = nameToIcon(iconName); + return ( +
+ onSelectedChange(iconName)} + /> +
+ ); + })} +
+ ); + })} +
+ +
+
+ ); +}; + +export const IconsSelectorButton = ({ + selected, + onSelectedChange, +}: { + selected: PagePropertyIcon; + onSelectedChange: (icon: PagePropertyIcon) => void; +}) => { + const Icon = nameToIcon(selected); + return ( + + } + > +
+ +
+
+ ); +}; diff --git a/packages/frontend/core/src/components/affine/page-properties/menu-items.tsx b/packages/frontend/core/src/components/affine/page-properties/menu-items.tsx new file mode 100644 index 0000000000..97cef77c95 --- /dev/null +++ b/packages/frontend/core/src/components/affine/page-properties/menu-items.tsx @@ -0,0 +1,125 @@ +import { + Input, + MenuIcon, + MenuItem, + type MenuItemProps, + MenuSeparator, + Scrollable, +} from '@affine/component'; +import type { PageInfoCustomPropertyMeta } from '@affine/core/modules/workspace/properties/schema'; +import { useAFFiNEI18N } from '@affine/i18n/hooks'; +import { + type ChangeEventHandler, + cloneElement, + isValidElement, + type MouseEventHandler, +} from 'react'; + +import { + getDefaultIconName, + getSafeIconName, + nameToIcon, + type PagePropertyIcon, +} from './icons-mapping'; +import { IconsSelectorButton } from './icons-selector'; +import * as styles from './styles.css'; +export type MenuItemOption = + | React.ReactElement + | '-' + | { + text: string; + onClick: MouseEventHandler; + key?: string; + icon?: React.ReactElement; + selected?: boolean; + checked?: boolean; + type?: MenuItemProps['type']; + } + | MenuItemOption[]; + +const isElementOption = (e: MenuItemOption): e is React.ReactElement => { + return isValidElement(e); +}; + +export const renderMenuItemOptions = (options: MenuItemOption[]) => { + return options.map((option, index) => { + if (option === '-') { + return ; + } else if (isElementOption(option)) { + return cloneElement(option, { key: index }); + } else if (Array.isArray(option)) { + // this is an area that needs scrollbar + return ( + + + {renderMenuItemOptions(option)} + + + + ); + } else { + const { text, icon, onClick, type, key, checked, selected } = option; + return ( + {icon} : null} + onClick={onClick} + > + {text} + + ); + } + }); +}; + +export const EditPropertyNameMenuItem = ({ + property, + onNameBlur: onBlur, + onNameChange, + onIconChange, +}: { + onNameBlur: ChangeEventHandler; + onNameChange: (name: string) => void; + onIconChange: (icon: PagePropertyIcon) => void; + property: PageInfoCustomPropertyMeta; +}) => { + const iconName = getSafeIconName(property.icon, property.type); + const t = useAFFiNEI18N(); + return ( +
+ + +
+ ); +}; + +export const PropertyTypeMenuItem = ({ + property, +}: { + property: PageInfoCustomPropertyMeta; +}) => { + const Icon = nameToIcon(getDefaultIconName(property.type), property.type); + const t = useAFFiNEI18N(); + return ( +
+ {t['com.affine.page-properties.create-property.menu.header']()} +
+ + {t[`com.affine.page-properties.property.${property.type}`]()} +
+
+ ); +}; diff --git a/packages/frontend/core/src/components/affine/page-properties/page-properties-manager.ts b/packages/frontend/core/src/components/affine/page-properties/page-properties-manager.ts index 500345fc51..da2f0b17d4 100644 --- a/packages/frontend/core/src/components/affine/page-properties/page-properties-manager.ts +++ b/packages/frontend/core/src/components/affine/page-properties/page-properties-manager.ts @@ -29,29 +29,11 @@ function validatePropertyValue(type: PagePropertyType, value: any) { } } -export interface NewPropertyOption { - name: string; - type: PagePropertyType; -} - -export const newPropertyOptions: NewPropertyOption[] = [ - // todo: name i18n? - { - name: 'Text', - type: PagePropertyType.Text, - }, - { - name: 'Number', - type: PagePropertyType.Number, - }, - { - name: 'Checkbox', - type: PagePropertyType.Checkbox, - }, - { - name: 'Date', - type: PagePropertyType.Date, - }, +export const newPropertyTypes: PagePropertyType[] = [ + PagePropertyType.Text, + PagePropertyType.Number, + PagePropertyType.Checkbox, + PagePropertyType.Date, // todo: add more ]; @@ -121,6 +103,21 @@ export class PagePropertiesMetaManager { return property; } + updateCustomPropertyMeta( + id: string, + opt: Partial + ) { + if (!this.checkPropertyExists(id)) { + logger.warn(`property ${id} not found`); + return; + } + Object.assign(this.customPropertiesSchema[id], opt); + } + + isPropertyRequired(id: string) { + return this.customPropertiesSchema[id]?.required; + } + removeCustomPropertyMeta(id: string) { // should warn if the property is in use delete this.customPropertiesSchema[id]; @@ -136,6 +133,7 @@ export class PagePropertiesMetaManager { mapping.get(id)?.add(page.id); } } + return mapping; } } @@ -149,6 +147,20 @@ export class PagePropertiesManager { this.metaManager = new PagePropertiesMetaManager(this.adapter); } + private ensuring = false; + ensureRequiredProperties() { + if (this.ensuring) return; + this.ensuring = true; + this.transact(() => { + this.metaManager.getOrderedCustomPropertiesSchema().forEach(property => { + if (property.required && !this.hasCustomProperty(property.id)) { + this.addCustomProperty(property.id); + } + }); + }); + this.ensuring = false; + } + get workspace() { return this.adapter.workspace; } @@ -174,6 +186,7 @@ export class PagePropertiesManager { } get properties() { + this.ensureRequiredProperties(); return this.adapter.getPageProperties(this.pageId); } @@ -192,7 +205,7 @@ export class PagePropertiesManager { /** * get custom properties (filter out properties that are not in schema) */ - getCustomProperties() { + getCustomProperties(): Record { return Object.fromEntries( Object.entries(this.properties.custom).filter(([id]) => this.metaManager.checkPropertyExists(id) @@ -213,13 +226,6 @@ export class PagePropertiesManager { ); } - leastOrder() { - return Math.min( - ...Object.values(this.properties.custom).map(p => p.order), - 0 - ); - } - getCustomPropertyMeta(id: string): PageInfoCustomPropertyMeta | undefined { return this.metaManager.customPropertiesSchema[id]; } @@ -275,15 +281,12 @@ export class PagePropertiesManager { Object.assign(this.properties.custom[id], opt); } - updateCustomPropertyMeta( - id: string, - opt: Partial - ) { - if (!this.metaManager.checkPropertyExists(id)) { - logger.warn(`property ${id} not found`); - return; - } - Object.assign(this.metaManager.customPropertiesSchema[id], opt); + get updateCustomPropertyMeta() { + return this.metaManager.updateCustomPropertyMeta.bind(this.metaManager); + } + + get isPropertyRequired() { + return this.metaManager.isPropertyRequired.bind(this.metaManager); } transact = this.adapter.transact; diff --git a/packages/frontend/core/src/components/affine/page-properties/property-row-values.tsx b/packages/frontend/core/src/components/affine/page-properties/property-row-value-renderer.tsx similarity index 100% rename from packages/frontend/core/src/components/affine/page-properties/property-row-values.tsx rename to packages/frontend/core/src/components/affine/page-properties/property-row-value-renderer.tsx diff --git a/packages/frontend/core/src/components/affine/page-properties/styles.css.ts b/packages/frontend/core/src/components/affine/page-properties/styles.css.ts index 6ad0a09672..c62c70ceec 100644 --- a/packages/frontend/core/src/components/affine/page-properties/styles.css.ts +++ b/packages/frontend/core/src/components/affine/page-properties/styles.css.ts @@ -19,6 +19,11 @@ export const rootCentered = style({ width: '100%', maxWidth: cssVar('editorWidth'), padding: `0 ${cssVar('editorSidePadding', '24px')}`, + '@container': { + [`viewport (width <= 640px)`]: { + padding: '0 24px', + }, + }, }); export const tableHeader = style({ @@ -226,7 +231,6 @@ export const propertyRowIconContainer = style({ justifyContent: 'center', borderRadius: '2px', fontSize: 16, - transition: 'transform 0.2s', color: 'inherit', }); @@ -277,13 +281,11 @@ export const menuHeader = style({ fontWeight: 500, color: cssVar('textSecondaryColor'), padding: '8px 16px', - minWidth: 320, + minWidth: 200, textTransform: 'uppercase', }); -export const menuItemListScrollable = style({ - maxHeight: 300, -}); +export const menuItemListScrollable = style({}); export const menuItemListScrollbar = style({ transform: 'translateX(4px)', @@ -296,7 +298,7 @@ export const menuItemList = style({ overflow: 'auto', }); -globalStyle(`${menuItemList}${menuItemList} > div`, { +globalStyle(`${menuItemList}[data-radix-scroll-area-viewport] > div`, { display: 'table !important', }); @@ -318,30 +320,6 @@ export const checkboxProperty = style({ fontSize: cssVar('fontH5'), }); -export const propertyNameIconEditable = style({ - fontSize: cssVar('fontH5'), - borderRadius: 4, - display: 'flex', - alignItems: 'center', - justifyContent: 'center', - width: 32, - height: 32, - flexShrink: 0, - border: `1px solid ${cssVar('borderColor')}`, - background: cssVar('backgroundSecondaryColor'), -}); - -export const propertyNameInput = style({ - fontSize: cssVar('fontSm'), - borderRadius: 4, - color: cssVar('textPrimaryColor'), - background: 'none', - border: `1px solid ${cssVar('borderColor')}`, - outline: 'none', - width: '100%', - padding: 6, -}); - globalStyle( `${propertyRow}:is([data-dragging=true], [data-other-dragging=true]) :is(${propertyRowValueCell}, ${propertyRowNameCell})`, @@ -387,7 +365,7 @@ export const propertySettingRowName = style({ export const selectorButton = style({ display: 'flex', alignItems: 'center', - justifyContent: 'center', + justifyContent: 'flex-end', borderRadius: 4, gap: 8, fontSize: cssVar('fontSm'), @@ -397,4 +375,29 @@ export const selectorButton = style({ ':hover': { backgroundColor: cssVar('hoverColor'), }, + selectors: { + '&[data-required=true]': { + color: cssVar('textDisableColor'), + pointerEvents: 'none', + }, + }, +}); + +export const propertyRowTypeItem = style({ + display: 'flex', + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + gap: '8px', + fontSize: cssVar('fontSm'), + padding: '8px 16px', + minWidth: 260, +}); + +export const propertyTypeName = style({ + color: cssVar('textSecondaryColor'), + fontSize: cssVar('fontXs'), + display: 'flex', + alignItems: 'center', + gap: 4, }); diff --git a/packages/frontend/core/src/components/affine/page-properties/table.tsx b/packages/frontend/core/src/components/affine/page-properties/table.tsx index fc8bb3af33..2d550f3ddd 100644 --- a/packages/frontend/core/src/components/affine/page-properties/table.tsx +++ b/packages/frontend/core/src/components/affine/page-properties/table.tsx @@ -4,7 +4,6 @@ import { Menu, MenuIcon, MenuItem, - Scrollable, Tooltip, } from '@affine/component'; import { useCurrentWorkspacePropertiesAdapter } from '@affine/core/hooks/use-affine-adapter'; @@ -12,6 +11,7 @@ import { useBlockSuitePageBacklinks } from '@affine/core/hooks/use-block-suite-p import type { PageInfoCustomProperty, PageInfoCustomPropertyMeta, + PagePropertyType, } from '@affine/core/modules/workspace/properties/schema'; import { timestampToLocalDate } from '@affine/core/utils'; import { useAFFiNEI18N } from '@affine/i18n/hooks'; @@ -54,19 +54,28 @@ import { useCallback, useContext, useMemo, - useRef, useState, } from 'react'; import { AffinePageReference } from '../reference-link'; import { managerContext, pageInfoCollapsedAtom } from './common'; -import { getDefaultIconName, nameToIcon } from './icons-mapping'; import { - type NewPropertyOption, - newPropertyOptions, + getDefaultIconName, + nameToIcon, + type PagePropertyIcon, +} from './icons-mapping'; +import { + EditPropertyNameMenuItem, + type MenuItemOption, + PropertyTypeMenuItem, + renderMenuItemOptions, +} from './menu-items'; +import type { PagePropertiesMetaManager } from './page-properties-manager'; +import { + newPropertyTypes, PagePropertiesManager, } from './page-properties-manager'; -import { propertyValueRenderers } from './property-row-values'; +import { propertyValueRenderers } from './property-row-value-renderer'; import * as styles from './styles.css'; type PagePropertiesSettingsPopupProps = PropsWithChildren<{ @@ -220,37 +229,36 @@ const VisibilityModeSelector = ({ const manager = useContext(managerContext); const t = useAFFiNEI18N(); const meta = manager.getCustomPropertyMeta(property.id); + const visibility = property.visibility || 'visible'; + + const menuItems = useMemo(() => { + const options: MenuItemOption[] = []; + options.push( + visibilities.map(v => { + const text = visibilityMenuText(v); + return { + text: t[text](), + selected: visibility === v, + onClick: () => { + manager.updateCustomProperty(property.id, { + visibility: v, + }); + }, + }; + }) + ); + return renderMenuItemOptions(options); + }, [manager, property.id, t, visibility]); if (!meta) { return null; } const required = meta.required; - const visibility = property.visibility || 'visible'; return ( - {visibilities.map(v => { - const text = visibilitySelectorText(v); - return ( - { - manager.updateCustomProperty(property.id, { - visibility: v, - }); - }} - > - {t[text]()} - - ); - })} - - } + items={menuItems} rootOptions={{ open: required ? false : undefined, }} @@ -276,46 +284,42 @@ export const PagePropertiesSettingsPopup = ({ const t = useAFFiNEI18N(); const properties = manager.getOrderedCustomProperties(); - return ( - -
- {t['com.affine.page-properties.settings.title']()} -
- - - - - {properties.map(property => { - const meta = manager.getCustomPropertyMeta(property.id); - assertExists(meta, 'meta should exist for property'); - const Icon = nameToIcon(meta.icon, meta.type); - const name = meta.name; - return ( - - - - -
{name}
- -
- ); - })} -
-
-
- - } - > - {children} -
- ); + const menuItems = useMemo(() => { + const options: MenuItemOption[] = []; + options.push( +
+ {t['com.affine.page-properties.settings.title']()} +
+ ); + options.push('-'); + options.push([ + + {properties.map(property => { + const meta = manager.getCustomPropertyMeta(property.id); + assertExists(meta, 'meta should exist for property'); + const Icon = nameToIcon(meta.icon, meta.type); + const name = meta.name; + return ( + + + + +
{name}
+ +
+ ); + })} +
, + ]); + return renderMenuItemOptions(options); + }, [manager, properties, t]); + + return {children}; }; type PageBacklinksPopupProps = PropsWithChildren<{ @@ -363,18 +367,24 @@ export const PagePropertyRowName = ({ children, }: PropsWithChildren) => { const manager = useContext(managerContext); - const Icon = nameToIcon(meta.icon, meta.type); - const localPropertyMetaRef = useRef({ ...meta }); - const localPropertyRef = useRef({ ...property }); - const [nextVisibility, setNextVisibility] = useState(property.visibility); - const toHide = - nextVisibility === 'hide' || nextVisibility === 'hide-if-empty'; + const [localPropertyMeta, setLocalPropertyMeta] = useState(() => ({ + ...meta, + })); + const [localProperty, setLocalProperty] = useState(() => ({ ...property })); + const nextVisibility = rotateVisibility(localProperty.visibility); const handleFinishEditing = useCallback(() => { onFinishEditing(); - manager.updateCustomPropertyMeta(meta.id, localPropertyMetaRef.current); - manager.updateCustomProperty(property.id, localPropertyRef.current); - }, [manager, meta.id, onFinishEditing, property.id]); + manager.updateCustomPropertyMeta(meta.id, localPropertyMeta); + manager.updateCustomProperty(property.id, localProperty); + }, [ + localProperty, + localPropertyMeta, + manager, + meta.id, + onFinishEditing, + property.id, + ]); const t = useAFFiNEI18N(); const handleNameBlur: ChangeEventHandler = useCallback( e => { @@ -385,21 +395,23 @@ export const PagePropertyRowName = ({ }, [manager, meta.id] ); - const handleNameChange: ChangeEventHandler = useCallback( - e => { - localPropertyMetaRef.current.name = e.target.value; - }, - [] - ); - const toggleHide = useCallback((e: MouseEvent) => { - e.stopPropagation(); - e.preventDefault(); - const nextVisibility = rotateVisibility( - localPropertyRef.current.visibility - ); - setNextVisibility(nextVisibility); - localPropertyRef.current.visibility = nextVisibility; + const handleNameChange: (name: string) => void = useCallback(name => { + setLocalPropertyMeta(prev => ({ + ...prev, + name: name, + })); }, []); + const toggleHide = useCallback( + (e: MouseEvent) => { + e.stopPropagation(); + e.preventDefault(); + setLocalProperty(prev => ({ + ...prev, + visibility: nextVisibility, + })); + }, + [nextVisibility] + ); const handleDelete = useCallback( (e: MouseEvent) => { e.stopPropagation(); @@ -409,6 +421,64 @@ export const PagePropertyRowName = ({ [manager, property.id] ); + const handleIconChange = useCallback( + (icon: PagePropertyIcon) => { + setLocalPropertyMeta(prev => ({ + ...prev, + icon, + })); + manager.updateCustomPropertyMeta(meta.id, { + icon: icon, + }); + }, + [manager, meta.id] + ); + + const menuItems = useMemo(() => { + const options: MenuItemOption[] = []; + options.push( + + ); + options.push(); + if (!localPropertyMeta.required) { + options.push('-'); + options.push({ + icon: + nextVisibility === 'hide' || nextVisibility === 'hide-if-empty' ? ( + + ) : ( + + ), + text: t[ + visibilityMenuText(rotateVisibility(localProperty.visibility)) + ](), + onClick: toggleHide, + }); + options.push({ + type: 'danger', + icon: , + text: t['com.affine.page-properties.property.remove-property'](), + onClick: handleDelete, + }); + } + return renderMenuItemOptions(options); + }, [ + handleDelete, + handleIconChange, + handleNameBlur, + handleNameChange, + localProperty.visibility, + localPropertyMeta, + nextVisibility, + t, + toggleHide, + ]); + return ( -
-
- -
- -
- - {!toHide ? : } - } - data-testid="page-property-row-name-hide-menu-item" - onClick={toggleHide} - > - {t[visibilityMenuText(nextVisibility)]()} - - - - - } - data-testid="page-property-row-name-delete-menu-item" - onClick={handleDelete} - > - {t['com.affine.page-properties.property.remove-property']()} - - - } + items={menuItems} > {children}
@@ -641,7 +675,10 @@ export const PagePropertiesTableBody = ({ style, }: PagePropertiesTableBodyProps) => { const manager = useContext(managerContext); - const properties = manager.getOrderedCustomProperties(); + + const properties = useMemo(() => { + return manager.getOrderedCustomProperties(); + }, [manager]); return ( - property.visibility !== 'hide' && - !(property.visibility === 'hide-if-empty' && !property.value) + manager.isPropertyRequired(property.id) || + (property.visibility !== 'hide' && + !(property.visibility === 'hide-if-empty' && !property.value)) ) .map(property => ( @@ -668,6 +706,7 @@ export const PagePropertiesTableBody = ({ interface PagePropertiesCreatePropertyMenuItemsProps { onCreated?: (e: React.MouseEvent, id: string) => void; + metaManager: PagePropertiesMetaManager; } const findNextDefaultName = (name: string, allNames: string[]): string => { @@ -688,63 +727,62 @@ const findNextDefaultName = (name: string, allNames: string[]): string => { export const PagePropertiesCreatePropertyMenuItems = ({ onCreated, + metaManager, }: PagePropertiesCreatePropertyMenuItemsProps) => { - const manager = useContext(managerContext); const t = useAFFiNEI18N(); const onAddProperty = useCallback( - (e: React.MouseEvent, option: NewPropertyOption & { icon: string }) => { - const nameExists = manager.metaManager - .getOrderedCustomPropertiesSchema() - .some(meta => meta.name === option.name); - const allNames = manager.metaManager - .getOrderedCustomPropertiesSchema() - .map(meta => meta.name); + ( + e: React.MouseEvent, + option: { type: PagePropertyType; name: string; icon: string } + ) => { + const schemaList = metaManager.getOrderedCustomPropertiesSchema(); + const nameExists = schemaList.some(meta => meta.name === option.name); + const allNames = schemaList.map(meta => meta.name); const name = nameExists ? findNextDefaultName(option.name, allNames) : option.name; - const { id } = manager.metaManager.addCustomPropertyMeta({ + const { id } = metaManager.addCustomPropertyMeta({ name, icon: option.icon, type: option.type, }); onCreated?.(e, id); }, - [manager.metaManager, onCreated] + [metaManager, onCreated] ); - return ( - <> + + return useMemo(() => { + const options: MenuItemOption[] = []; + options.push(
{t['com.affine.page-properties.create-property.menu.header']()}
- -
- {newPropertyOptions.map(({ name, type }) => { - const iconName = getDefaultIconName(type); - const Icon = nameToIcon(iconName, type); - return ( - - - - } - data-testid="page-properties-create-property-menu-item" - onClick={e => { - onAddProperty(e, { icon: iconName, name, type }); - }} - > - {name} - - ); - })} -
- - ); + ); + options.push('-'); + options.push( + newPropertyTypes.map(type => { + const iconName = getDefaultIconName(type); + const Icon = nameToIcon(iconName, type); + const name = t[`com.affine.page-properties.property.${type}`](); + return { + icon: , + text: name, + onClick: (e: React.MouseEvent) => { + onAddProperty(e, { + icon: iconName, + name: name, + type: type, + }); + }, + }; + }) + ); + return renderMenuItemOptions(options); + }, [onAddProperty, t]); }; interface PagePropertiesAddPropertyMenuItemsProps { - onCreateClicked?: (e: React.MouseEvent) => void; + onCreateClicked: (e: React.MouseEvent) => void; } const PagePropertiesAddPropertyMenuItems = ({ @@ -754,6 +792,7 @@ const PagePropertiesAddPropertyMenuItems = ({ const t = useAFFiNEI18N(); const metaList = manager.metaManager.getOrderedCustomPropertiesSchema(); + const nonRequiredMetaList = metaList.filter(meta => !meta.required); const isChecked = useCallback( (m: string) => { return manager.hasCustomProperty(m); @@ -774,59 +813,41 @@ const PagePropertiesAddPropertyMenuItems = ({ [isChecked, manager] ); - return ( - <> + const menuItems = useMemo(() => { + const options: MenuItemOption[] = []; + options.push(
{t['com.affine.page-properties.add-property.menu.header']()}
- {/* hide available properties if there are none */} - {metaList.length > 0 ? ( - <> - - - - {metaList.map(meta => { - const Icon = nameToIcon(meta.icon, meta.type); - const name = meta.name; - return ( - - - - } - data-testid="page-properties-add-property-menu-item" - data-property={meta.id} - checked={isChecked(meta.id)} - onClick={(e: React.MouseEvent) => - onClickProperty(e, meta.id) - } - > - {name} - - ); - })} - - - - - ) : null} - - - - + ); + + if (nonRequiredMetaList.length > 0) { + options.push('-'); + const nonRequiredMetaOptions: MenuItemOption = nonRequiredMetaList.map( + meta => { + const Icon = nameToIcon(meta.icon, meta.type); + const name = meta.name; + return { + icon: , + text: name, + selected: isChecked(meta.id), + onClick: (e: React.MouseEvent) => onClickProperty(e, meta.id), + }; } - > -
- {t['com.affine.page-properties.add-property.menu.create']()} -
-
- - ); + ); + options.push(nonRequiredMetaOptions); + } + options.push('-'); + options.push({ + icon: , + text: t['com.affine.page-properties.add-property.menu.create'](), + onClick: onCreateClicked, + }); + + return renderMenuItemOptions(options); + }, [isChecked, nonRequiredMetaList, onClickProperty, onCreateClicked, t]); + + return menuItems; }; export const PagePropertiesAddProperty = () => { @@ -848,7 +869,10 @@ export const PagePropertiesAddProperty = () => { const items = adding ? ( ) : ( - + ); return ( setAdding(true) }} items={items}> @@ -882,6 +906,13 @@ const PagePropertiesTableInner = () => { export const PagePropertiesTable = ({ page }: { page: Page }) => { const manager = usePagePropertiesManager(page); + // if the given page is not in the current workspace, then we don't render anything + // eg. when it is in history modal + + if (!manager.page) { + return null; + } + return ( diff --git a/packages/frontend/core/src/components/affine/setting-modal/setting-sidebar/index.tsx b/packages/frontend/core/src/components/affine/setting-modal/setting-sidebar/index.tsx index 4369c937e8..34a0432607 100644 --- a/packages/frontend/core/src/components/affine/setting-modal/setting-sidebar/index.tsx +++ b/packages/frontend/core/src/components/affine/setting-modal/setting-sidebar/index.tsx @@ -223,6 +223,10 @@ const subTabConfigs = [ key: 'experimental-features', title: 'com.affine.settings.workspace.experimental-features', }, + { + key: 'properties', + title: 'com.affine.settings.workspace.properties', + }, ] satisfies { key: WorkspaceSubTab; title: keyof ReturnType; diff --git a/packages/frontend/core/src/components/affine/setting-modal/types.ts b/packages/frontend/core/src/components/affine/setting-modal/types.ts index d6e6d604ad..8f60d151a5 100644 --- a/packages/frontend/core/src/components/affine/setting-modal/types.ts +++ b/packages/frontend/core/src/components/affine/setting-modal/types.ts @@ -9,6 +9,7 @@ export const GeneralSettingKeys = [ export const WorkspaceSubTabs = [ 'preference', 'experimental-features', + 'properties', ] as const; export type GeneralSettingKey = (typeof GeneralSettingKeys)[number]; diff --git a/packages/frontend/core/src/components/affine/setting-modal/workspace-setting/index.tsx b/packages/frontend/core/src/components/affine/setting-modal/workspace-setting/index.tsx index b59e406944..45a7fabba2 100644 --- a/packages/frontend/core/src/components/affine/setting-modal/workspace-setting/index.tsx +++ b/packages/frontend/core/src/components/affine/setting-modal/workspace-setting/index.tsx @@ -3,13 +3,14 @@ import type { WorkspaceMetadata } from '@toeverything/infra'; import { useIsWorkspaceOwner } from '../../../../hooks/affine/use-is-workspace-owner'; import { ExperimentalFeatures } from './experimental-features'; import { WorkspaceSettingDetail } from './new-workspace-setting-detail'; +import { WorkspaceSettingProperties } from './properties'; export const WorkspaceSetting = ({ workspaceMetadata, subTab, }: { workspaceMetadata: WorkspaceMetadata; - subTab: 'preference' | 'experimental-features'; + subTab: 'preference' | 'experimental-features' | 'properties'; }) => { const isOwner = useIsWorkspaceOwner(workspaceMetadata); @@ -23,5 +24,9 @@ export const WorkspaceSetting = ({ ); case 'experimental-features': return ; + case 'properties': + return ( + + ); } }; diff --git a/packages/frontend/core/src/components/affine/setting-modal/workspace-setting/properties/index.tsx b/packages/frontend/core/src/components/affine/setting-modal/workspace-setting/properties/index.tsx new file mode 100644 index 0000000000..0bc73cbf8e --- /dev/null +++ b/packages/frontend/core/src/components/affine/setting-modal/workspace-setting/properties/index.tsx @@ -0,0 +1,421 @@ +import { Button, IconButton, Menu } from '@affine/component'; +import { SettingHeader } from '@affine/component/setting-components'; +import { useWorkspacePropertiesAdapter } from '@affine/core/hooks/use-affine-adapter'; +import { useWorkspace } from '@affine/core/hooks/use-workspace'; +import { useWorkspaceInfo } from '@affine/core/hooks/use-workspace-info'; +import type { PageInfoCustomPropertyMeta } from '@affine/core/modules/workspace/properties/schema'; +import { Trans } from '@affine/i18n'; +import { useAFFiNEI18N } from '@affine/i18n/hooks'; +import { DeleteIcon, FilterIcon, MoreHorizontalIcon } from '@blocksuite/icons'; +import type { + Workspace, + WorkspaceMetadata, +} from '@toeverything/infra/workspace'; +import { + type ChangeEventHandler, + createContext, + Fragment, + type MouseEvent, + type MouseEventHandler, + useCallback, + useContext, + useEffect, + useMemo, + useState, +} from 'react'; + +import { + nameToIcon, + PagePropertiesCreatePropertyMenuItems, + PagePropertiesMetaManager, + type PagePropertyIcon, +} from '../../../page-properties'; +import { + EditPropertyNameMenuItem, + type MenuItemOption, + PropertyTypeMenuItem, + renderMenuItemOptions, +} from '../../../page-properties/menu-items'; +import * as styles from './styles.css'; + +// @ts-expect-error this should always be set +const managerContext = createContext(); + +const usePagePropertiesMetaManager = (workspace: Workspace) => { + // the workspace properties adapter adapter is reactive, + // which means it's reference will change when any of the properties change + // also it will trigger a re-render of the component + const adapter = useWorkspacePropertiesAdapter(workspace); + const manager = useMemo(() => { + return new PagePropertiesMetaManager(adapter); + }, [adapter]); + return manager; +}; + +const Divider = () => { + return
; +}; + +const EditPropertyButton = ({ + property, +}: { + property: PageInfoCustomPropertyMeta; +}) => { + const t = useAFFiNEI18N(); + const manager = useContext(managerContext); + const [localPropertyMeta, setLocalPropertyMeta] = useState(() => ({ + ...property, + })); + useEffect(() => { + setLocalPropertyMeta(property); + }, [property]); + const handleToggleRequired: MouseEventHandler = useCallback( + e => { + e.stopPropagation(); + e.preventDefault(); + manager.updateCustomPropertyMeta(localPropertyMeta.id, { + required: !localPropertyMeta.required, + }); + }, + [manager, localPropertyMeta.id, localPropertyMeta.required] + ); + const handleDelete: MouseEventHandler = useCallback( + e => { + e.stopPropagation(); + e.preventDefault(); + manager.removeCustomPropertyMeta(localPropertyMeta.id); + }, + [manager, localPropertyMeta.id] + ); + const [open, setOpen] = useState(false); + const [editing, setEditing] = useState(false); + + const handleFinishEditing = useCallback(() => { + setOpen(false); + setEditing(false); + manager.updateCustomPropertyMeta(localPropertyMeta.id, localPropertyMeta); + }, [localPropertyMeta, manager]); + + const defaultMenuItems = useMemo(() => { + const options: MenuItemOption[] = []; + options.push({ + text: t['com.affine.settings.workspace.properties.set-as-required'](), + onClick: handleToggleRequired, + checked: localPropertyMeta.required, + }); + options.push('-'); + options.push({ + text: t['com.affine.settings.workspace.properties.edit-property'](), + onClick: e => { + e.preventDefault(); + setEditing(true); + }, + }); + options.push({ + text: t['com.affine.settings.workspace.properties.delete-property'](), + onClick: handleDelete, + type: 'danger', + icon: , + }); + return renderMenuItemOptions(options); + }, [handleDelete, handleToggleRequired, localPropertyMeta.required, t]); + + const handleNameBlur: ChangeEventHandler = useCallback( + e => { + e.stopPropagation(); + manager.updateCustomPropertyMeta(localPropertyMeta.id, { + name: e.target.value, + }); + }, + [manager, localPropertyMeta.id] + ); + const handleNameChange: (name: string) => void = useCallback(name => { + setLocalPropertyMeta(prev => ({ + ...prev, + name: name, + })); + }, []); + const handleIconChange = useCallback( + (icon: PagePropertyIcon) => { + setLocalPropertyMeta(prev => ({ + ...prev, + icon, + })); + manager.updateCustomPropertyMeta(localPropertyMeta.id, { + icon, + }); + }, + [localPropertyMeta.id, manager] + ); + const editMenuItems = useMemo(() => { + const options: MenuItemOption[] = []; + options.push( + + ); + options.push(); + options.push('-'); + options.push({ + text: t['com.affine.settings.workspace.properties.delete-property'](), + onClick: handleDelete, + type: 'danger', + icon: , + }); + return renderMenuItemOptions(options); + }, [ + handleDelete, + handleIconChange, + handleNameBlur, + handleNameChange, + localPropertyMeta, + t, + ]); + + return ( + + setOpen(true)} + type="plain" + icon={} + /> + + ); +}; + +const CustomPropertyRow = ({ + property, + relatedPages, +}: { + relatedPages: string[]; + property: PageInfoCustomPropertyMeta; +}) => { + const Icon = nameToIcon(property.icon, property.type); + const required = property.required; + const t = useAFFiNEI18N(); + return ( +
+ +
+ {property.name || t['unnamed']()} +
+ {relatedPages.length > 0 ? ( +
+ ยท{' '} + 1 + ? 'com.affine.settings.workspace.properties.doc_others' + : 'com.affine.settings.workspace.properties.doc' + } + count={relatedPages.length} + > + {{ count: relatedPages.length } as any} doc + +
+ ) : null} +
+ {required ? ( +
+ {t['com.affine.page-properties.property.required']()} +
+ ) : null} + +
+ ); +}; + +const propertyFilterModes = ['all', 'in-use', 'unused'] as const; +type PropertyFilterMode = (typeof propertyFilterModes)[number]; + +const CustomPropertyRows = ({ + properties, + statistics, +}: { + properties: PageInfoCustomPropertyMeta[]; + statistics: Map>; +}) => { + return ( +
+ {properties.map(property => { + const pages = [...(statistics.get(property.id) ?? [])]; + return ( + + + + + ); + })} +
+ ); +}; + +const CustomPropertyRowsList = ({ + filterMode, +}: { + filterMode: PropertyFilterMode; +}) => { + const manager = useContext(managerContext); + const properties = manager.getOrderedCustomPropertiesSchema(); + const statistics = manager.getCustomPropertyStatistics(); + const t = useAFFiNEI18N(); + + if (filterMode !== 'all') { + const filtered = properties.filter(property => { + const count = statistics.get(property.id)?.size ?? 0; + return filterMode === 'in-use' ? count > 0 : count === 0; + }); + + return ; + } else { + const required = properties.filter(property => property.required); + const optional = properties.filter(property => !property.required); + return ( + <> + {required.length > 0 ? ( + <> +
+ {t[ + 'com.affine.settings.workspace.properties.required-properties' + ]()} +
+ + + ) : null} + + {optional.length > 0 ? ( + <> +
+ {t[ + 'com.affine.settings.workspace.properties.general-properties' + ]()} +
+ + + ) : null} + + ); + } +}; + +const WorkspaceSettingPropertiesMain = () => { + const t = useAFFiNEI18N(); + const manager = useContext(managerContext); + const [filterMode, setFilterMode] = useState('all'); + const properties = manager.getOrderedCustomPropertiesSchema(); + const filterMenuItems = useMemo(() => { + const options: MenuItemOption[] = ( + ['all', '-', 'in-use', 'unused'] as const + ).map(mode => { + return mode === '-' + ? '-' + : { + text: t[`com.affine.settings.workspace.properties.${mode}`](), + onClick: () => setFilterMode(mode), + checked: filterMode === mode, + }; + }); + return renderMenuItemOptions(options); + }, [filterMode, t]); + + const onPropertyCreated = useCallback((_e: MouseEvent, id: string) => { + setTimeout(() => { + const newRow = document.querySelector( + `[data-testid="custom-property-row"][data-property-id="${id}"]` + ); + if (newRow) { + newRow.scrollIntoView({ behavior: 'smooth' }); + newRow.dataset.highlight = ''; + setTimeout(() => { + delete newRow.dataset.highlight; + }, 3000); + } + }); + }, []); + return ( +
+
+ {properties.length > 0 ? ( + + + + ) : null} + + } + > + + +
+ +
+ ); +}; + +const WorkspaceSettingPropertiesInner = ({ + workspace, +}: { + workspace: Workspace; +}) => { + const manager = usePagePropertiesMetaManager(workspace); + return ( + + + + ); +}; + +export const WorkspaceSettingProperties = ({ + workspaceMetadata, +}: { + workspaceMetadata: WorkspaceMetadata; +}) => { + const t = useAFFiNEI18N(); + const workspace = useWorkspace(workspaceMetadata); + const workspaceInfo = useWorkspaceInfo(workspaceMetadata); + const title = workspaceInfo.name || 'untitled'; + + return ( + <> + + Manage workspace name properties + + } + /> + {workspace ? ( + + ) : null} + + ); +}; diff --git a/packages/frontend/core/src/components/affine/setting-modal/workspace-setting/properties/styles.css.ts b/packages/frontend/core/src/components/affine/setting-modal/workspace-setting/properties/styles.css.ts new file mode 100644 index 0000000000..ddeb7ba359 --- /dev/null +++ b/packages/frontend/core/src/components/affine/setting-modal/workspace-setting/properties/styles.css.ts @@ -0,0 +1,110 @@ +import { cssVar } from '@toeverything/theme'; +import { style } from '@vanilla-extract/css'; + +export const main = style({ + display: 'flex', + flexDirection: 'column', + width: '100%', + justifyContent: 'center', +}); + +export const listHeader = style({ + display: 'flex', + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + padding: '6px 0', + marginBottom: 16, +}); + +export const propertyRow = style({ + display: 'flex', + flexDirection: 'row', + alignItems: 'center', + padding: '6px 0', + gap: 6, + outline: 'none', + transition: 'background 0.2s ease', + selectors: { + '&[data-highlight]': { + background: cssVar('backgroundSecondaryColor'), + }, + }, +}); + +export const metaList = style({ + display: 'flex', + flexDirection: 'column', + gap: 4, + marginBottom: 16, +}); + +export const propertyIcon = style({ + color: cssVar('iconColor'), + fontSize: 16, +}); + +export const propertyName = style({ + color: cssVar('textPrimaryColor'), + fontSize: cssVar('fontSm'), + overflow: 'hidden', + textOverflow: 'ellipsis', + whiteSpace: 'nowrap', + selectors: { + '&[data-unnamed=true]': { + color: cssVar('placeholderColor'), + }, + }, +}); + +export const propertyDocCount = style({ + color: cssVar('textSecondaryColor'), + fontSize: cssVar('fontSm'), + whiteSpace: 'nowrap', +}); + +export const divider = style({ + width: '100%', + height: 1, + backgroundColor: cssVar('dividerColor'), +}); + +export const spacer = style({ + flexGrow: 1, +}); + +export const propertyRequired = style({ + color: cssVar('textDisableColor'), + fontSize: cssVar('fontXs'), +}); + +export const subListHeader = style({ + color: cssVar('textSecondaryColor'), + fontSize: cssVar('fontSm'), + marginBottom: 8, +}); + +export const propertyRowNamePopupRow = style({ + display: 'flex', + flexDirection: 'row', + alignItems: 'center', + gap: '8px', + fontSize: cssVar('fontSm'), + fontWeight: 500, + color: cssVar('textSecondaryColor'), + padding: '8px 16px', + minWidth: 260, +}); + +export const propertyNameIconEditable = style({ + fontSize: cssVar('fontH5'), + borderRadius: 4, + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + width: 32, + height: 32, + flexShrink: 0, + border: `1px solid ${cssVar('borderColor')}`, + background: cssVar('backgroundSecondaryColor'), +}); diff --git a/packages/frontend/i18n/src/resources/en.json b/packages/frontend/i18n/src/resources/en.json index d539909ef7..ded6415e99 100644 --- a/packages/frontend/i18n/src/resources/en.json +++ b/packages/frontend/i18n/src/resources/en.json @@ -308,6 +308,7 @@ "Unpublished hint": "Once published to the web, visitors can view the contents through the provided link.", "Untitled": "Untitled", "Untitled Collection": "Untitled Collection", + "unnamed": "unnamed", "Update Available": "Update available", "Update Collection": "Update Collection", "Update workspace name success": "Update workspace name success", @@ -901,8 +902,22 @@ "com.affine.settings.workspace.experimental-features.prompt-disclaimer": "I am aware of the risks, and I am willing to continue to use it.", "com.affine.settings.workspace.experimental-features.get-started": "Get Started", "com.affine.settings.workspace.experimental-features.header.plugins": "Experimental Features", + "com.affine.settings.workspace.properties.header.title": "Properties", + "com.affine.settings.workspace.properties.header.subtitle": "Manage workspace <1>{{name}} properties", + "com.affine.settings.workspace.properties.doc": "<0>{{count}} doc", + "com.affine.settings.workspace.properties.doc_others": "<0>{{count}} docs", + "com.affine.settings.workspace.properties.add_property": "Add property", + "com.affine.settings.workspace.properties.all": "All", + "com.affine.settings.workspace.properties.in-use": "In use", + "com.affine.settings.workspace.properties.unused": "Unused", + "com.affine.settings.workspace.properties.set-as-required": "Set as required property", + "com.affine.settings.workspace.properties.edit-property": "Edit property", + "com.affine.settings.workspace.properties.delete-property": "Delete property", + "com.affine.settings.workspace.properties.required-properties": "Required properties", + "com.affine.settings.workspace.properties.general-properties": "General properties", "com.affine.settings.workspace.preferences": "Preference", "com.affine.settings.workspace.experimental-features": "Plugins", + "com.affine.settings.workspace.properties": "Properties", "com.affine.share-menu.EnableCloudDescription": "Sharing doc requires AFFiNE Cloud.", "com.affine.share-menu.ShareMode": "Share mode", "com.affine.share-menu.SharePage": "Share Doc", @@ -1103,5 +1118,12 @@ "com.affine.page-properties.property.always-show": "Always show", "com.affine.page-properties.property.hide-when-empty": "Hide when empty", "com.affine.page-properties.property.required": "Required", - "com.affine.page-properties.property.remove-property": "Remove property" + "com.affine.page-properties.property.remove-property": "Remove property", + "com.affine.page-properties.property.text": "Text", + "com.affine.page-properties.property.number": "Number", + "com.affine.page-properties.property.date": "Date", + "com.affine.page-properties.property.checkbox": "Checkbox", + "com.affine.page-properties.property.progress": "Progress", + "com.affine.page-properties.property.tags": "Tags", + "com.affine.page-properties.icons": "Icons" }