mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-14 05:14:54 +00:00
feat(core): add system property types support (#12332)
This commit is contained in:
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
67
packages/frontend/core/src/components/explorer/properties.ts
Normal file
67
packages/frontend/core/src/components/explorer/properties.ts
Normal 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;
|
||||
}
|
||||
@@ -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;
|
||||
})}
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
export {
|
||||
CreateAtDocListProperty,
|
||||
CreatedAtFilterValue,
|
||||
CreatedAtGroupHeader,
|
||||
UpdatedAtDocListProperty,
|
||||
UpdatedAtFilterValue,
|
||||
UpdatedAtGroupHeader,
|
||||
} from '../workspace-property-types/created-updated-at';
|
||||
@@ -0,0 +1,6 @@
|
||||
export {
|
||||
CreatedByDocListInlineProperty,
|
||||
CreatedByUpdatedByFilterValue,
|
||||
ModifiedByGroupHeader,
|
||||
UpdatedByDocListInlineProperty,
|
||||
} from '../workspace-property-types/created-updated-by';
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
@@ -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';
|
||||
@@ -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"
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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} />;
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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));
|
||||
|
||||
|
||||
Reference in New Issue
Block a user