feat(core): add system property types support (#12332)

This commit is contained in:
EYHN
2025-05-19 09:15:37 +00:00
parent fbf590ddd4
commit 91e7b28dd5
18 changed files with 889 additions and 481 deletions

View File

@@ -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 (
<MenuItem
key={v.id}
onClick={e => {
e.preventDefault();
onChange?.({
type: 'property',
key: v.id,
});
}}
suffixIcon={
groupBy?.type === 'property' && groupBy?.key === v.id ? (
<DoneIcon style={{ color: cssVarV2('icon/activated') }} />
) : null
}
>
<WorkspacePropertyName propertyInfo={v} />
</MenuItem>
);
})}
{explorerPropertyList.map(property => (
<GroupByListItem
key={property.systemProperty?.type ?? property.workspaceProperty?.id}
property={property}
groupBy={groupBy}
onChange={onChange}
/>
))}
</>
);
};
const GroupByListItem = ({
property,
groupBy,
onChange,
}: {
property: ReturnType<typeof generateExplorerPropertyList>[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 ? (
<WorkspacePropertyName propertyInfo={workspaceProperty} />
) : systemProperty ? (
t.t(systemProperty.name)
) : null;
return (
<MenuItem
onClick={e => {
e.preventDefault();
if (value) {
onChange?.(value);
}
}}
suffixIcon={
active ? (
<DoneIcon style={{ color: cssVarV2('icon/activated') }} />
) : null
}
>
{name}
</MenuItem>
);
};

View File

@@ -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 (
<MenuItem
key={v.id}
onClick={e => {
e.preventDefault();
onChange?.({
type: 'property',
key: v.id,
desc: !active ? false : !orderBy.desc,
});
}}
suffixIcon={
active ? (
!orderBy.desc ? (
<SortUpIcon style={{ color: cssVarV2('icon/activated') }} />
) : (
<SortDownIcon style={{ color: cssVarV2('icon/activated') }} />
)
) : null
}
>
<WorkspacePropertyName propertyInfo={v} />
</MenuItem>
);
})}
{explorerPropertyList.map(property => (
<OrderByListItem
key={property.systemProperty?.type ?? property.workspaceProperty?.id}
property={property}
orderBy={orderBy}
onChange={onChange}
/>
))}
</>
);
};
const OrderByListItem = ({
property,
orderBy,
onChange,
}: {
property: ReturnType<typeof generateExplorerPropertyList>[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 ? (
<WorkspacePropertyName propertyInfo={workspaceProperty} />
) : systemProperty ? (
t.t(systemProperty.name)
) : null;
return (
<MenuItem
onClick={e => {
e.preventDefault();
if (value) {
onChange?.(value);
}
}}
suffixIcon={
active ? (
!orderBy.desc ? (
<SortUpIcon style={{ color: cssVarV2('icon/activated') }} />
) : (
<SortDownIcon style={{ color: cssVarV2('icon/activated') }} />
)
) : null
}
>
{name}
</MenuItem>
);
};

View File

@@ -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 = ({
<section className={styles.sectionLabel}>
{t['com.affine.all-docs.display.properties']()}
</section>
{propertiesGroups.map(list => {
return (
<div className={styles.properties} key={list.type}>
{list.properties.map(({ property }) => {
return (
<Button
key={property.id}
data-show={
displayProperties
? displayProperties.includes(property.id)
: false
}
onClick={() => handlePropertyClick(property.id)}
className={styles.property}
data-property-id={property.id}
>
<WorkspacePropertyName propertyInfo={property} />
</Button>
);
})}
</div>
);
})}
<div className={styles.properties}>
{explorerPropertyList
.filter(p => p.systemProperty)
.map(property => {
return (
<PropertyRenderer
key={
property.systemProperty?.type ??
property.workspaceProperty?.id
}
property={property}
displayProperties={displayProperties ?? []}
handlePropertyClick={handlePropertyClick}
/>
);
})}
</div>
<div className={styles.properties}>
{explorerPropertyList
.filter(p => !p.systemProperty)
.map(property => {
return (
<PropertyRenderer
key={
property.systemProperty?.type ??
property.workspaceProperty?.id
}
property={property}
displayProperties={displayProperties ?? []}
handlePropertyClick={handlePropertyClick}
/>
);
})}
</div>
<Divider size="thinner" />
<section className={styles.sectionLabel}>
{t['com.affine.all-docs.display.list-view']()}
@@ -143,3 +123,46 @@ export const DisplayProperties = ({
</div>
);
};
const PropertyRenderer = ({
property,
displayProperties,
handlePropertyClick,
}: {
property: ReturnType<typeof generateExplorerPropertyList>[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 (
<Button
key={key}
data-show={isActive}
onClick={() => handlePropertyClick(key)}
className={styles.property}
data-key={key}
>
{workspaceProperty ? (
<WorkspacePropertyName propertyInfo={workspaceProperty} />
) : systemProperty ? (
t.t(systemProperty.name)
) : null}
</Button>
);
};

View File

@@ -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 (
<GroupHeader
groupId={groupId}
docCount={itemCount}
collapsed={!!collapsed}
/>
);
} else if (groupType === 'property') {
const property = allProperties.find(p => p.id === groupKey);
if (!property) return null;

View File

@@ -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 */}
<div className={clsx(styles.stackContainer, listHide750)}>
<div className={styles.stackProperties}>
{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 (
<PropertyRenderer
key={property.id}
doc={doc}
property={property}
config={config}
/>
);
if (systemProperty) {
return (
<SystemPropertyRenderer
doc={doc}
config={SystemPropertyTypes[systemProperty.type]}
key={systemProperty.type}
/>
);
} else if (workspaceProperty) {
return (
<WorkspacePropertyRenderer
key={workspaceProperty.id}
doc={doc}
property={workspaceProperty}
config={WorkspacePropertyTypes[workspaceProperty.type]}
/>
);
}
return null;
})}
</div>
{tagsProperty &&
displayProperties?.includes(tagsProperty.property.id) ? (
{displayProperties?.includes('system:tags') ? (
<div className={styles.stackProperties}>
<PropertyRenderer
<SystemPropertyRenderer
doc={doc}
property={tagsProperty.property}
config={tagsProperty.config}
config={SystemPropertyTypes.tags}
/>
</div>
) : null}
</div>
{/* 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 (
<div
key={property.id}
className={clsx(styles.inlineProperty, listHide560)}
>
<PropertyRenderer doc={doc} property={property} config={config} />
</div>
);
if (systemProperty) {
return (
<SystemPropertyRenderer
doc={doc}
config={SystemPropertyTypes[systemProperty.type]}
key={systemProperty.type}
/>
);
} else if (workspaceProperty) {
return (
<div
key={workspaceProperty.id}
className={clsx(styles.inlineProperty, listHide560)}
>
<WorkspacePropertyRenderer
doc={doc}
property={workspaceProperty}
config={WorkspacePropertyTypes[workspaceProperty.type]}
/>
</div>
);
}
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 (
<div className={styles.cardProperties}>
{inlineProperties.map(({ property, config }) => {
if (!displayProperties?.includes(property.id)) {
return null;
}
return (
<div key={property.id} className={styles.inlineProperty}>
<PropertyRenderer doc={doc} property={property} config={config} />
<>
{/* stack properties */}
<div className={styles.stackContainer}>
<div className={styles.stackProperties}>
{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 (
<SystemPropertyRenderer
doc={doc}
config={SystemPropertyTypes[systemProperty.type]}
key={systemProperty.type}
/>
);
} else if (workspaceProperty) {
return (
<WorkspacePropertyRenderer
key={workspaceProperty.id}
doc={doc}
property={workspaceProperty}
config={WorkspacePropertyTypes[workspaceProperty.type]}
/>
);
}
return null;
})}
</div>
{displayProperties?.includes('system:tags') ? (
<div className={styles.stackProperties}>
<SystemPropertyRenderer
doc={doc}
config={SystemPropertyTypes.tags}
/>
</div>
);
})}
{tagsProperty && displayProperties?.includes(tagsProperty.property.id) ? (
<PropertyRenderer
doc={doc}
property={tagsProperty.property}
config={tagsProperty.config}
/>
) : null}
{stackProperties.map(({ property, config }) => {
if (!displayProperties?.includes(property.id)) {
) : null}
</div>
{/* 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 (
<PropertyRenderer
key={property.id}
doc={doc}
property={property}
config={config}
/>
);
if (systemProperty) {
return (
<SystemPropertyRenderer
doc={doc}
config={SystemPropertyTypes[systemProperty.type]}
key={systemProperty.type}
/>
);
} else if (workspaceProperty) {
return (
<div key={workspaceProperty.id} className={styles.inlineProperty}>
<WorkspacePropertyRenderer
doc={doc}
property={workspaceProperty}
config={WorkspacePropertyTypes[workspaceProperty.type]}
/>
</div>
);
}
return null;
})}
</div>
</>
);
};
const PropertyRenderer = ({
const SystemPropertyRenderer = ({
doc,
config,
}: {
doc: DocRecord;
config: (typeof SystemPropertyTypes)[string];
}) => {
if (!config.docListProperty) {
return null;
}
return <config.docListProperty doc={doc} />;
};
const WorkspacePropertyRenderer = ({
property,
doc,
config,

View File

@@ -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;
}

View File

@@ -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']()}
</span>
</MenuItem>
{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 (
<MenuItem
prefixIcon={
<systemProperty.icon className={styles.filterTypeItemIcon} />
}
key={systemProperty.type}
onClick={() => {
onAdd({
type: 'system',
key: systemProperty.type,
...defaultFilter,
});
}}
>
<span className={styles.filterTypeItemName}>
{t.t(systemProperty.name)}
</span>
</MenuItem>
);
} else if (workspaceProperty) {
const type = WorkspacePropertyTypes[workspaceProperty.type];
const defaultFilter = type?.defaultFilter;
if (!defaultFilter) {
return null;
}
return (
<MenuItem
prefixIcon={
<WorkspacePropertyIcon
propertyInfo={workspaceProperty}
className={styles.filterTypeItemIcon}
/>
}
key={workspaceProperty.id}
onClick={() => {
onAdd({
type: 'property',
key: workspaceProperty.id,
...defaultFilter,
});
}}
>
<span className={styles.filterTypeItemName}>
<WorkspacePropertyName propertyInfo={workspaceProperty} />
</span>
</MenuItem>
);
}
return (
<MenuItem
prefixIcon={
<WorkspacePropertyIcon
propertyInfo={property}
className={styles.filterTypeItemIcon}
/>
}
key={property.id}
onClick={() => {
onAdd({
type: 'property',
key: property.id,
...defaultFilter,
});
}}
>
<span className={styles.filterTypeItemName}>
<WorkspacePropertyName propertyInfo={property} />
</span>
</MenuItem>
);
return null;
})}
</>
);

View File

@@ -0,0 +1,8 @@
export {
CreateAtDocListProperty,
CreatedAtFilterValue,
CreatedAtGroupHeader,
UpdatedAtDocListProperty,
UpdatedAtFilterValue,
UpdatedAtGroupHeader,
} from '../workspace-property-types/created-updated-at';

View File

@@ -0,0 +1,6 @@
export {
CreatedByDocListInlineProperty,
CreatedByUpdatedByFilterValue,
ModifiedByGroupHeader,
UpdatedByDocListInlineProperty,
} from '../workspace-property-types/created-updated-by';

View File

@@ -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<React.SVGProps<SVGSVGElement>>;
name: I18nString;
@@ -45,13 +131,20 @@ export const SystemPropertyTypes = {
filter: FilterParams;
onChange: (filter: FilterParams) => void;
}>;
defaultFilter?: Omit<FilterParams, 'type' | 'key'>;
/**
* 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<GroupHeaderProps>;
};
};
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;
};

View File

@@ -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' ? (
<WorkspaceTagsInlineEditor
placeholder={
<span style={{ color: cssVarV2('text/placeholder') }}>
{t['com.affine.filter.empty']()}
</span>
}
selectedTags={selectedTags}
onSelectTag={handleSelectTag}
onDeselectTag={handleDeselectTag}
tagMode="inline-tag"
/>
) : undefined;
};
export {
TagsDocListProperty,
TagsFilterValue,
TagsGroupHeader,
} from '../workspace-property-types/tags';

View File

@@ -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,
});

View File

@@ -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 (
<Tooltip content={date} side="top" align="end">
<PropertyValue
className={relativeDate ? '' : styles.empty}
isEmpty={!relativeDate}
>
{relativeDate}
</PropertyValue>
</Tooltip>
);
};
export const CreateAtValue = MetaDateValueFactory({
type: 'createDate',
});
export const UpdatedAtValue = MetaDateValueFactory({
type: 'updatedDate',
});
export const CreatedAtGroupHeader = (props: GroupHeaderProps) => {
return <PlainTextDocGroupHeader {...props} />;
};
export const UpdatedAtGroupHeader = (props: GroupHeaderProps) => {
return <PlainTextDocGroupHeader {...props} />;
};
export const CreateAtDocListProperty = ({ doc }: { doc: DocRecord }) => {
const t = useI18n();
const docMeta = useLiveData(doc.meta$);
const createDate = docMeta?.createDate;
if (!createDate) return null;
return (
<Tooltip
content={
<span className={styles.tooltip}>
{t.t('created at', { time: i18nTime(createDate) })}
</span>
}
>
<div className={styles.dateDocListInlineProperty}>
{i18nTime(createDate, { relative: true })}
</div>
</Tooltip>
);
};
export const UpdatedAtDocListProperty = ({ doc }: { doc: DocRecord }) => {
const t = useI18n();
const docMeta = useLiveData(doc.meta$);
const updatedDate = docMeta?.updatedDate;
if (!updatedDate) return null;
return (
<Tooltip
content={
<span className={styles.tooltip}>
{t.t('updated at', { time: i18nTime(updatedDate) })}
</span>
}
>
<div className={styles.dateDocListInlineProperty}>
{i18nTime(updatedDate, { relative: true })}
</div>
</Tooltip>
);
};
export { DateFilterValue as CreatedAtFilterValue } from './date';
export { DateFilterValue as UpdatedAtFilterValue } from './date';

View File

@@ -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 (
<CreatedByUpdatedByAvatar
doc={doc}
@@ -149,9 +147,7 @@ export const CreatedByDocListInlineProperty = ({
);
};
export const UpdatedByDocListInlineProperty = ({
doc,
}: DocListPropertyProps) => {
export const UpdatedByDocListInlineProperty = ({ doc }: { doc: DocRecord }) => {
return (
<CreatedByUpdatedByAvatar
type="UpdatedBy"

View File

@@ -1,28 +1,6 @@
import { cssVar } from '@toeverything/theme';
import { cssVarV2 } from '@toeverything/theme/v2';
import { style } from '@vanilla-extract/css';
export const empty = style({
color: cssVar('placeholderColor'),
});
export const dateDocListInlineProperty = style({
width: 60,
textAlign: 'center',
fontSize: 12,
lineHeight: '20px',
color: cssVarV2.text.secondary,
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
flexShrink: 0,
});
export const tooltip = style({
display: 'inline-block',
selectors: {
'&::first-letter': {
textTransform: 'uppercase',
},
},
});

View File

@@ -1,9 +1,7 @@
import { DatePicker, Menu, PropertyValue, Tooltip } from '@affine/component';
import { DatePicker, Menu, PropertyValue } from '@affine/component';
import type { FilterParams } from '@affine/core/modules/collection-rules';
import { DocService } from '@affine/core/modules/doc';
import { i18nTime, useI18n } from '@affine/i18n';
import { DateTimeIcon } from '@blocksuite/icons/rc';
import { useLiveData, useServices } from '@toeverything/infra';
import { cssVarV2 } from '@toeverything/theme/v2';
import { useCallback } from 'react';
@@ -66,53 +64,6 @@ export const DateValue = ({
);
};
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 (
<Tooltip content={date} side="top" align="end">
<PropertyValue
className={relativeDate ? '' : styles.empty}
isEmpty={!relativeDate}
>
{relativeDate}
</PropertyValue>
</Tooltip>
);
};
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 (
<Tooltip
content={
<span className={styles.tooltip}>
{t.t('created at', { time: i18nTime(createDate) })}
</span>
}
>
<div className={styles.dateDocListInlineProperty}>
{i18nTime(createDate, { relative: true })}
</div>
</Tooltip>
);
};
export const UpdatedDateDocListProperty = ({ doc }: DocListPropertyProps) => {
const t = useI18n();
const docMeta = useLiveData(doc.meta$);
const updatedDate = docMeta?.updatedDate;
if (!updatedDate) return null;
return (
<Tooltip
content={
<span className={styles.tooltip}>
{t.t('updated at', { time: i18nTime(updatedDate) })}
</span>
}
>
<div className={styles.dateDocListInlineProperty}>
{i18nTime(updatedDate, { relative: true })}
</div>
</Tooltip>
);
};
export const DateGroupHeader = ({ groupId, docCount }: GroupHeaderProps) => {
const date = groupId || 'No Date';
@@ -252,7 +159,3 @@ export const DateGroupHeader = ({ groupId, docCount }: GroupHeaderProps) => {
</PlainTextDocGroupHeader>
);
};
export const CreatedGroupHeader = (props: GroupHeaderProps) => {
return <PlainTextDocGroupHeader {...props} />;
};

View File

@@ -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,

View File

@@ -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));