mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-21 16:26:58 +08: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 { DoneIcon } from '@blocksuite/icons/rc';
|
||||||
import { useLiveData, useService } from '@toeverything/infra';
|
import { useLiveData, useService } from '@toeverything/infra';
|
||||||
import { cssVarV2 } from '@toeverything/theme/v2';
|
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
import { WorkspacePropertyName } from '../../properties';
|
import { WorkspacePropertyName } from '../../properties';
|
||||||
import {
|
import {
|
||||||
@@ -15,6 +16,7 @@ import {
|
|||||||
isSupportedWorkspacePropertyType,
|
isSupportedWorkspacePropertyType,
|
||||||
WorkspacePropertyTypes,
|
WorkspacePropertyTypes,
|
||||||
} from '../../workspace-property-types';
|
} from '../../workspace-property-types';
|
||||||
|
import { generateExplorerPropertyList } from '../properties';
|
||||||
|
|
||||||
const PropertyGroupByName = ({ groupBy }: { groupBy: GroupByParams }) => {
|
const PropertyGroupByName = ({ groupBy }: { groupBy: GroupByParams }) => {
|
||||||
const workspacePropertyService = useService(WorkspacePropertyService);
|
const workspacePropertyService = useService(WorkspacePropertyService);
|
||||||
@@ -49,37 +51,89 @@ export const GroupByList = ({
|
|||||||
onChange?: (next: GroupByParams) => void;
|
onChange?: (next: GroupByParams) => void;
|
||||||
}) => {
|
}) => {
|
||||||
const workspacePropertyService = useService(WorkspacePropertyService);
|
const workspacePropertyService = useService(WorkspacePropertyService);
|
||||||
const propertyList = useLiveData(workspacePropertyService.properties$);
|
const propertyList = useLiveData(workspacePropertyService.sortedProperties$);
|
||||||
|
const explorerPropertyList = useMemo(() => {
|
||||||
|
return generateExplorerPropertyList(propertyList);
|
||||||
|
}, [propertyList]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{propertyList.map(v => {
|
{explorerPropertyList.map(property => (
|
||||||
const allowInGroupBy = isSupportedWorkspacePropertyType(v.type)
|
<GroupByListItem
|
||||||
? WorkspacePropertyTypes[v.type].allowInGroupBy
|
key={property.systemProperty?.type ?? property.workspaceProperty?.id}
|
||||||
: false;
|
property={property}
|
||||||
if (!allowInGroupBy) {
|
groupBy={groupBy}
|
||||||
return null;
|
onChange={onChange}
|
||||||
}
|
/>
|
||||||
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>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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 { SortDownIcon, SortUpIcon } from '@blocksuite/icons/rc';
|
||||||
import { useLiveData, useService } from '@toeverything/infra';
|
import { useLiveData, useService } from '@toeverything/infra';
|
||||||
import { cssVarV2 } from '@toeverything/theme/v2';
|
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
import { WorkspacePropertyName } from '../../properties';
|
import { WorkspacePropertyName } from '../../properties';
|
||||||
import {
|
import {
|
||||||
@@ -15,6 +16,7 @@ import {
|
|||||||
isSupportedWorkspacePropertyType,
|
isSupportedWorkspacePropertyType,
|
||||||
WorkspacePropertyTypes,
|
WorkspacePropertyTypes,
|
||||||
} from '../../workspace-property-types';
|
} from '../../workspace-property-types';
|
||||||
|
import { generateExplorerPropertyList } from '../properties';
|
||||||
|
|
||||||
const PropertyOrderByName = ({ orderBy }: { orderBy: OrderByParams }) => {
|
const PropertyOrderByName = ({ orderBy }: { orderBy: OrderByParams }) => {
|
||||||
const workspacePropertyService = useService(WorkspacePropertyService);
|
const workspacePropertyService = useService(WorkspacePropertyService);
|
||||||
@@ -49,43 +51,95 @@ export const OrderByList = ({
|
|||||||
onChange?: (next: OrderByParams) => void;
|
onChange?: (next: OrderByParams) => void;
|
||||||
}) => {
|
}) => {
|
||||||
const workspacePropertyService = useService(WorkspacePropertyService);
|
const workspacePropertyService = useService(WorkspacePropertyService);
|
||||||
const propertyList = useLiveData(workspacePropertyService.properties$);
|
const propertyList = useLiveData(workspacePropertyService.sortedProperties$);
|
||||||
|
const explorerPropertyList = useMemo(() => {
|
||||||
|
return generateExplorerPropertyList(propertyList);
|
||||||
|
}, [propertyList]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{propertyList.map(v => {
|
{explorerPropertyList.map(property => (
|
||||||
const allowInOrderBy = isSupportedWorkspacePropertyType(v.type)
|
<OrderByListItem
|
||||||
? WorkspacePropertyTypes[v.type].allowInOrderBy
|
key={property.systemProperty?.type ?? property.workspaceProperty?.id}
|
||||||
: false;
|
property={property}
|
||||||
const active = orderBy?.type === 'property' && orderBy?.key === v.id;
|
orderBy={orderBy}
|
||||||
if (!allowInOrderBy) {
|
onChange={onChange}
|
||||||
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>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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 { Button, Divider } from '@affine/component';
|
||||||
import {
|
import { WorkspacePropertyService } from '@affine/core/modules/workspace-property';
|
||||||
WorkspacePropertyService,
|
|
||||||
type WorkspacePropertyType,
|
|
||||||
} from '@affine/core/modules/workspace-property';
|
|
||||||
import { useI18n } from '@affine/i18n';
|
import { useI18n } from '@affine/i18n';
|
||||||
import { useLiveData, useService } from '@toeverything/infra';
|
import { useLiveData, useService } from '@toeverything/infra';
|
||||||
import { useCallback, useMemo } from 'react';
|
import { useCallback, useMemo } from 'react';
|
||||||
|
|
||||||
import { WorkspacePropertyName } from '../../properties';
|
import { WorkspacePropertyName } from '../../properties';
|
||||||
import { WorkspacePropertyTypes } from '../../workspace-property-types';
|
import { generateExplorerPropertyList } from '../properties';
|
||||||
import type { ExplorerDisplayPreference } from '../types';
|
import type { ExplorerDisplayPreference } from '../types';
|
||||||
import * as styles from './properties.css';
|
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 = ({
|
export const DisplayProperties = ({
|
||||||
displayPreference,
|
displayPreference,
|
||||||
onDisplayPreferenceChange,
|
onDisplayPreferenceChange,
|
||||||
@@ -40,26 +20,15 @@ export const DisplayProperties = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
const workspacePropertyService = useService(WorkspacePropertyService);
|
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 displayProperties = displayPreference.displayProperties;
|
||||||
const showIcon = displayPreference.showDocIcon ?? false;
|
const showIcon = displayPreference.showDocIcon ?? false;
|
||||||
const showBody = displayPreference.showDocPreview ?? 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(
|
const handleDisplayPropertiesChange = useCallback(
|
||||||
(displayProperties: string[]) => {
|
(displayProperties: string[]) => {
|
||||||
onDisplayPreferenceChange({ ...displayPreference, displayProperties });
|
onDisplayPreferenceChange({ ...displayPreference, displayProperties });
|
||||||
@@ -68,11 +37,11 @@ export const DisplayProperties = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const handlePropertyClick = useCallback(
|
const handlePropertyClick = useCallback(
|
||||||
(propertyId: string) => {
|
(key: string) => {
|
||||||
handleDisplayPropertiesChange(
|
handleDisplayPropertiesChange(
|
||||||
displayProperties && displayProperties.includes(propertyId)
|
displayProperties && displayProperties.includes(key)
|
||||||
? displayProperties.filter(id => id !== propertyId)
|
? displayProperties.filter(k => k !== key)
|
||||||
: [...(displayProperties || []), propertyId]
|
: [...(displayProperties || []), key]
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
[displayProperties, handleDisplayPropertiesChange]
|
[displayProperties, handleDisplayPropertiesChange]
|
||||||
@@ -97,29 +66,40 @@ export const DisplayProperties = ({
|
|||||||
<section className={styles.sectionLabel}>
|
<section className={styles.sectionLabel}>
|
||||||
{t['com.affine.all-docs.display.properties']()}
|
{t['com.affine.all-docs.display.properties']()}
|
||||||
</section>
|
</section>
|
||||||
{propertiesGroups.map(list => {
|
<div className={styles.properties}>
|
||||||
return (
|
{explorerPropertyList
|
||||||
<div className={styles.properties} key={list.type}>
|
.filter(p => p.systemProperty)
|
||||||
{list.properties.map(({ property }) => {
|
.map(property => {
|
||||||
return (
|
return (
|
||||||
<Button
|
<PropertyRenderer
|
||||||
key={property.id}
|
key={
|
||||||
data-show={
|
property.systemProperty?.type ??
|
||||||
displayProperties
|
property.workspaceProperty?.id
|
||||||
? displayProperties.includes(property.id)
|
}
|
||||||
: false
|
property={property}
|
||||||
}
|
displayProperties={displayProperties ?? []}
|
||||||
onClick={() => handlePropertyClick(property.id)}
|
handlePropertyClick={handlePropertyClick}
|
||||||
className={styles.property}
|
/>
|
||||||
data-property-id={property.id}
|
);
|
||||||
>
|
})}
|
||||||
<WorkspacePropertyName propertyInfo={property} />
|
</div>
|
||||||
</Button>
|
<div className={styles.properties}>
|
||||||
);
|
{explorerPropertyList
|
||||||
})}
|
.filter(p => !p.systemProperty)
|
||||||
</div>
|
.map(property => {
|
||||||
);
|
return (
|
||||||
})}
|
<PropertyRenderer
|
||||||
|
key={
|
||||||
|
property.systemProperty?.type ??
|
||||||
|
property.workspaceProperty?.id
|
||||||
|
}
|
||||||
|
property={property}
|
||||||
|
displayProperties={displayProperties ?? []}
|
||||||
|
handlePropertyClick={handlePropertyClick}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
<Divider size="thinner" />
|
<Divider size="thinner" />
|
||||||
<section className={styles.sectionLabel}>
|
<section className={styles.sectionLabel}>
|
||||||
{t['com.affine.all-docs.display.list-view']()}
|
{t['com.affine.all-docs.display.list-view']()}
|
||||||
@@ -143,3 +123,46 @@ export const DisplayProperties = ({
|
|||||||
</div>
|
</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 { memo, useCallback, useContext, useEffect, useMemo } from 'react';
|
||||||
|
|
||||||
import { ListFloatingToolbar } from '../../page-list/components/list-floating-toolbar';
|
import { ListFloatingToolbar } from '../../page-list/components/list-floating-toolbar';
|
||||||
|
import { SystemPropertyTypes } from '../../system-property-types';
|
||||||
import { WorkspacePropertyTypes } from '../../workspace-property-types';
|
import { WorkspacePropertyTypes } from '../../workspace-property-types';
|
||||||
import { DocExplorerContext } from '../context';
|
import { DocExplorerContext } from '../context';
|
||||||
import { DocListItem } from './doc-list-item';
|
import { DocListItem } from './doc-list-item';
|
||||||
@@ -30,7 +31,19 @@ const GroupHeader = memo(function GroupHeader({
|
|||||||
const groupKey = groupBy?.key;
|
const groupKey = groupBy?.key;
|
||||||
|
|
||||||
const header = useMemo(() => {
|
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);
|
const property = allProperties.find(p => p.id === groupKey);
|
||||||
if (!property) return null;
|
if (!property) return null;
|
||||||
|
|
||||||
|
|||||||
@@ -1,94 +1,97 @@
|
|||||||
import type { DocCustomPropertyInfo } from '@affine/core/modules/db';
|
import type { DocCustomPropertyInfo } from '@affine/core/modules/db';
|
||||||
import { type DocRecord, DocsService } from '@affine/core/modules/doc';
|
import { type DocRecord, DocsService } from '@affine/core/modules/doc';
|
||||||
import {
|
import { WorkspacePropertyService } from '@affine/core/modules/workspace-property';
|
||||||
WorkspacePropertyService,
|
|
||||||
type WorkspacePropertyType,
|
|
||||||
} from '@affine/core/modules/workspace-property';
|
|
||||||
import { useLiveData, useService } from '@toeverything/infra';
|
import { useLiveData, useService } from '@toeverything/infra';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import { useContext, useMemo } from 'react';
|
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 { DocExplorerContext } from '../context';
|
||||||
import { filterDisplayProperties } from '../display-menu/properties';
|
import { generateExplorerPropertyList } from '../properties';
|
||||||
import { listHide560, listHide750 } from './doc-list-item.css';
|
import { listHide560, listHide750 } from './doc-list-item.css';
|
||||||
import * as styles from './properties.css';
|
import * as styles from './properties.css';
|
||||||
|
|
||||||
const listInlinePropertyOrder: WorkspacePropertyType[] = [
|
const listInlinePropertyOrder: string[] = [
|
||||||
'createdAt',
|
'createdAt',
|
||||||
'updatedAt',
|
'updatedAt',
|
||||||
'createdBy',
|
'createdBy',
|
||||||
'updatedBy',
|
'updatedBy',
|
||||||
];
|
];
|
||||||
const cardInlinePropertyOrder: WorkspacePropertyType[] = [
|
const cardInlinePropertyOrder: string[] = [
|
||||||
'createdBy',
|
'createdBy',
|
||||||
'updatedBy',
|
'updatedBy',
|
||||||
'createdAt',
|
'createdAt',
|
||||||
'updatedAt',
|
'updatedAt',
|
||||||
];
|
];
|
||||||
|
|
||||||
const useProperties = (docId: string, view: 'list' | 'card') => {
|
const useProperties = (view: 'list' | 'card') => {
|
||||||
const contextValue = useContext(DocExplorerContext);
|
|
||||||
const displayProperties = useLiveData(contextValue.displayProperties$);
|
|
||||||
const docsService = useService(DocsService);
|
|
||||||
const workspacePropertyService = useService(WorkspacePropertyService);
|
const workspacePropertyService = useService(WorkspacePropertyService);
|
||||||
|
|
||||||
const doc = useLiveData(docsService.list.doc$(docId));
|
const propertyList = useLiveData(workspacePropertyService.sortedProperties$);
|
||||||
const properties = useLiveData(doc?.properties$);
|
|
||||||
const propertyList = useLiveData(workspacePropertyService.properties$);
|
const explorerPropertyList = useMemo(() => {
|
||||||
|
return generateExplorerPropertyList(propertyList);
|
||||||
|
}, [propertyList]);
|
||||||
|
|
||||||
const stackProperties = useMemo(
|
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(
|
const inlineProperties = useMemo(
|
||||||
() =>
|
() =>
|
||||||
properties
|
explorerPropertyList
|
||||||
? filterDisplayProperties(propertyList, 'inline')
|
.filter(
|
||||||
.filter(p => p.property.type !== 'tags')
|
property =>
|
||||||
.sort((a, b) => {
|
(property.systemProperty &&
|
||||||
const orderList =
|
property.systemProperty.showInDocList === 'inline') ||
|
||||||
view === 'list'
|
(property.workspaceProperty &&
|
||||||
? listInlinePropertyOrder
|
WorkspacePropertyTypes[property.workspaceProperty.type]
|
||||||
: cardInlinePropertyOrder;
|
.showInDocList === 'inline')
|
||||||
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'
|
|
||||||
)
|
)
|
||||||
: undefined;
|
.filter(p => p.systemProperty?.type !== 'tags')
|
||||||
}, [propertyList]);
|
.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(
|
return useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
doc,
|
|
||||||
displayProperties,
|
|
||||||
stackProperties,
|
stackProperties,
|
||||||
inlineProperties,
|
inlineProperties,
|
||||||
tagsProperty,
|
|
||||||
}),
|
}),
|
||||||
[doc, displayProperties, stackProperties, inlineProperties, tagsProperty]
|
[stackProperties, inlineProperties]
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
export const ListViewProperties = ({ docId }: { docId: string }) => {
|
export const ListViewProperties = ({ docId }: { docId: string }) => {
|
||||||
const {
|
const contextValue = useContext(DocExplorerContext);
|
||||||
doc,
|
const displayProperties = useLiveData(contextValue?.displayProperties$);
|
||||||
displayProperties,
|
const docsService = useService(DocsService);
|
||||||
stackProperties,
|
const doc = useLiveData(docsService.list.doc$(docId));
|
||||||
inlineProperties,
|
|
||||||
tagsProperty,
|
const { stackProperties, inlineProperties } = useProperties('list');
|
||||||
} = useProperties(docId, 'list');
|
|
||||||
|
|
||||||
if (!doc) {
|
if (!doc) {
|
||||||
return null;
|
return null;
|
||||||
@@ -99,99 +102,188 @@ export const ListViewProperties = ({ docId }: { docId: string }) => {
|
|||||||
{/* stack properties */}
|
{/* stack properties */}
|
||||||
<div className={clsx(styles.stackContainer, listHide750)}>
|
<div className={clsx(styles.stackContainer, listHide750)}>
|
||||||
<div className={styles.stackProperties}>
|
<div className={styles.stackProperties}>
|
||||||
{stackProperties.map(({ property, config }) => {
|
{stackProperties.map(({ systemProperty, workspaceProperty }) => {
|
||||||
if (!displayProperties?.includes(property.id)) {
|
const displayKey = systemProperty
|
||||||
|
? `system:${systemProperty.type}`
|
||||||
|
: workspaceProperty
|
||||||
|
? `property:${workspaceProperty.id}`
|
||||||
|
: null;
|
||||||
|
if (!displayKey || !displayProperties?.includes(displayKey)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return (
|
if (systemProperty) {
|
||||||
<PropertyRenderer
|
return (
|
||||||
key={property.id}
|
<SystemPropertyRenderer
|
||||||
doc={doc}
|
doc={doc}
|
||||||
property={property}
|
config={SystemPropertyTypes[systemProperty.type]}
|
||||||
config={config}
|
key={systemProperty.type}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
} else if (workspaceProperty) {
|
||||||
|
return (
|
||||||
|
<WorkspacePropertyRenderer
|
||||||
|
key={workspaceProperty.id}
|
||||||
|
doc={doc}
|
||||||
|
property={workspaceProperty}
|
||||||
|
config={WorkspacePropertyTypes[workspaceProperty.type]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
{tagsProperty &&
|
{displayProperties?.includes('system:tags') ? (
|
||||||
displayProperties?.includes(tagsProperty.property.id) ? (
|
|
||||||
<div className={styles.stackProperties}>
|
<div className={styles.stackProperties}>
|
||||||
<PropertyRenderer
|
<SystemPropertyRenderer
|
||||||
doc={doc}
|
doc={doc}
|
||||||
property={tagsProperty.property}
|
config={SystemPropertyTypes.tags}
|
||||||
config={tagsProperty.config}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
{/* inline properties */}
|
{/* inline properties */}
|
||||||
{inlineProperties.map(({ property, config }) => {
|
{inlineProperties.map(({ systemProperty, workspaceProperty }) => {
|
||||||
if (!displayProperties?.includes(property.id)) {
|
const displayKeys = [
|
||||||
|
systemProperty ? `system:${systemProperty.type}` : null,
|
||||||
|
workspaceProperty ? `property:${workspaceProperty.id}` : null,
|
||||||
|
];
|
||||||
|
if (!displayKeys.some(key => key && displayProperties?.includes(key))) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return (
|
if (systemProperty) {
|
||||||
<div
|
return (
|
||||||
key={property.id}
|
<SystemPropertyRenderer
|
||||||
className={clsx(styles.inlineProperty, listHide560)}
|
doc={doc}
|
||||||
>
|
config={SystemPropertyTypes[systemProperty.type]}
|
||||||
<PropertyRenderer doc={doc} property={property} config={config} />
|
key={systemProperty.type}
|
||||||
</div>
|
/>
|
||||||
);
|
);
|
||||||
|
} 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 }) => {
|
export const CardViewProperties = ({ docId }: { docId: string }) => {
|
||||||
const {
|
const contextValue = useContext(DocExplorerContext);
|
||||||
doc,
|
const displayProperties = useLiveData(contextValue?.displayProperties$);
|
||||||
displayProperties,
|
const docsService = useService(DocsService);
|
||||||
stackProperties,
|
const doc = useLiveData(docsService.list.doc$(docId));
|
||||||
inlineProperties,
|
|
||||||
tagsProperty,
|
const { stackProperties, inlineProperties } = useProperties('card');
|
||||||
} = useProperties(docId, 'card');
|
|
||||||
|
|
||||||
if (!doc) {
|
if (!doc) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.cardProperties}>
|
<>
|
||||||
{inlineProperties.map(({ property, config }) => {
|
{/* stack properties */}
|
||||||
if (!displayProperties?.includes(property.id)) {
|
<div className={styles.stackContainer}>
|
||||||
return null;
|
<div className={styles.stackProperties}>
|
||||||
}
|
{stackProperties.map(({ systemProperty, workspaceProperty }) => {
|
||||||
return (
|
const displayKeys = [
|
||||||
<div key={property.id} className={styles.inlineProperty}>
|
systemProperty ? `system:${systemProperty.type}` : null,
|
||||||
<PropertyRenderer doc={doc} property={property} config={config} />
|
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>
|
</div>
|
||||||
);
|
) : null}
|
||||||
})}
|
</div>
|
||||||
{tagsProperty && displayProperties?.includes(tagsProperty.property.id) ? (
|
{/* inline properties */}
|
||||||
<PropertyRenderer
|
{inlineProperties.map(({ systemProperty, workspaceProperty }) => {
|
||||||
doc={doc}
|
const displayKeys = [
|
||||||
property={tagsProperty.property}
|
systemProperty ? `system:${systemProperty.type}` : null,
|
||||||
config={tagsProperty.config}
|
workspaceProperty ? `property:${workspaceProperty.id}` : null,
|
||||||
/>
|
];
|
||||||
) : null}
|
if (!displayKeys.some(key => key && displayProperties?.includes(key))) {
|
||||||
{stackProperties.map(({ property, config }) => {
|
|
||||||
if (!displayProperties?.includes(property.id)) {
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return (
|
if (systemProperty) {
|
||||||
<PropertyRenderer
|
return (
|
||||||
key={property.id}
|
<SystemPropertyRenderer
|
||||||
doc={doc}
|
doc={doc}
|
||||||
property={property}
|
config={SystemPropertyTypes[systemProperty.type]}
|
||||||
config={config}
|
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,
|
property,
|
||||||
doc,
|
doc,
|
||||||
config,
|
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 { useI18n } from '@affine/i18n';
|
||||||
import { ArrowLeftBigIcon, FavoriteIcon, PlusIcon } from '@blocksuite/icons/rc';
|
import { ArrowLeftBigIcon, FavoriteIcon, PlusIcon } from '@blocksuite/icons/rc';
|
||||||
import { useLiveData, useService } from '@toeverything/infra';
|
import { useLiveData, useService } from '@toeverything/infra';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
|
import { generateExplorerPropertyList } from '../explorer/properties';
|
||||||
import { WorkspacePropertyIcon, WorkspacePropertyName } from '../properties';
|
import { WorkspacePropertyIcon, WorkspacePropertyName } from '../properties';
|
||||||
import { WorkspacePropertyTypes } from '../workspace-property-types';
|
import { WorkspacePropertyTypes } from '../workspace-property-types';
|
||||||
import * as styles from './styles.css';
|
import * as styles from './styles.css';
|
||||||
@@ -18,7 +20,13 @@ export const AddFilterMenu = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
const workspacePropertyService = useService(WorkspacePropertyService);
|
const workspacePropertyService = useService(WorkspacePropertyService);
|
||||||
const workspaceProperties = useLiveData(workspacePropertyService.properties$);
|
const workspaceProperties = useLiveData(
|
||||||
|
workspacePropertyService.sortedProperties$
|
||||||
|
);
|
||||||
|
const explorerPropertyList = useMemo(
|
||||||
|
() => generateExplorerPropertyList(workspaceProperties),
|
||||||
|
[workspaceProperties]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -64,34 +72,62 @@ export const AddFilterMenu = ({
|
|||||||
{t['com.affine.filter.is-public']()}
|
{t['com.affine.filter.is-public']()}
|
||||||
</span>
|
</span>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
{workspaceProperties.map(property => {
|
{explorerPropertyList.map(({ systemProperty, workspaceProperty }) => {
|
||||||
const type = WorkspacePropertyTypes[property.type];
|
if (systemProperty) {
|
||||||
const defaultFilter = type?.defaultFilter;
|
const defaultFilter =
|
||||||
if (!defaultFilter) {
|
'defaultFilter' in systemProperty && systemProperty.defaultFilter;
|
||||||
return null;
|
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 (
|
return null;
|
||||||
<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>
|
|
||||||
);
|
|
||||||
})}
|
})}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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 { FilterParams } from '@affine/core/modules/collection-rules';
|
||||||
|
import type { DocRecord } from '@affine/core/modules/doc';
|
||||||
import type { I18nString } from '@affine/i18n';
|
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 { FavoriteFilterValue } from './favorite';
|
||||||
import { SharedFilterValue } from './shared';
|
import { SharedFilterValue } from './shared';
|
||||||
import { TagsFilterValue } from './tags';
|
import { TagsDocListProperty, TagsFilterValue, TagsGroupHeader } from './tags';
|
||||||
|
|
||||||
export const SystemPropertyTypes = {
|
export const SystemPropertyTypes = {
|
||||||
tags: {
|
tags: {
|
||||||
@@ -16,6 +40,68 @@ export const SystemPropertyTypes = {
|
|||||||
'is-empty': 'com.affine.filter.is empty',
|
'is-empty': 'com.affine.filter.is empty',
|
||||||
},
|
},
|
||||||
filterValue: TagsFilterValue,
|
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: {
|
favorite: {
|
||||||
icon: FavoriteIcon,
|
icon: FavoriteIcon,
|
||||||
@@ -33,7 +119,7 @@ export const SystemPropertyTypes = {
|
|||||||
},
|
},
|
||||||
filterValue: SharedFilterValue,
|
filterValue: SharedFilterValue,
|
||||||
},
|
},
|
||||||
} satisfies {
|
} as {
|
||||||
[type: string]: {
|
[type: string]: {
|
||||||
icon: React.FC<React.SVGProps<SVGSVGElement>>;
|
icon: React.FC<React.SVGProps<SVGSVGElement>>;
|
||||||
name: I18nString;
|
name: I18nString;
|
||||||
@@ -45,13 +131,20 @@ export const SystemPropertyTypes = {
|
|||||||
filter: FilterParams;
|
filter: FilterParams;
|
||||||
onChange: (filter: FilterParams) => void;
|
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 type SystemPropertyType = keyof typeof SystemPropertyTypes;
|
||||||
|
|
||||||
export const isSupportedSystemPropertyType = (
|
export const isSupportedSystemPropertyType = (type?: string) => {
|
||||||
type?: string
|
|
||||||
): type is SystemPropertyType => {
|
|
||||||
return type ? type in SystemPropertyTypes : false;
|
return type ? type in SystemPropertyTypes : false;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,62 +1,5 @@
|
|||||||
import type { FilterParams } from '@affine/core/modules/collection-rules';
|
export {
|
||||||
import { TagService } from '@affine/core/modules/tag';
|
TagsDocListProperty,
|
||||||
import { useI18n } from '@affine/i18n';
|
TagsFilterValue,
|
||||||
import { useLiveData, useService } from '@toeverything/infra';
|
TagsGroupHeader,
|
||||||
import { cssVarV2 } from '@toeverything/theme/v2';
|
} from '../workspace-property-types/tags';
|
||||||
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;
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -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 { type ReactNode, useCallback, useMemo } from 'react';
|
||||||
|
|
||||||
import { PlainTextDocGroupHeader } from '../explorer/docs-view/group-header';
|
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 { MemberSelectorInline } from '../member-selector';
|
||||||
import * as styles from './created-updated-by.css';
|
import * as styles from './created-updated-by.css';
|
||||||
|
|
||||||
@@ -135,9 +135,7 @@ export const CreatedByUpdatedByFilterValue = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const CreatedByDocListInlineProperty = ({
|
export const CreatedByDocListInlineProperty = ({ doc }: { doc: DocRecord }) => {
|
||||||
doc,
|
|
||||||
}: DocListPropertyProps) => {
|
|
||||||
return (
|
return (
|
||||||
<CreatedByUpdatedByAvatar
|
<CreatedByUpdatedByAvatar
|
||||||
doc={doc}
|
doc={doc}
|
||||||
@@ -149,9 +147,7 @@ export const CreatedByDocListInlineProperty = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const UpdatedByDocListInlineProperty = ({
|
export const UpdatedByDocListInlineProperty = ({ doc }: { doc: DocRecord }) => {
|
||||||
doc,
|
|
||||||
}: DocListPropertyProps) => {
|
|
||||||
return (
|
return (
|
||||||
<CreatedByUpdatedByAvatar
|
<CreatedByUpdatedByAvatar
|
||||||
type="UpdatedBy"
|
type="UpdatedBy"
|
||||||
|
|||||||
@@ -1,28 +1,6 @@
|
|||||||
import { cssVar } from '@toeverything/theme';
|
import { cssVar } from '@toeverything/theme';
|
||||||
import { cssVarV2 } from '@toeverything/theme/v2';
|
|
||||||
import { style } from '@vanilla-extract/css';
|
import { style } from '@vanilla-extract/css';
|
||||||
|
|
||||||
export const empty = style({
|
export const empty = style({
|
||||||
color: cssVar('placeholderColor'),
|
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 type { FilterParams } from '@affine/core/modules/collection-rules';
|
||||||
import { DocService } from '@affine/core/modules/doc';
|
|
||||||
import { i18nTime, useI18n } from '@affine/i18n';
|
import { i18nTime, useI18n } from '@affine/i18n';
|
||||||
import { DateTimeIcon } from '@blocksuite/icons/rc';
|
import { DateTimeIcon } from '@blocksuite/icons/rc';
|
||||||
import { useLiveData, useServices } from '@toeverything/infra';
|
|
||||||
import { cssVarV2 } from '@toeverything/theme/v2';
|
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||||
import { useCallback } from 'react';
|
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 = ({
|
export const DateFilterValue = ({
|
||||||
filter,
|
filter,
|
||||||
onChange,
|
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) => {
|
export const DateGroupHeader = ({ groupId, docCount }: GroupHeaderProps) => {
|
||||||
const date = groupId || 'No Date';
|
const date = groupId || 'No Date';
|
||||||
|
|
||||||
@@ -252,7 +159,3 @@ export const DateGroupHeader = ({ groupId, docCount }: GroupHeaderProps) => {
|
|||||||
</PlainTextDocGroupHeader>
|
</PlainTextDocGroupHeader>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const CreatedGroupHeader = (props: GroupHeaderProps) => {
|
|
||||||
return <PlainTextDocGroupHeader {...props} />;
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -28,6 +28,16 @@ import {
|
|||||||
CheckboxGroupHeader,
|
CheckboxGroupHeader,
|
||||||
CheckboxValue,
|
CheckboxValue,
|
||||||
} from './checkbox';
|
} from './checkbox';
|
||||||
|
import {
|
||||||
|
CreateAtDocListProperty,
|
||||||
|
CreateAtValue,
|
||||||
|
CreatedAtFilterValue,
|
||||||
|
CreatedAtGroupHeader,
|
||||||
|
UpdatedAtDocListProperty,
|
||||||
|
UpdatedAtFilterValue,
|
||||||
|
UpdatedAtGroupHeader,
|
||||||
|
UpdatedAtValue,
|
||||||
|
} from './created-updated-at';
|
||||||
import {
|
import {
|
||||||
CreatedByDocListInlineProperty,
|
CreatedByDocListInlineProperty,
|
||||||
CreatedByUpdatedByFilterValue,
|
CreatedByUpdatedByFilterValue,
|
||||||
@@ -37,15 +47,10 @@ import {
|
|||||||
UpdatedByValue,
|
UpdatedByValue,
|
||||||
} from './created-updated-by';
|
} from './created-updated-by';
|
||||||
import {
|
import {
|
||||||
CreateDateDocListProperty,
|
|
||||||
CreateDateValue,
|
|
||||||
CreatedGroupHeader,
|
|
||||||
DateDocListProperty,
|
DateDocListProperty,
|
||||||
DateFilterValue,
|
DateFilterValue,
|
||||||
DateGroupHeader,
|
DateGroupHeader,
|
||||||
DateValue,
|
DateValue,
|
||||||
UpdatedDateDocListProperty,
|
|
||||||
UpdatedDateValue,
|
|
||||||
} from './date';
|
} from './date';
|
||||||
import {
|
import {
|
||||||
DocPrimaryModeDocListProperty,
|
DocPrimaryModeDocListProperty,
|
||||||
@@ -79,7 +84,7 @@ import {
|
|||||||
TextValue,
|
TextValue,
|
||||||
} from './text';
|
} from './text';
|
||||||
|
|
||||||
const DateFilterMethod = {
|
export const DateFilterMethod = {
|
||||||
after: 'com.affine.filter.after',
|
after: 'com.affine.filter.after',
|
||||||
before: 'com.affine.filter.before',
|
before: 'com.affine.filter.before',
|
||||||
between: 'com.affine.filter.between',
|
between: 'com.affine.filter.between',
|
||||||
@@ -213,7 +218,7 @@ export const WorkspacePropertyTypes = {
|
|||||||
},
|
},
|
||||||
updatedAt: {
|
updatedAt: {
|
||||||
icon: DateTimeIcon,
|
icon: DateTimeIcon,
|
||||||
value: UpdatedDateValue,
|
value: UpdatedAtValue,
|
||||||
name: 'com.affine.page-properties.property.updatedAt',
|
name: 'com.affine.page-properties.property.updatedAt',
|
||||||
description: 'com.affine.page-properties.property.updatedAt.tooltips',
|
description: 'com.affine.page-properties.property.updatedAt.tooltips',
|
||||||
renameable: false,
|
renameable: false,
|
||||||
@@ -222,15 +227,15 @@ export const WorkspacePropertyTypes = {
|
|||||||
filterMethod: {
|
filterMethod: {
|
||||||
...DateFilterMethod,
|
...DateFilterMethod,
|
||||||
},
|
},
|
||||||
filterValue: DateFilterValue,
|
filterValue: UpdatedAtFilterValue,
|
||||||
defaultFilter: { method: 'this-week' },
|
defaultFilter: { method: 'this-week' },
|
||||||
showInDocList: 'inline',
|
showInDocList: 'inline',
|
||||||
docListProperty: UpdatedDateDocListProperty,
|
docListProperty: UpdatedAtDocListProperty,
|
||||||
groupHeader: CreatedGroupHeader,
|
groupHeader: UpdatedAtGroupHeader,
|
||||||
},
|
},
|
||||||
createdAt: {
|
createdAt: {
|
||||||
icon: HistoryIcon,
|
icon: HistoryIcon,
|
||||||
value: CreateDateValue,
|
value: CreateAtValue,
|
||||||
name: 'com.affine.page-properties.property.createdAt',
|
name: 'com.affine.page-properties.property.createdAt',
|
||||||
description: 'com.affine.page-properties.property.createdAt.tooltips',
|
description: 'com.affine.page-properties.property.createdAt.tooltips',
|
||||||
renameable: false,
|
renameable: false,
|
||||||
@@ -239,11 +244,11 @@ export const WorkspacePropertyTypes = {
|
|||||||
filterMethod: {
|
filterMethod: {
|
||||||
...DateFilterMethod,
|
...DateFilterMethod,
|
||||||
},
|
},
|
||||||
filterValue: DateFilterValue,
|
filterValue: CreatedAtFilterValue,
|
||||||
defaultFilter: { method: 'this-week' },
|
defaultFilter: { method: 'this-week' },
|
||||||
showInDocList: 'inline',
|
showInDocList: 'inline',
|
||||||
docListProperty: CreateDateDocListProperty,
|
docListProperty: CreateAtDocListProperty,
|
||||||
groupHeader: CreatedGroupHeader,
|
groupHeader: CreatedAtGroupHeader,
|
||||||
},
|
},
|
||||||
docPrimaryMode: {
|
docPrimaryMode: {
|
||||||
icon: FileIcon,
|
icon: FileIcon,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { PropertyValue } from '@affine/component';
|
import { PropertyValue } from '@affine/component';
|
||||||
import type { FilterParams } from '@affine/core/modules/collection-rules';
|
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 { type Tag, TagService } from '@affine/core/modules/tag';
|
||||||
import { WorkspaceService } from '@affine/core/modules/workspace';
|
import { WorkspaceService } from '@affine/core/modules/workspace';
|
||||||
import { useI18n } from '@affine/i18n';
|
import { useI18n } from '@affine/i18n';
|
||||||
@@ -11,7 +11,7 @@ import { useCallback, useMemo } from 'react';
|
|||||||
|
|
||||||
import { PlainTextDocGroupHeader } from '../explorer/docs-view/group-header';
|
import { PlainTextDocGroupHeader } from '../explorer/docs-view/group-header';
|
||||||
import { StackProperty } from '../explorer/docs-view/stack-property';
|
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 { useNavigateHelper } from '../hooks/use-navigate-helper';
|
||||||
import type { PropertyValueProps } from '../properties/types';
|
import type { PropertyValueProps } from '../properties/types';
|
||||||
import {
|
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 tagList = useService(TagService).tagList;
|
||||||
const tags = useLiveData(tagList.tagsByPageId$(doc.id));
|
const tags = useLiveData(tagList.tagsByPageId$(doc.id));
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user