diff --git a/packages/frontend/core/src/components/explorer/display-menu/group.tsx b/packages/frontend/core/src/components/explorer/display-menu/group.tsx index 2cf9f2b2e5..377e1e4173 100644 --- a/packages/frontend/core/src/components/explorer/display-menu/group.tsx +++ b/packages/frontend/core/src/components/explorer/display-menu/group.tsx @@ -5,6 +5,7 @@ import { useI18n } from '@affine/i18n'; import { DoneIcon } from '@blocksuite/icons/rc'; import { useLiveData, useService } from '@toeverything/infra'; import { cssVarV2 } from '@toeverything/theme/v2'; +import { useMemo } from 'react'; import { WorkspacePropertyName } from '../../properties'; import { @@ -15,6 +16,7 @@ import { isSupportedWorkspacePropertyType, WorkspacePropertyTypes, } from '../../workspace-property-types'; +import { generateExplorerPropertyList } from '../properties'; const PropertyGroupByName = ({ groupBy }: { groupBy: GroupByParams }) => { const workspacePropertyService = useService(WorkspacePropertyService); @@ -49,37 +51,89 @@ export const GroupByList = ({ onChange?: (next: GroupByParams) => void; }) => { const workspacePropertyService = useService(WorkspacePropertyService); - const propertyList = useLiveData(workspacePropertyService.properties$); + const propertyList = useLiveData(workspacePropertyService.sortedProperties$); + const explorerPropertyList = useMemo(() => { + return generateExplorerPropertyList(propertyList); + }, [propertyList]); return ( <> - {propertyList.map(v => { - const allowInGroupBy = isSupportedWorkspacePropertyType(v.type) - ? WorkspacePropertyTypes[v.type].allowInGroupBy - : false; - if (!allowInGroupBy) { - return null; - } - return ( - { - e.preventDefault(); - onChange?.({ - type: 'property', - key: v.id, - }); - }} - suffixIcon={ - groupBy?.type === 'property' && groupBy?.key === v.id ? ( - - ) : null - } - > - - - ); - })} + {explorerPropertyList.map(property => ( + + ))} ); }; + +const GroupByListItem = ({ + property, + groupBy, + onChange, +}: { + property: ReturnType[number]; + groupBy?: GroupByParams; + onChange?: (next: GroupByParams) => void; +}) => { + const t = useI18n(); + const { systemProperty, workspaceProperty } = property; + + const allowInGroupBy = systemProperty + ? 'allowInGroupBy' in systemProperty && systemProperty.allowInGroupBy + : workspaceProperty + ? isSupportedWorkspacePropertyType(workspaceProperty.type) && + WorkspacePropertyTypes[workspaceProperty.type].allowInGroupBy + : false; + + if (!allowInGroupBy) { + return null; + } + + const active = + (systemProperty && + groupBy?.type === 'system' && + groupBy?.key === systemProperty.type) || + (workspaceProperty && + groupBy?.type === 'property' && + groupBy?.key === workspaceProperty.id); + + const value = systemProperty + ? { + type: 'system', + key: systemProperty.type, + } + : workspaceProperty + ? { + type: 'property', + key: workspaceProperty.id, + } + : null; + + const name = workspaceProperty ? ( + + ) : systemProperty ? ( + t.t(systemProperty.name) + ) : null; + + return ( + { + e.preventDefault(); + if (value) { + onChange?.(value); + } + }} + suffixIcon={ + active ? ( + + ) : null + } + > + {name} + + ); +}; diff --git a/packages/frontend/core/src/components/explorer/display-menu/order.tsx b/packages/frontend/core/src/components/explorer/display-menu/order.tsx index bc4d4ddf66..292679a647 100644 --- a/packages/frontend/core/src/components/explorer/display-menu/order.tsx +++ b/packages/frontend/core/src/components/explorer/display-menu/order.tsx @@ -5,6 +5,7 @@ import { useI18n } from '@affine/i18n'; import { SortDownIcon, SortUpIcon } from '@blocksuite/icons/rc'; import { useLiveData, useService } from '@toeverything/infra'; import { cssVarV2 } from '@toeverything/theme/v2'; +import { useMemo } from 'react'; import { WorkspacePropertyName } from '../../properties'; import { @@ -15,6 +16,7 @@ import { isSupportedWorkspacePropertyType, WorkspacePropertyTypes, } from '../../workspace-property-types'; +import { generateExplorerPropertyList } from '../properties'; const PropertyOrderByName = ({ orderBy }: { orderBy: OrderByParams }) => { const workspacePropertyService = useService(WorkspacePropertyService); @@ -49,43 +51,95 @@ export const OrderByList = ({ onChange?: (next: OrderByParams) => void; }) => { const workspacePropertyService = useService(WorkspacePropertyService); - const propertyList = useLiveData(workspacePropertyService.properties$); + const propertyList = useLiveData(workspacePropertyService.sortedProperties$); + const explorerPropertyList = useMemo(() => { + return generateExplorerPropertyList(propertyList); + }, [propertyList]); return ( <> - {propertyList.map(v => { - const allowInOrderBy = isSupportedWorkspacePropertyType(v.type) - ? WorkspacePropertyTypes[v.type].allowInOrderBy - : false; - const active = orderBy?.type === 'property' && orderBy?.key === v.id; - if (!allowInOrderBy) { - return null; - } - return ( - { - e.preventDefault(); - onChange?.({ - type: 'property', - key: v.id, - desc: !active ? false : !orderBy.desc, - }); - }} - suffixIcon={ - active ? ( - !orderBy.desc ? ( - - ) : ( - - ) - ) : null - } - > - - - ); - })} + {explorerPropertyList.map(property => ( + + ))} ); }; + +const OrderByListItem = ({ + property, + orderBy, + onChange, +}: { + property: ReturnType[number]; + orderBy?: OrderByParams; + onChange?: (next: OrderByParams) => void; +}) => { + const t = useI18n(); + const { systemProperty, workspaceProperty } = property; + + const allowInOrderBy = systemProperty + ? 'allowInOrderBy' in systemProperty && systemProperty.allowInOrderBy + : workspaceProperty + ? isSupportedWorkspacePropertyType(workspaceProperty.type) && + WorkspacePropertyTypes[workspaceProperty.type].allowInOrderBy + : false; + + if (!allowInOrderBy) { + return null; + } + + const active = + (systemProperty && + orderBy?.type === 'system' && + orderBy?.key === systemProperty.type) || + (workspaceProperty && + orderBy?.type === 'property' && + orderBy?.key === workspaceProperty.id); + + const value = systemProperty + ? { + type: 'system', + key: systemProperty.type, + desc: !active ? false : !orderBy.desc, + } + : workspaceProperty + ? { + type: 'property', + key: workspaceProperty.id, + desc: !active ? false : !orderBy.desc, + } + : null; + + const name = workspaceProperty ? ( + + ) : systemProperty ? ( + t.t(systemProperty.name) + ) : null; + + return ( + { + e.preventDefault(); + if (value) { + onChange?.(value); + } + }} + suffixIcon={ + active ? ( + !orderBy.desc ? ( + + ) : ( + + ) + ) : null + } + > + {name} + + ); +}; diff --git a/packages/frontend/core/src/components/explorer/display-menu/properties.tsx b/packages/frontend/core/src/components/explorer/display-menu/properties.tsx index 74bd9802b1..26f6c0c53f 100644 --- a/packages/frontend/core/src/components/explorer/display-menu/properties.tsx +++ b/packages/frontend/core/src/components/explorer/display-menu/properties.tsx @@ -1,34 +1,14 @@ import { Button, Divider } from '@affine/component'; -import { - WorkspacePropertyService, - type WorkspacePropertyType, -} from '@affine/core/modules/workspace-property'; +import { WorkspacePropertyService } from '@affine/core/modules/workspace-property'; import { useI18n } from '@affine/i18n'; import { useLiveData, useService } from '@toeverything/infra'; import { useCallback, useMemo } from 'react'; import { WorkspacePropertyName } from '../../properties'; -import { WorkspacePropertyTypes } from '../../workspace-property-types'; +import { generateExplorerPropertyList } from '../properties'; import type { ExplorerDisplayPreference } from '../types'; import * as styles from './properties.css'; -export const filterDisplayProperties = < - T extends { type: WorkspacePropertyType }, ->( - propertyList: T[], - showInDocList: 'inline' | 'stack' -) => { - return propertyList - .filter( - property => - WorkspacePropertyTypes[property.type].showInDocList === showInDocList - ) - .map(property => ({ - property, - config: WorkspacePropertyTypes[property.type], - })); -}; - export const DisplayProperties = ({ displayPreference, onDisplayPreferenceChange, @@ -40,26 +20,15 @@ export const DisplayProperties = ({ }) => { const t = useI18n(); const workspacePropertyService = useService(WorkspacePropertyService); - const propertyList = useLiveData(workspacePropertyService.properties$); + const propertyList = useLiveData(workspacePropertyService.sortedProperties$); + const explorerPropertyList = useMemo(() => { + return generateExplorerPropertyList(propertyList); + }, [propertyList]); const displayProperties = displayPreference.displayProperties; const showIcon = displayPreference.showDocIcon ?? false; const showBody = displayPreference.showDocPreview ?? false; - const propertiesGroups = useMemo( - () => [ - { - type: 'inline', - properties: filterDisplayProperties(propertyList, 'inline'), - }, - { - type: 'stack', - properties: filterDisplayProperties(propertyList, 'stack'), - }, - ], - [propertyList] - ); - const handleDisplayPropertiesChange = useCallback( (displayProperties: string[]) => { onDisplayPreferenceChange({ ...displayPreference, displayProperties }); @@ -68,11 +37,11 @@ export const DisplayProperties = ({ ); const handlePropertyClick = useCallback( - (propertyId: string) => { + (key: string) => { handleDisplayPropertiesChange( - displayProperties && displayProperties.includes(propertyId) - ? displayProperties.filter(id => id !== propertyId) - : [...(displayProperties || []), propertyId] + displayProperties && displayProperties.includes(key) + ? displayProperties.filter(k => k !== key) + : [...(displayProperties || []), key] ); }, [displayProperties, handleDisplayPropertiesChange] @@ -97,29 +66,40 @@ export const DisplayProperties = ({
{t['com.affine.all-docs.display.properties']()}
- {propertiesGroups.map(list => { - return ( -
- {list.properties.map(({ property }) => { - return ( - - ); - })} -
- ); - })} +
+ {explorerPropertyList + .filter(p => p.systemProperty) + .map(property => { + return ( + + ); + })} +
+
+ {explorerPropertyList + .filter(p => !p.systemProperty) + .map(property => { + return ( + + ); + })} +
{t['com.affine.all-docs.display.list-view']()} @@ -143,3 +123,46 @@ export const DisplayProperties = ({ ); }; + +const PropertyRenderer = ({ + property, + displayProperties, + handlePropertyClick, +}: { + property: ReturnType[number]; + displayProperties: string[]; + handlePropertyClick: (key: string) => void; +}) => { + const t = useI18n(); + const { systemProperty, workspaceProperty } = property; + const key = systemProperty + ? `system:${systemProperty.type}` + : workspaceProperty + ? `property:${workspaceProperty?.id}` + : null; + const activeKey = systemProperty + ? `system:${systemProperty.type}` + : workspaceProperty + ? `property:${workspaceProperty?.id}` + : null; + const isActive = activeKey && displayProperties.includes(activeKey); + + if (!key) { + return null; + } + return ( + + ); +}; diff --git a/packages/frontend/core/src/components/explorer/docs-view/docs-list.tsx b/packages/frontend/core/src/components/explorer/docs-view/docs-list.tsx index 7ae7613aff..a09f6161d4 100644 --- a/packages/frontend/core/src/components/explorer/docs-view/docs-list.tsx +++ b/packages/frontend/core/src/components/explorer/docs-view/docs-list.tsx @@ -7,6 +7,7 @@ import { cssVarV2 } from '@toeverything/theme/v2'; import { memo, useCallback, useContext, useEffect, useMemo } from 'react'; import { ListFloatingToolbar } from '../../page-list/components/list-floating-toolbar'; +import { SystemPropertyTypes } from '../../system-property-types'; import { WorkspacePropertyTypes } from '../../workspace-property-types'; import { DocExplorerContext } from '../context'; import { DocListItem } from './doc-list-item'; @@ -30,7 +31,19 @@ const GroupHeader = memo(function GroupHeader({ const groupKey = groupBy?.key; const header = useMemo(() => { - if (groupType === 'property') { + if (groupType === 'system') { + const property = groupKey && SystemPropertyTypes[groupKey]; + if (!property) return null; + const GroupHeader = property.groupHeader; + if (!GroupHeader) return null; + return ( + + ); + } else if (groupType === 'property') { const property = allProperties.find(p => p.id === groupKey); if (!property) return null; diff --git a/packages/frontend/core/src/components/explorer/docs-view/properties.tsx b/packages/frontend/core/src/components/explorer/docs-view/properties.tsx index 6193b527a8..604a7560bb 100644 --- a/packages/frontend/core/src/components/explorer/docs-view/properties.tsx +++ b/packages/frontend/core/src/components/explorer/docs-view/properties.tsx @@ -1,94 +1,97 @@ import type { DocCustomPropertyInfo } from '@affine/core/modules/db'; import { type DocRecord, DocsService } from '@affine/core/modules/doc'; -import { - WorkspacePropertyService, - type WorkspacePropertyType, -} from '@affine/core/modules/workspace-property'; +import { WorkspacePropertyService } from '@affine/core/modules/workspace-property'; import { useLiveData, useService } from '@toeverything/infra'; import clsx from 'clsx'; import { useContext, useMemo } from 'react'; -import type { WorkspacePropertyTypes } from '../../workspace-property-types'; +import { SystemPropertyTypes } from '../../system-property-types'; +import { WorkspacePropertyTypes } from '../../workspace-property-types'; import { DocExplorerContext } from '../context'; -import { filterDisplayProperties } from '../display-menu/properties'; +import { generateExplorerPropertyList } from '../properties'; import { listHide560, listHide750 } from './doc-list-item.css'; import * as styles from './properties.css'; -const listInlinePropertyOrder: WorkspacePropertyType[] = [ +const listInlinePropertyOrder: string[] = [ 'createdAt', 'updatedAt', 'createdBy', 'updatedBy', ]; -const cardInlinePropertyOrder: WorkspacePropertyType[] = [ +const cardInlinePropertyOrder: string[] = [ 'createdBy', 'updatedBy', 'createdAt', 'updatedAt', ]; -const useProperties = (docId: string, view: 'list' | 'card') => { - const contextValue = useContext(DocExplorerContext); - const displayProperties = useLiveData(contextValue.displayProperties$); - const docsService = useService(DocsService); +const useProperties = (view: 'list' | 'card') => { const workspacePropertyService = useService(WorkspacePropertyService); - const doc = useLiveData(docsService.list.doc$(docId)); - const properties = useLiveData(doc?.properties$); - const propertyList = useLiveData(workspacePropertyService.properties$); + const propertyList = useLiveData(workspacePropertyService.sortedProperties$); + + const explorerPropertyList = useMemo(() => { + return generateExplorerPropertyList(propertyList); + }, [propertyList]); const stackProperties = useMemo( - () => (properties ? filterDisplayProperties(propertyList, 'stack') : []), - [properties, propertyList] + () => + explorerPropertyList.filter( + property => + (property.systemProperty && + property.systemProperty.showInDocList === 'stack') || + (property.workspaceProperty && + WorkspacePropertyTypes[property.workspaceProperty.type] + .showInDocList === 'stack') + ), + [explorerPropertyList] ); + const inlineProperties = useMemo( () => - properties - ? filterDisplayProperties(propertyList, 'inline') - .filter(p => p.property.type !== 'tags') - .sort((a, b) => { - const orderList = - view === 'list' - ? listInlinePropertyOrder - : cardInlinePropertyOrder; - const aIndex = orderList.indexOf(a.property.type); - const bIndex = orderList.indexOf(b.property.type); - // Push un-recognised types to the tail instead of the head - return ( - (aIndex === -1 ? Number.MAX_SAFE_INTEGER : aIndex) - - (bIndex === -1 ? Number.MAX_SAFE_INTEGER : bIndex) - ); - }) - : [], - [properties, propertyList, view] - ); - const tagsProperty = useMemo(() => { - return propertyList - ? filterDisplayProperties(propertyList, 'inline').find( - prop => prop.property.type === 'tags' + explorerPropertyList + .filter( + property => + (property.systemProperty && + property.systemProperty.showInDocList === 'inline') || + (property.workspaceProperty && + WorkspacePropertyTypes[property.workspaceProperty.type] + .showInDocList === 'inline') ) - : undefined; - }, [propertyList]); + .filter(p => p.systemProperty?.type !== 'tags') + .sort((a, b) => { + const orderList = + view === 'list' ? listInlinePropertyOrder : cardInlinePropertyOrder; + const aIndex = orderList.indexOf( + a.systemProperty?.type ?? a.workspaceProperty?.type ?? '' + ); + const bIndex = orderList.indexOf( + b.systemProperty?.type ?? b.workspaceProperty?.type ?? '' + ); + // Push un-recognised types to the tail instead of the head + return ( + (aIndex === -1 ? Number.MAX_SAFE_INTEGER : aIndex) - + (bIndex === -1 ? Number.MAX_SAFE_INTEGER : bIndex) + ); + }), + [explorerPropertyList, view] + ); return useMemo( () => ({ - doc, - displayProperties, stackProperties, inlineProperties, - tagsProperty, }), - [doc, displayProperties, stackProperties, inlineProperties, tagsProperty] + [stackProperties, inlineProperties] ); }; export const ListViewProperties = ({ docId }: { docId: string }) => { - const { - doc, - displayProperties, - stackProperties, - inlineProperties, - tagsProperty, - } = useProperties(docId, 'list'); + const contextValue = useContext(DocExplorerContext); + const displayProperties = useLiveData(contextValue?.displayProperties$); + const docsService = useService(DocsService); + const doc = useLiveData(docsService.list.doc$(docId)); + + const { stackProperties, inlineProperties } = useProperties('list'); if (!doc) { return null; @@ -99,99 +102,188 @@ export const ListViewProperties = ({ docId }: { docId: string }) => { {/* stack properties */}
- {stackProperties.map(({ property, config }) => { - if (!displayProperties?.includes(property.id)) { + {stackProperties.map(({ systemProperty, workspaceProperty }) => { + const displayKey = systemProperty + ? `system:${systemProperty.type}` + : workspaceProperty + ? `property:${workspaceProperty.id}` + : null; + if (!displayKey || !displayProperties?.includes(displayKey)) { return null; } - return ( - - ); + if (systemProperty) { + return ( + + ); + } else if (workspaceProperty) { + return ( + + ); + } + return null; })}
- {tagsProperty && - displayProperties?.includes(tagsProperty.property.id) ? ( + {displayProperties?.includes('system:tags') ? (
-
) : null}
{/* inline properties */} - {inlineProperties.map(({ property, config }) => { - if (!displayProperties?.includes(property.id)) { + {inlineProperties.map(({ systemProperty, workspaceProperty }) => { + const displayKeys = [ + systemProperty ? `system:${systemProperty.type}` : null, + workspaceProperty ? `property:${workspaceProperty.id}` : null, + ]; + if (!displayKeys.some(key => key && displayProperties?.includes(key))) { return null; } - return ( -
- -
- ); + if (systemProperty) { + return ( + + ); + } else if (workspaceProperty) { + return ( +
+ +
+ ); + } + return null; })} ); }; export const CardViewProperties = ({ docId }: { docId: string }) => { - const { - doc, - displayProperties, - stackProperties, - inlineProperties, - tagsProperty, - } = useProperties(docId, 'card'); + const contextValue = useContext(DocExplorerContext); + const displayProperties = useLiveData(contextValue?.displayProperties$); + const docsService = useService(DocsService); + const doc = useLiveData(docsService.list.doc$(docId)); + + const { stackProperties, inlineProperties } = useProperties('card'); if (!doc) { return null; } return ( -
- {inlineProperties.map(({ property, config }) => { - if (!displayProperties?.includes(property.id)) { - return null; - } - return ( -
- + <> + {/* stack properties */} +
+
+ {stackProperties.map(({ systemProperty, workspaceProperty }) => { + const displayKeys = [ + systemProperty ? `system:${systemProperty.type}` : null, + workspaceProperty ? `property:${workspaceProperty.id}` : null, + ]; + if ( + !displayKeys.some(key => key && displayProperties?.includes(key)) + ) { + return null; + } + if (systemProperty) { + return ( + + ); + } else if (workspaceProperty) { + return ( + + ); + } + return null; + })} +
+ {displayProperties?.includes('system:tags') ? ( +
+
- ); - })} - {tagsProperty && displayProperties?.includes(tagsProperty.property.id) ? ( - - ) : null} - {stackProperties.map(({ property, config }) => { - if (!displayProperties?.includes(property.id)) { + ) : null} +
+ {/* inline properties */} + {inlineProperties.map(({ systemProperty, workspaceProperty }) => { + const displayKeys = [ + systemProperty ? `system:${systemProperty.type}` : null, + workspaceProperty ? `property:${workspaceProperty.id}` : null, + ]; + if (!displayKeys.some(key => key && displayProperties?.includes(key))) { return null; } - return ( - - ); + if (systemProperty) { + return ( + + ); + } else if (workspaceProperty) { + return ( +
+ +
+ ); + } + return null; })} -
+ ); }; -const PropertyRenderer = ({ +const SystemPropertyRenderer = ({ + doc, + config, +}: { + doc: DocRecord; + config: (typeof SystemPropertyTypes)[string]; +}) => { + if (!config.docListProperty) { + return null; + } + + return ; +}; + +const WorkspacePropertyRenderer = ({ property, doc, config, diff --git a/packages/frontend/core/src/components/explorer/properties.ts b/packages/frontend/core/src/components/explorer/properties.ts new file mode 100644 index 0000000000..de3800d2ad --- /dev/null +++ b/packages/frontend/core/src/components/explorer/properties.ts @@ -0,0 +1,67 @@ +import type { DocCustomPropertyInfo } from '@affine/core/modules/db'; + +import { SystemPropertyTypes } from '../system-property-types'; + +const systemProperties = Object.entries(SystemPropertyTypes); + +/** + * In AFFiNE's property system, the property list can be fully customized by users. + * For example, system properties like `createdAt` and `updatedAt`. + * Users can completely remove them or create multiple instances. (This doesn't affect the underlying data, only the property display) + * + * To prevent user-defined properties from affecting the display of system properties, we've designed a dedicated property list for the Explorer. + * This list generates a final property list based on system properties and user-defined properties, arranged in a specific order. + * + * For example, we have a workspace property list: + * + * - `{name: 'Birth', type: 'createdAt'}` + * - `{name: 'Labels', type: 'tags'}` + * - `{name: 'Name', type: 'Text'}` + * + * Assuming we have 3 system properties: `createdAt`, `updatedAt`, and `tags` + * + * The final property list should be: + * + * - `{systemProperty: {type: 'createdAt'}, workspaceProperty: {name: 'Birth'}}` + * - `{systemProperty: {type: 'updatedAt'}, workspaceProperty: null}` + * - `{systemProperty: {type: 'tags'}, workspaceProperty: {name: 'Labels'}}` + * - `{systemProperty: null, workspaceProperty: {name: 'Name'}}` + * + * When displaying the list to users, we prioritize showing the workspace property if it exists, otherwise we show the system property. + * + * When users configure a property, we prioritize recording the system property's ID. This ensures that when users delete a property, it won't affect these settings. + */ +export function generateExplorerPropertyList( + workspaceProperties: DocCustomPropertyInfo[] +): { + systemProperty?: (typeof SystemPropertyTypes)[number] & { type: string }; + workspaceProperty?: DocCustomPropertyInfo; +}[] { + const finalList = []; + workspaceProperties = [...workspaceProperties]; + + for (const [type, info] of systemProperties) { + const workspacePropertyIndex = workspaceProperties.findIndex( + p => p.type === type + ); + if (workspacePropertyIndex === -1) { + finalList.push({ + systemProperty: { ...info, type }, + }); + } else { + finalList.push({ + systemProperty: { ...info, type }, + workspaceProperty: workspaceProperties[workspacePropertyIndex], + }); + workspaceProperties.splice(workspacePropertyIndex, 1); + } + } + + for (const workspaceProperty of workspaceProperties) { + finalList.push({ + workspaceProperty, + }); + } + + return finalList; +} diff --git a/packages/frontend/core/src/components/filter/add-filter.tsx b/packages/frontend/core/src/components/filter/add-filter.tsx index 0ca6a47a49..021388d07b 100644 --- a/packages/frontend/core/src/components/filter/add-filter.tsx +++ b/packages/frontend/core/src/components/filter/add-filter.tsx @@ -4,7 +4,9 @@ import { WorkspacePropertyService } from '@affine/core/modules/workspace-propert import { useI18n } from '@affine/i18n'; import { ArrowLeftBigIcon, FavoriteIcon, PlusIcon } from '@blocksuite/icons/rc'; import { useLiveData, useService } from '@toeverything/infra'; +import { useMemo } from 'react'; +import { generateExplorerPropertyList } from '../explorer/properties'; import { WorkspacePropertyIcon, WorkspacePropertyName } from '../properties'; import { WorkspacePropertyTypes } from '../workspace-property-types'; import * as styles from './styles.css'; @@ -18,7 +20,13 @@ export const AddFilterMenu = ({ }) => { const t = useI18n(); const workspacePropertyService = useService(WorkspacePropertyService); - const workspaceProperties = useLiveData(workspacePropertyService.properties$); + const workspaceProperties = useLiveData( + workspacePropertyService.sortedProperties$ + ); + const explorerPropertyList = useMemo( + () => generateExplorerPropertyList(workspaceProperties), + [workspaceProperties] + ); return ( <> @@ -64,34 +72,62 @@ export const AddFilterMenu = ({ {t['com.affine.filter.is-public']()} - {workspaceProperties.map(property => { - const type = WorkspacePropertyTypes[property.type]; - const defaultFilter = type?.defaultFilter; - if (!defaultFilter) { - return null; + {explorerPropertyList.map(({ systemProperty, workspaceProperty }) => { + if (systemProperty) { + const defaultFilter = + 'defaultFilter' in systemProperty && systemProperty.defaultFilter; + if (!defaultFilter) { + return null; + } + return ( + + } + key={systemProperty.type} + onClick={() => { + onAdd({ + type: 'system', + key: systemProperty.type, + ...defaultFilter, + }); + }} + > + + {t.t(systemProperty.name)} + + + ); + } else if (workspaceProperty) { + const type = WorkspacePropertyTypes[workspaceProperty.type]; + const defaultFilter = type?.defaultFilter; + if (!defaultFilter) { + return null; + } + return ( + + } + key={workspaceProperty.id} + onClick={() => { + onAdd({ + type: 'property', + key: workspaceProperty.id, + ...defaultFilter, + }); + }} + > + + + + + ); } - return ( - - } - key={property.id} - onClick={() => { - onAdd({ - type: 'property', - key: property.id, - ...defaultFilter, - }); - }} - > - - - - - ); + return null; })} ); diff --git a/packages/frontend/core/src/components/system-property-types/created-updated-at.tsx b/packages/frontend/core/src/components/system-property-types/created-updated-at.tsx new file mode 100644 index 0000000000..4bab0154f1 --- /dev/null +++ b/packages/frontend/core/src/components/system-property-types/created-updated-at.tsx @@ -0,0 +1,8 @@ +export { + CreateAtDocListProperty, + CreatedAtFilterValue, + CreatedAtGroupHeader, + UpdatedAtDocListProperty, + UpdatedAtFilterValue, + UpdatedAtGroupHeader, +} from '../workspace-property-types/created-updated-at'; diff --git a/packages/frontend/core/src/components/system-property-types/created-updated-by.tsx b/packages/frontend/core/src/components/system-property-types/created-updated-by.tsx new file mode 100644 index 0000000000..f9f7c923a7 --- /dev/null +++ b/packages/frontend/core/src/components/system-property-types/created-updated-by.tsx @@ -0,0 +1,6 @@ +export { + CreatedByDocListInlineProperty, + CreatedByUpdatedByFilterValue, + ModifiedByGroupHeader, + UpdatedByDocListInlineProperty, +} from '../workspace-property-types/created-updated-by'; diff --git a/packages/frontend/core/src/components/system-property-types/index.ts b/packages/frontend/core/src/components/system-property-types/index.ts index bd707ad787..0a0067b958 100644 --- a/packages/frontend/core/src/components/system-property-types/index.ts +++ b/packages/frontend/core/src/components/system-property-types/index.ts @@ -1,10 +1,34 @@ import type { FilterParams } from '@affine/core/modules/collection-rules'; +import type { DocRecord } from '@affine/core/modules/doc'; import type { I18nString } from '@affine/i18n'; -import { FavoriteIcon, ShareIcon, TagIcon } from '@blocksuite/icons/rc'; +import { + DateTimeIcon, + FavoriteIcon, + HistoryIcon, + MemberIcon, + ShareIcon, + TagIcon, +} from '@blocksuite/icons/rc'; +import type { GroupHeaderProps } from '../explorer/types'; +import { DateFilterMethod } from '../workspace-property-types'; +import { + CreateAtDocListProperty, + CreatedAtFilterValue, + CreatedAtGroupHeader, + UpdatedAtDocListProperty, + UpdatedAtFilterValue, + UpdatedAtGroupHeader, +} from './created-updated-at'; +import { + CreatedByDocListInlineProperty, + CreatedByUpdatedByFilterValue, + ModifiedByGroupHeader, + UpdatedByDocListInlineProperty, +} from './created-updated-by'; import { FavoriteFilterValue } from './favorite'; import { SharedFilterValue } from './shared'; -import { TagsFilterValue } from './tags'; +import { TagsDocListProperty, TagsFilterValue, TagsGroupHeader } from './tags'; export const SystemPropertyTypes = { tags: { @@ -16,6 +40,68 @@ export const SystemPropertyTypes = { 'is-empty': 'com.affine.filter.is empty', }, filterValue: TagsFilterValue, + allowInGroupBy: true, + allowInOrderBy: true, + defaultFilter: { method: 'is-not-empty' }, + showInDocList: 'stack', + docListProperty: TagsDocListProperty, + groupHeader: TagsGroupHeader, + }, + createdBy: { + icon: MemberIcon, + name: 'com.affine.page-properties.property.createdBy', + allowInGroupBy: true, + allowInOrderBy: true, + filterMethod: { + include: 'com.affine.filter.contains all', + }, + filterValue: CreatedByUpdatedByFilterValue, + defaultFilter: { method: 'include', value: '' }, + showInDocList: 'inline', + docListProperty: CreatedByDocListInlineProperty, + groupHeader: ModifiedByGroupHeader, + }, + updatedBy: { + icon: MemberIcon, + name: 'com.affine.page-properties.property.updatedBy', + allowInGroupBy: true, + allowInOrderBy: true, + filterMethod: { + include: 'com.affine.filter.contains all', + }, + filterValue: CreatedByUpdatedByFilterValue, + defaultFilter: { method: 'include', value: '' }, + showInDocList: 'inline', + docListProperty: UpdatedByDocListInlineProperty, + groupHeader: ModifiedByGroupHeader, + }, + updatedAt: { + icon: DateTimeIcon, + name: 'com.affine.page-properties.property.updatedAt', + allowInGroupBy: true, + allowInOrderBy: true, + filterMethod: { + ...DateFilterMethod, + }, + filterValue: UpdatedAtFilterValue, + defaultFilter: { method: 'this-week' }, + showInDocList: 'inline', + docListProperty: UpdatedAtDocListProperty, + groupHeader: UpdatedAtGroupHeader, + }, + createdAt: { + icon: HistoryIcon, + name: 'com.affine.page-properties.property.createdAt', + allowInGroupBy: true, + allowInOrderBy: true, + filterMethod: { + ...DateFilterMethod, + }, + filterValue: CreatedAtFilterValue, + defaultFilter: { method: 'this-week' }, + showInDocList: 'inline', + docListProperty: CreateAtDocListProperty, + groupHeader: CreatedAtGroupHeader, }, favorite: { icon: FavoriteIcon, @@ -33,7 +119,7 @@ export const SystemPropertyTypes = { }, filterValue: SharedFilterValue, }, -} satisfies { +} as { [type: string]: { icon: React.FC>; name: I18nString; @@ -45,13 +131,20 @@ export const SystemPropertyTypes = { filter: FilterParams; onChange: (filter: FilterParams) => void; }>; + defaultFilter?: Omit; + /** + * Whether to show the property in the doc list, + * - `inline`: show the property in the doc list inline + * - `stack`: show as tags + */ + showInDocList?: 'inline' | 'stack'; + docListProperty?: React.FC<{ doc: DocRecord }>; + groupHeader?: React.FC; }; }; export type SystemPropertyType = keyof typeof SystemPropertyTypes; -export const isSupportedSystemPropertyType = ( - type?: string -): type is SystemPropertyType => { +export const isSupportedSystemPropertyType = (type?: string) => { return type ? type in SystemPropertyTypes : false; }; diff --git a/packages/frontend/core/src/components/system-property-types/tags.tsx b/packages/frontend/core/src/components/system-property-types/tags.tsx index 498836edff..b7d4f0d192 100644 --- a/packages/frontend/core/src/components/system-property-types/tags.tsx +++ b/packages/frontend/core/src/components/system-property-types/tags.tsx @@ -1,62 +1,5 @@ -import type { FilterParams } from '@affine/core/modules/collection-rules'; -import { TagService } from '@affine/core/modules/tag'; -import { useI18n } from '@affine/i18n'; -import { useLiveData, useService } from '@toeverything/infra'; -import { cssVarV2 } from '@toeverything/theme/v2'; -import { useCallback, useMemo } from 'react'; - -import { WorkspaceTagsInlineEditor } from '../tags'; - -export const TagsFilterValue = ({ - filter, - onChange, -}: { - filter: FilterParams; - onChange: (filter: FilterParams) => void; -}) => { - const t = useI18n(); - const tagService = useService(TagService); - const allTagMetas = useLiveData(tagService.tagList.tagMetas$); - - const selectedTags = useMemo( - () => - filter.value - ?.split(',') - .filter(id => allTagMetas.some(tag => tag.id === id)) ?? [], - [filter, allTagMetas] - ); - - const handleSelectTag = useCallback( - (tagId: string) => { - onChange({ - ...filter, - value: [...selectedTags, tagId].join(','), - }); - }, - [filter, onChange, selectedTags] - ); - - const handleDeselectTag = useCallback( - (tagId: string) => { - onChange({ - ...filter, - value: selectedTags.filter(id => id !== tagId).join(','), - }); - }, - [filter, onChange, selectedTags] - ); - - return filter.method !== 'is-not-empty' && filter.method !== 'is-empty' ? ( - - {t['com.affine.filter.empty']()} - - } - selectedTags={selectedTags} - onSelectTag={handleSelectTag} - onDeselectTag={handleDeselectTag} - tagMode="inline-tag" - /> - ) : undefined; -}; +export { + TagsDocListProperty, + TagsFilterValue, + TagsGroupHeader, +} from '../workspace-property-types/tags'; diff --git a/packages/frontend/core/src/components/workspace-property-types/created-updated-at.css.ts b/packages/frontend/core/src/components/workspace-property-types/created-updated-at.css.ts new file mode 100644 index 0000000000..86ba2faaca --- /dev/null +++ b/packages/frontend/core/src/components/workspace-property-types/created-updated-at.css.ts @@ -0,0 +1,27 @@ +import { cssVarV2 } from '@toeverything/theme/v2'; +import { style } from '@vanilla-extract/css'; + +export const empty = style({ + color: cssVarV2.text.placeholder, +}); + +export const tooltip = style({ + display: 'inline-block', + selectors: { + '&::first-letter': { + textTransform: 'uppercase', + }, + }, +}); + +export const dateDocListInlineProperty = style({ + width: 60, + textAlign: 'center', + fontSize: 12, + lineHeight: '20px', + color: cssVarV2.text.secondary, + whiteSpace: 'nowrap', + overflow: 'hidden', + textOverflow: 'ellipsis', + flexShrink: 0, +}); diff --git a/packages/frontend/core/src/components/workspace-property-types/created-updated-at.tsx b/packages/frontend/core/src/components/workspace-property-types/created-updated-at.tsx new file mode 100644 index 0000000000..e1000b4cb4 --- /dev/null +++ b/packages/frontend/core/src/components/workspace-property-types/created-updated-at.tsx @@ -0,0 +1,110 @@ +import { PropertyValue, Tooltip } from '@affine/component'; +import { type DocRecord, DocService } from '@affine/core/modules/doc'; +import { i18nTime, useI18n } from '@affine/i18n'; +import { useLiveData, useServices } from '@toeverything/infra'; + +import { PlainTextDocGroupHeader } from '../explorer/docs-view/group-header'; +import type { GroupHeaderProps } from '../explorer/types'; +import * as styles from './created-updated-at.css'; + +const toRelativeDate = (time: string | number) => { + return i18nTime(time, { + relative: { + max: [1, 'day'], + }, + absolute: { + accuracy: 'day', + }, + }); +}; + +const MetaDateValueFactory = ({ + type, +}: { + type: 'createDate' | 'updatedDate'; +}) => + function ReadonlyDateValue() { + const { docService } = useServices({ + DocService, + }); + + const docMeta = useLiveData(docService.doc.meta$); + const value = docMeta?.[type]; + + const relativeDate = value ? toRelativeDate(value) : null; + const date = value ? i18nTime(value) : null; + + return ( + + + {relativeDate} + + + ); + }; + +export const CreateAtValue = MetaDateValueFactory({ + type: 'createDate', +}); + +export const UpdatedAtValue = MetaDateValueFactory({ + type: 'updatedDate', +}); + +export const CreatedAtGroupHeader = (props: GroupHeaderProps) => { + return ; +}; + +export const UpdatedAtGroupHeader = (props: GroupHeaderProps) => { + return ; +}; + +export const CreateAtDocListProperty = ({ doc }: { doc: DocRecord }) => { + const t = useI18n(); + const docMeta = useLiveData(doc.meta$); + const createDate = docMeta?.createDate; + + if (!createDate) return null; + + return ( + + {t.t('created at', { time: i18nTime(createDate) })} + + } + > +
+ {i18nTime(createDate, { relative: true })} +
+
+ ); +}; + +export const UpdatedAtDocListProperty = ({ doc }: { doc: DocRecord }) => { + const t = useI18n(); + const docMeta = useLiveData(doc.meta$); + const updatedDate = docMeta?.updatedDate; + + if (!updatedDate) return null; + + return ( + + {t.t('updated at', { time: i18nTime(updatedDate) })} + + } + > +
+ {i18nTime(updatedDate, { relative: true })} +
+
+ ); +}; + +export { DateFilterValue as CreatedAtFilterValue } from './date'; +export { DateFilterValue as UpdatedAtFilterValue } from './date'; diff --git a/packages/frontend/core/src/components/workspace-property-types/created-updated-by.tsx b/packages/frontend/core/src/components/workspace-property-types/created-updated-by.tsx index 8794796d6b..62a30f1b57 100644 --- a/packages/frontend/core/src/components/workspace-property-types/created-updated-by.tsx +++ b/packages/frontend/core/src/components/workspace-property-types/created-updated-by.tsx @@ -9,7 +9,7 @@ import { cssVarV2 } from '@toeverything/theme/v2'; import { type ReactNode, useCallback, useMemo } from 'react'; import { PlainTextDocGroupHeader } from '../explorer/docs-view/group-header'; -import type { DocListPropertyProps, GroupHeaderProps } from '../explorer/types'; +import type { GroupHeaderProps } from '../explorer/types'; import { MemberSelectorInline } from '../member-selector'; import * as styles from './created-updated-by.css'; @@ -135,9 +135,7 @@ export const CreatedByUpdatedByFilterValue = ({ ); }; -export const CreatedByDocListInlineProperty = ({ - doc, -}: DocListPropertyProps) => { +export const CreatedByDocListInlineProperty = ({ doc }: { doc: DocRecord }) => { return ( { +export const UpdatedByDocListInlineProperty = ({ doc }: { doc: DocRecord }) => { return ( { - return i18nTime(time, { - relative: { - max: [1, 'day'], - }, - absolute: { - accuracy: 'day', - }, - }); -}; - -const MetaDateValueFactory = ({ - type, -}: { - type: 'createDate' | 'updatedDate'; -}) => - function ReadonlyDateValue() { - const { docService } = useServices({ - DocService, - }); - - const docMeta = useLiveData(docService.doc.meta$); - const value = docMeta?.[type]; - - const relativeDate = value ? toRelativeDate(value) : null; - const date = value ? i18nTime(value) : null; - - return ( - - - {relativeDate} - - - ); - }; - -export const CreateDateValue = MetaDateValueFactory({ - type: 'createDate', -}); - -export const UpdatedDateValue = MetaDateValueFactory({ - type: 'updatedDate', -}); - export const DateFilterValue = ({ filter, onChange, @@ -199,50 +150,6 @@ export const DateDocListProperty = ({ value }: DocListPropertyProps) => { ); }; -export const CreateDateDocListProperty = ({ doc }: DocListPropertyProps) => { - const t = useI18n(); - const docMeta = useLiveData(doc.meta$); - const createDate = docMeta?.createDate; - - if (!createDate) return null; - - return ( - - {t.t('created at', { time: i18nTime(createDate) })} - - } - > -
- {i18nTime(createDate, { relative: true })} -
-
- ); -}; - -export const UpdatedDateDocListProperty = ({ doc }: DocListPropertyProps) => { - const t = useI18n(); - const docMeta = useLiveData(doc.meta$); - const updatedDate = docMeta?.updatedDate; - - if (!updatedDate) return null; - - return ( - - {t.t('updated at', { time: i18nTime(updatedDate) })} - - } - > -
- {i18nTime(updatedDate, { relative: true })} -
-
- ); -}; - export const DateGroupHeader = ({ groupId, docCount }: GroupHeaderProps) => { const date = groupId || 'No Date'; @@ -252,7 +159,3 @@ export const DateGroupHeader = ({ groupId, docCount }: GroupHeaderProps) => {
); }; - -export const CreatedGroupHeader = (props: GroupHeaderProps) => { - return ; -}; diff --git a/packages/frontend/core/src/components/workspace-property-types/index.ts b/packages/frontend/core/src/components/workspace-property-types/index.ts index 4958109ba9..9ac85630d3 100644 --- a/packages/frontend/core/src/components/workspace-property-types/index.ts +++ b/packages/frontend/core/src/components/workspace-property-types/index.ts @@ -28,6 +28,16 @@ import { CheckboxGroupHeader, CheckboxValue, } from './checkbox'; +import { + CreateAtDocListProperty, + CreateAtValue, + CreatedAtFilterValue, + CreatedAtGroupHeader, + UpdatedAtDocListProperty, + UpdatedAtFilterValue, + UpdatedAtGroupHeader, + UpdatedAtValue, +} from './created-updated-at'; import { CreatedByDocListInlineProperty, CreatedByUpdatedByFilterValue, @@ -37,15 +47,10 @@ import { UpdatedByValue, } from './created-updated-by'; import { - CreateDateDocListProperty, - CreateDateValue, - CreatedGroupHeader, DateDocListProperty, DateFilterValue, DateGroupHeader, DateValue, - UpdatedDateDocListProperty, - UpdatedDateValue, } from './date'; import { DocPrimaryModeDocListProperty, @@ -79,7 +84,7 @@ import { TextValue, } from './text'; -const DateFilterMethod = { +export const DateFilterMethod = { after: 'com.affine.filter.after', before: 'com.affine.filter.before', between: 'com.affine.filter.between', @@ -213,7 +218,7 @@ export const WorkspacePropertyTypes = { }, updatedAt: { icon: DateTimeIcon, - value: UpdatedDateValue, + value: UpdatedAtValue, name: 'com.affine.page-properties.property.updatedAt', description: 'com.affine.page-properties.property.updatedAt.tooltips', renameable: false, @@ -222,15 +227,15 @@ export const WorkspacePropertyTypes = { filterMethod: { ...DateFilterMethod, }, - filterValue: DateFilterValue, + filterValue: UpdatedAtFilterValue, defaultFilter: { method: 'this-week' }, showInDocList: 'inline', - docListProperty: UpdatedDateDocListProperty, - groupHeader: CreatedGroupHeader, + docListProperty: UpdatedAtDocListProperty, + groupHeader: UpdatedAtGroupHeader, }, createdAt: { icon: HistoryIcon, - value: CreateDateValue, + value: CreateAtValue, name: 'com.affine.page-properties.property.createdAt', description: 'com.affine.page-properties.property.createdAt.tooltips', renameable: false, @@ -239,11 +244,11 @@ export const WorkspacePropertyTypes = { filterMethod: { ...DateFilterMethod, }, - filterValue: DateFilterValue, + filterValue: CreatedAtFilterValue, defaultFilter: { method: 'this-week' }, showInDocList: 'inline', - docListProperty: CreateDateDocListProperty, - groupHeader: CreatedGroupHeader, + docListProperty: CreateAtDocListProperty, + groupHeader: CreatedAtGroupHeader, }, docPrimaryMode: { icon: FileIcon, diff --git a/packages/frontend/core/src/components/workspace-property-types/tags.tsx b/packages/frontend/core/src/components/workspace-property-types/tags.tsx index 4db7d6e101..d8fd43349b 100644 --- a/packages/frontend/core/src/components/workspace-property-types/tags.tsx +++ b/packages/frontend/core/src/components/workspace-property-types/tags.tsx @@ -1,6 +1,6 @@ import { PropertyValue } from '@affine/component'; import type { FilterParams } from '@affine/core/modules/collection-rules'; -import { DocService } from '@affine/core/modules/doc'; +import { type DocRecord, DocService } from '@affine/core/modules/doc'; import { type Tag, TagService } from '@affine/core/modules/tag'; import { WorkspaceService } from '@affine/core/modules/workspace'; import { useI18n } from '@affine/i18n'; @@ -11,7 +11,7 @@ import { useCallback, useMemo } from 'react'; import { PlainTextDocGroupHeader } from '../explorer/docs-view/group-header'; import { StackProperty } from '../explorer/docs-view/stack-property'; -import type { DocListPropertyProps, GroupHeaderProps } from '../explorer/types'; +import type { GroupHeaderProps } from '../explorer/types'; import { useNavigateHelper } from '../hooks/use-navigate-helper'; import type { PropertyValueProps } from '../properties/types'; import { @@ -185,7 +185,7 @@ const TagIcon = ({ tag, size = 8 }: { tag: Tag; size?: number }) => { /> ); }; -export const TagsDocListProperty = ({ doc }: DocListPropertyProps) => { +export const TagsDocListProperty = ({ doc }: { doc: DocRecord }) => { const tagList = useService(TagService).tagList; const tags = useLiveData(tagList.tagsByPageId$(doc.id));