mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-09-07 01:09:54 +08:00
refactor(core): refactor tag to use di (#6079)
use case ``` const tagService = useService(TagService); const tags = useLiveData(tagService.tags); const currentTagLiveData = tagService.tagByTagId(tagId); const currentTag = useLiveData(currentTagLiveData); ```
This commit is contained in:
Vendored
+4
@@ -53,6 +53,8 @@ export const collectionSchema = z.object({
|
|||||||
name: z.string(),
|
name: z.string(),
|
||||||
filterList: z.array(filterSchema),
|
filterList: z.array(filterSchema),
|
||||||
allowList: z.array(z.string()),
|
allowList: z.array(z.string()),
|
||||||
|
createDate: z.union([z.date(), z.number()]).optional(),
|
||||||
|
updateDate: z.union([z.date(), z.number()]).optional(),
|
||||||
});
|
});
|
||||||
export const deletedCollectionSchema = z.object({
|
export const deletedCollectionSchema = z.object({
|
||||||
userId: z.string().optional(),
|
userId: z.string().optional(),
|
||||||
@@ -78,6 +80,8 @@ export const tagSchema = z.object({
|
|||||||
value: z.string(),
|
value: z.string(),
|
||||||
color: z.string(),
|
color: z.string(),
|
||||||
parentId: z.string().optional(),
|
parentId: z.string().optional(),
|
||||||
|
createDate: z.union([z.date(), z.number()]).optional(),
|
||||||
|
updateDate: z.union([z.date(), z.number()]).optional(),
|
||||||
});
|
});
|
||||||
export type Tag = z.input<typeof tagSchema>;
|
export type Tag = z.input<typeof tagSchema>;
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import type { PagePropertiesManager } from './page-properties-manager';
|
|||||||
export const managerContext = createContext<PagePropertiesManager>();
|
export const managerContext = createContext<PagePropertiesManager>();
|
||||||
|
|
||||||
type TagColorHelper<T> = T extends `paletteLine${infer Color}` ? Color : never;
|
type TagColorHelper<T> = T extends `paletteLine${infer Color}` ? Color : never;
|
||||||
type TagColorName = TagColorHelper<Parameters<typeof cssVar>[0]>;
|
export type TagColorName = TagColorHelper<Parameters<typeof cssVar>[0]>;
|
||||||
|
|
||||||
const tagColorIds: TagColorName[] = [
|
const tagColorIds: TagColorName[] = [
|
||||||
'Red',
|
'Red',
|
||||||
|
|||||||
+2
-15
@@ -1,6 +1,5 @@
|
|||||||
import { Checkbox, DatePicker, Menu } from '@affine/component';
|
import { Checkbox, DatePicker, Menu } from '@affine/component';
|
||||||
import { useAllBlockSuiteDocMeta } from '@affine/core/hooks/use-all-block-suite-page-meta';
|
import { useAllBlockSuiteDocMeta } from '@affine/core/hooks/use-all-block-suite-page-meta';
|
||||||
import { WorkspaceLegacyProperties } from '@affine/core/modules/workspace';
|
|
||||||
import type {
|
import type {
|
||||||
PageInfoCustomProperty,
|
PageInfoCustomProperty,
|
||||||
PageInfoCustomPropertyMeta,
|
PageInfoCustomPropertyMeta,
|
||||||
@@ -9,7 +8,7 @@ import type {
|
|||||||
import { timestampToLocalDate } from '@affine/core/utils';
|
import { timestampToLocalDate } from '@affine/core/utils';
|
||||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
import { assertExists } from '@blocksuite/global/utils';
|
import { assertExists } from '@blocksuite/global/utils';
|
||||||
import { Doc, useLiveData, useService, Workspace } from '@toeverything/infra';
|
import { Doc, useService, Workspace } from '@toeverything/infra';
|
||||||
import { noop } from 'lodash-es';
|
import { noop } from 'lodash-es';
|
||||||
import {
|
import {
|
||||||
type ChangeEventHandler,
|
type ChangeEventHandler,
|
||||||
@@ -179,29 +178,17 @@ export const TagsValue = () => {
|
|||||||
const page = useService(Doc);
|
const page = useService(Doc);
|
||||||
const docCollection = workspace.docCollection;
|
const docCollection = workspace.docCollection;
|
||||||
const pageMetas = useAllBlockSuiteDocMeta(docCollection);
|
const pageMetas = useAllBlockSuiteDocMeta(docCollection);
|
||||||
const legacyProperties = useService(WorkspaceLegacyProperties);
|
|
||||||
const options = useLiveData(legacyProperties.tagOptions$);
|
|
||||||
|
|
||||||
const pageMeta = pageMetas.find(x => x.id === page.id);
|
const pageMeta = pageMetas.find(x => x.id === page.id);
|
||||||
assertExists(pageMeta, 'pageMeta should exist');
|
assertExists(pageMeta, 'pageMeta should exist');
|
||||||
const tagIds = pageMeta.tags;
|
|
||||||
const t = useAFFiNEI18N();
|
const t = useAFFiNEI18N();
|
||||||
const onChange = useCallback(
|
|
||||||
(tags: string[]) => {
|
|
||||||
legacyProperties.updatePageTags(page.id, tags);
|
|
||||||
},
|
|
||||||
[legacyProperties, page.id]
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TagsInlineEditor
|
<TagsInlineEditor
|
||||||
className={styles.propertyRowValueCell}
|
className={styles.propertyRowValueCell}
|
||||||
placeholder={t['com.affine.page-properties.property-value-placeholder']()}
|
placeholder={t['com.affine.page-properties.property-value-placeholder']()}
|
||||||
value={tagIds}
|
pageId={page.id}
|
||||||
options={options}
|
|
||||||
readonly={page.blockSuiteDoc.readonly}
|
readonly={page.blockSuiteDoc.readonly}
|
||||||
onChange={onChange}
|
|
||||||
onOptionsChange={legacyProperties.updateTagOptions}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
+71
-108
@@ -6,13 +6,13 @@ import {
|
|||||||
Scrollable,
|
Scrollable,
|
||||||
} from '@affine/component';
|
} from '@affine/component';
|
||||||
import { useNavigateHelper } from '@affine/core/hooks/use-navigate-helper';
|
import { useNavigateHelper } from '@affine/core/hooks/use-navigate-helper';
|
||||||
|
import { TagService } from '@affine/core/modules/tag';
|
||||||
import { WorkspaceLegacyProperties } from '@affine/core/modules/workspace';
|
import { WorkspaceLegacyProperties } from '@affine/core/modules/workspace';
|
||||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
import { DeleteIcon, MoreHorizontalIcon, TagsIcon } from '@blocksuite/icons';
|
import { DeleteIcon, MoreHorizontalIcon, TagsIcon } from '@blocksuite/icons';
|
||||||
import type { Tag } from '@blocksuite/store';
|
import { useLiveData } from '@toeverything/infra';
|
||||||
import { useService } from '@toeverything/infra/di';
|
import { useService } from '@toeverything/infra/di';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import { nanoid } from 'nanoid';
|
|
||||||
import {
|
import {
|
||||||
type HTMLAttributes,
|
type HTMLAttributes,
|
||||||
type PropsWithChildren,
|
type PropsWithChildren,
|
||||||
@@ -22,16 +22,13 @@ import {
|
|||||||
useState,
|
useState,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
|
|
||||||
import { TagItem } from '../../page-list';
|
import { TagItem, TempTagItem } from '../../page-list';
|
||||||
import { tagColors } from './common';
|
import { tagColors } from './common';
|
||||||
import { type MenuItemOption, renderMenuItemOptions } from './menu-items';
|
import { type MenuItemOption, renderMenuItemOptions } from './menu-items';
|
||||||
import * as styles from './tags-inline-editor.css';
|
import * as styles from './tags-inline-editor.css';
|
||||||
|
|
||||||
interface TagsEditorProps {
|
interface TagsEditorProps {
|
||||||
value: string[]; // selected tag ids
|
pageId: string;
|
||||||
onChange?: (value: string[]) => void;
|
|
||||||
options: Tag[];
|
|
||||||
onOptionsChange?: (options: Tag[]) => void; // adding/updating/removing tags
|
|
||||||
readonly?: boolean;
|
readonly?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -40,25 +37,26 @@ interface InlineTagsListProps
|
|||||||
Omit<TagsEditorProps, 'onOptionsChange'> {}
|
Omit<TagsEditorProps, 'onOptionsChange'> {}
|
||||||
|
|
||||||
const InlineTagsList = ({
|
const InlineTagsList = ({
|
||||||
value,
|
pageId,
|
||||||
onChange,
|
|
||||||
options,
|
|
||||||
readonly,
|
readonly,
|
||||||
children,
|
children,
|
||||||
}: PropsWithChildren<InlineTagsListProps>) => {
|
}: PropsWithChildren<InlineTagsListProps>) => {
|
||||||
|
const tagService = useService(TagService);
|
||||||
|
const tags = useLiveData(tagService.tags);
|
||||||
|
const tagIds = useLiveData(tagService.tagIdsByPageId(pageId));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.inlineTagsContainer} data-testid="inline-tags-list">
|
<div className={styles.inlineTagsContainer} data-testid="inline-tags-list">
|
||||||
{value.map((tagId, idx) => {
|
{tagIds.map((tagId, idx) => {
|
||||||
const tag = options.find(t => t.id === tagId);
|
const tag = tags.find(t => t.id === tagId);
|
||||||
if (!tag) {
|
if (!tag) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const onRemoved =
|
const onRemoved = readonly
|
||||||
readonly || !onChange
|
? undefined
|
||||||
? undefined
|
: () => {
|
||||||
: () => {
|
tag.untag(pageId);
|
||||||
onChange(value.filter(v => v !== tagId));
|
};
|
||||||
};
|
|
||||||
return (
|
return (
|
||||||
<TagItem
|
<TagItem
|
||||||
key={tagId}
|
key={tagId}
|
||||||
@@ -74,18 +72,16 @@ const InlineTagsList = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const filterOption = (option: Tag, inputValue?: string) => {
|
|
||||||
const trimmedValue = inputValue?.trim().toLowerCase() ?? '';
|
|
||||||
const trimmedOptionValue = option.value.trim().toLowerCase();
|
|
||||||
return trimmedOptionValue.includes(trimmedValue);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const EditTagMenu = ({
|
export const EditTagMenu = ({
|
||||||
tag,
|
tagId,
|
||||||
children,
|
children,
|
||||||
}: PropsWithChildren<{ tag: Tag }>) => {
|
}: PropsWithChildren<{ tagId: string }>) => {
|
||||||
const t = useAFFiNEI18N();
|
const t = useAFFiNEI18N();
|
||||||
const legacyProperties = useService(WorkspaceLegacyProperties);
|
const legacyProperties = useService(WorkspaceLegacyProperties);
|
||||||
|
const tagService = useService(TagService);
|
||||||
|
const tag = useLiveData(tagService.tagByTagId(tagId));
|
||||||
|
const tagColor = useLiveData(tag?.color);
|
||||||
|
const tagValue = useLiveData(tag?.value);
|
||||||
const navigate = useNavigateHelper();
|
const navigate = useNavigateHelper();
|
||||||
|
|
||||||
const menuProps = useMemo(() => {
|
const menuProps = useMemo(() => {
|
||||||
@@ -94,14 +90,11 @@ export const EditTagMenu = ({
|
|||||||
if (name.trim() === '') {
|
if (name.trim() === '') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
legacyProperties.updateTagOption(tag.id, {
|
tag?.rename(name);
|
||||||
...tag,
|
|
||||||
value: name,
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
options.push(
|
options.push(
|
||||||
<Input
|
<Input
|
||||||
defaultValue={tag.value}
|
defaultValue={tagValue}
|
||||||
onBlur={e => {
|
onBlur={e => {
|
||||||
updateTagName(e.currentTarget.value);
|
updateTagName(e.currentTarget.value);
|
||||||
}}
|
}}
|
||||||
@@ -123,7 +116,7 @@ export const EditTagMenu = ({
|
|||||||
icon: <DeleteIcon />,
|
icon: <DeleteIcon />,
|
||||||
type: 'danger',
|
type: 'danger',
|
||||||
onClick() {
|
onClick() {
|
||||||
legacyProperties.removeTagOption(tag.id);
|
tagService.deleteTag(tag?.id || '');
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -131,7 +124,7 @@ export const EditTagMenu = ({
|
|||||||
text: t['com.affine.page-properties.tags.open-tags-page'](),
|
text: t['com.affine.page-properties.tags.open-tags-page'](),
|
||||||
icon: <TagsIcon />,
|
icon: <TagsIcon />,
|
||||||
onClick() {
|
onClick() {
|
||||||
navigate.jumpToTag(legacyProperties.workspaceId, tag.id);
|
navigate.jumpToTag(legacyProperties.workspaceId, tag?.id || '');
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -151,12 +144,9 @@ export const EditTagMenu = ({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
checked: tag.color === color,
|
checked: tagColor === color,
|
||||||
onClick() {
|
onClick() {
|
||||||
legacyProperties.updateTagOption(tag.id, {
|
tag?.changeColor(color);
|
||||||
...tag,
|
|
||||||
color,
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
@@ -171,26 +161,35 @@ export const EditTagMenu = ({
|
|||||||
},
|
},
|
||||||
items,
|
items,
|
||||||
} satisfies Partial<MenuProps>;
|
} satisfies Partial<MenuProps>;
|
||||||
}, [legacyProperties, navigate, t, tag]);
|
}, [
|
||||||
|
legacyProperties.workspaceId,
|
||||||
|
navigate,
|
||||||
|
t,
|
||||||
|
tag,
|
||||||
|
tagColor,
|
||||||
|
tagService,
|
||||||
|
tagValue,
|
||||||
|
]);
|
||||||
|
|
||||||
return <Menu {...menuProps}>{children}</Menu>;
|
return <Menu {...menuProps}>{children}</Menu>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const TagsEditor = ({
|
export const TagsEditor = ({ pageId, readonly }: TagsEditorProps) => {
|
||||||
options,
|
|
||||||
value,
|
|
||||||
onChange,
|
|
||||||
onOptionsChange,
|
|
||||||
readonly,
|
|
||||||
}: TagsEditorProps) => {
|
|
||||||
const t = useAFFiNEI18N();
|
const t = useAFFiNEI18N();
|
||||||
|
const tagService = useService(TagService);
|
||||||
|
const tags = useLiveData(tagService.tags);
|
||||||
|
const tagIds = useLiveData(tagService.tagIdsByPageId(pageId));
|
||||||
const [inputValue, setInputValue] = useState('');
|
const [inputValue, setInputValue] = useState('');
|
||||||
const exactMatch = options.find(o => o.value === inputValue);
|
|
||||||
const filteredOptions = useMemo(
|
const exactMatch = useLiveData(tagService.tagByTagValue(inputValue));
|
||||||
() =>
|
|
||||||
options.filter(o => (inputValue ? filterOption(o, inputValue) : true)),
|
const filteredLiveData = useMemo(() => {
|
||||||
[inputValue, options]
|
if (inputValue) {
|
||||||
);
|
return tagService.filterTagsByName(inputValue);
|
||||||
|
}
|
||||||
|
return tagService.tags;
|
||||||
|
}, [inputValue, tagService]);
|
||||||
|
const filteredTags = useLiveData(filteredLiveData);
|
||||||
|
|
||||||
const onInputChange = useCallback(
|
const onInputChange = useCallback(
|
||||||
(e: React.ChangeEvent<HTMLInputElement>) => {
|
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
@@ -201,11 +200,11 @@ export const TagsEditor = ({
|
|||||||
|
|
||||||
const onAddTag = useCallback(
|
const onAddTag = useCallback(
|
||||||
(id: string) => {
|
(id: string) => {
|
||||||
if (!value.includes(id)) {
|
if (!tagIds.includes(id)) {
|
||||||
onChange?.([...value, id]);
|
tags.find(o => o.id === id)?.tag(pageId);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[onChange, value]
|
[pageId, tagIds, tags]
|
||||||
);
|
);
|
||||||
|
|
||||||
const [nextColor, rotateNextColor] = useReducer(
|
const [nextColor, rotateNextColor] = useReducer(
|
||||||
@@ -221,17 +220,11 @@ export const TagsEditor = ({
|
|||||||
if (!name.trim()) {
|
if (!name.trim()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const newTag = {
|
|
||||||
id: nanoid(),
|
|
||||||
value: name.trim(),
|
|
||||||
color: nextColor,
|
|
||||||
};
|
|
||||||
rotateNextColor();
|
rotateNextColor();
|
||||||
onOptionsChange?.([...options, newTag]);
|
const newTag = tagService.createTag(name.trim(), nextColor);
|
||||||
onChange?.([...value, newTag.id]);
|
newTag.tag(pageId);
|
||||||
},
|
},
|
||||||
[nextColor, onChange, onOptionsChange, options, value]
|
[nextColor, pageId, tagService]
|
||||||
);
|
);
|
||||||
|
|
||||||
const onInputKeyDown = useCallback(
|
const onInputKeyDown = useCallback(
|
||||||
@@ -243,22 +236,18 @@ export const TagsEditor = ({
|
|||||||
onCreateTag(inputValue);
|
onCreateTag(inputValue);
|
||||||
}
|
}
|
||||||
setInputValue('');
|
setInputValue('');
|
||||||
} else if (e.key === 'Backspace' && inputValue === '' && value.length) {
|
} else if (e.key === 'Backspace' && inputValue === '' && tagIds.length) {
|
||||||
onChange?.(value.slice(0, value.length - 1));
|
const lastTagId = tagIds[tagIds.length - 1];
|
||||||
|
tags.find(tag => tag.id === lastTagId)?.untag(pageId);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[exactMatch, inputValue, onAddTag, onChange, onCreateTag, value]
|
[exactMatch, inputValue, onAddTag, onCreateTag, pageId, tagIds, tags]
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div data-testid="tags-editor-popup" className={styles.tagsEditorRoot}>
|
<div data-testid="tags-editor-popup" className={styles.tagsEditorRoot}>
|
||||||
<div className={styles.tagsEditorSelectedTags}>
|
<div className={styles.tagsEditorSelectedTags}>
|
||||||
<InlineTagsList
|
<InlineTagsList pageId={pageId} readonly={readonly}>
|
||||||
options={options}
|
|
||||||
value={value}
|
|
||||||
onChange={onChange}
|
|
||||||
readonly={readonly}
|
|
||||||
>
|
|
||||||
<input
|
<input
|
||||||
value={inputValue}
|
value={inputValue}
|
||||||
onChange={onInputChange}
|
onChange={onInputChange}
|
||||||
@@ -277,7 +266,7 @@ export const TagsEditor = ({
|
|||||||
<Scrollable.Viewport
|
<Scrollable.Viewport
|
||||||
className={styles.tagSelectorTagsScrollContainer}
|
className={styles.tagSelectorTagsScrollContainer}
|
||||||
>
|
>
|
||||||
{filteredOptions.map(tag => {
|
{filteredTags.map(tag => {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={tag.id}
|
key={tag.id}
|
||||||
@@ -291,7 +280,7 @@ export const TagsEditor = ({
|
|||||||
>
|
>
|
||||||
<TagItem maxWidth="100%" tag={tag} mode="inline" />
|
<TagItem maxWidth="100%" tag={tag} mode="inline" />
|
||||||
<div className={styles.spacer} />
|
<div className={styles.spacer} />
|
||||||
<EditTagMenu tag={tag}>
|
<EditTagMenu tagId={tag.id}>
|
||||||
<IconButton
|
<IconButton
|
||||||
className={styles.tagEditIcon}
|
className={styles.tagEditIcon}
|
||||||
type="plain"
|
type="plain"
|
||||||
@@ -311,15 +300,7 @@ export const TagsEditor = ({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{t['Create']()}{' '}
|
{t['Create']()}{' '}
|
||||||
<TagItem
|
<TempTagItem value={inputValue} color={nextColor} />
|
||||||
maxWidth="100%"
|
|
||||||
tag={{
|
|
||||||
id: inputValue,
|
|
||||||
value: inputValue,
|
|
||||||
color: nextColor,
|
|
||||||
}}
|
|
||||||
mode="inline"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Scrollable.Viewport>
|
</Scrollable.Viewport>
|
||||||
@@ -337,15 +318,14 @@ interface TagsInlineEditorProps extends TagsEditorProps {
|
|||||||
|
|
||||||
// this tags value renderer right now only renders the legacy tags for now
|
// this tags value renderer right now only renders the legacy tags for now
|
||||||
export const TagsInlineEditor = ({
|
export const TagsInlineEditor = ({
|
||||||
value,
|
pageId,
|
||||||
onChange,
|
|
||||||
options,
|
|
||||||
onOptionsChange,
|
|
||||||
readonly,
|
readonly,
|
||||||
placeholder,
|
placeholder,
|
||||||
className,
|
className,
|
||||||
}: TagsInlineEditorProps) => {
|
}: TagsInlineEditorProps) => {
|
||||||
const empty = !value || value.length === 0;
|
const tagService = useService(TagService);
|
||||||
|
const tagIds = useLiveData(tagService.tagIdsByPageId(pageId));
|
||||||
|
const empty = !tagIds || tagIds.length === 0;
|
||||||
return (
|
return (
|
||||||
<Menu
|
<Menu
|
||||||
contentOptions={{
|
contentOptions={{
|
||||||
@@ -358,31 +338,14 @@ export const TagsInlineEditor = ({
|
|||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
items={
|
items={<TagsEditor pageId={pageId} readonly={readonly} />}
|
||||||
<TagsEditor
|
|
||||||
value={value}
|
|
||||||
options={options}
|
|
||||||
onChange={onChange}
|
|
||||||
onOptionsChange={onOptionsChange}
|
|
||||||
readonly={readonly}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={clsx(styles.tagsInlineEditor, className)}
|
className={clsx(styles.tagsInlineEditor, className)}
|
||||||
data-empty={empty}
|
data-empty={empty}
|
||||||
data-readonly={readonly}
|
data-readonly={readonly}
|
||||||
>
|
>
|
||||||
{empty ? (
|
{empty ? placeholder : <InlineTagsList pageId={pageId} readonly />}
|
||||||
placeholder
|
|
||||||
) : (
|
|
||||||
<InlineTagsList
|
|
||||||
value={value}
|
|
||||||
onChange={onChange}
|
|
||||||
options={options}
|
|
||||||
readonly
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</Menu>
|
</Menu>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { Button, Divider, Menu, Scrollable } from '@affine/component';
|
import { Button, Divider, Menu, Scrollable } from '@affine/component';
|
||||||
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
|
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
|
||||||
import { useNavigateHelper } from '@affine/core/hooks/use-navigate-helper';
|
import { useNavigateHelper } from '@affine/core/hooks/use-navigate-helper';
|
||||||
import { WorkspaceLegacyProperties } from '@affine/core/modules/workspace';
|
import { type Tag, TagService } from '@affine/core/modules/tag';
|
||||||
import type { Collection, Tag } from '@affine/env/filter';
|
import type { Collection } from '@affine/env/filter';
|
||||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
import {
|
import {
|
||||||
ArrowDownSmallIcon,
|
ArrowDownSmallIcon,
|
||||||
@@ -18,7 +18,6 @@ import { Link } from 'react-router-dom';
|
|||||||
import { CollectionService } from '../../../modules/collection';
|
import { CollectionService } from '../../../modules/collection';
|
||||||
import { createTagFilter } from '../filter/utils';
|
import { createTagFilter } from '../filter/utils';
|
||||||
import { createEmptyCollection } from '../use-collection-manager';
|
import { createEmptyCollection } from '../use-collection-manager';
|
||||||
import { tagColorMap } from '../utils';
|
|
||||||
import type { AllPageListConfig } from '../view/edit-collection/edit-collection';
|
import type { AllPageListConfig } from '../view/edit-collection/edit-collection';
|
||||||
import {
|
import {
|
||||||
useEditCollection,
|
useEditCollection,
|
||||||
@@ -95,8 +94,9 @@ export const TagPageListHeader = ({
|
|||||||
tag: Tag;
|
tag: Tag;
|
||||||
workspaceId: string;
|
workspaceId: string;
|
||||||
}) => {
|
}) => {
|
||||||
const legacyProperties = useService(WorkspaceLegacyProperties);
|
const tagColor = useLiveData(tag.color);
|
||||||
const options = useLiveData(legacyProperties.tagOptions$);
|
const tagTitle = useLiveData(tag.value);
|
||||||
|
|
||||||
const t = useAFFiNEI18N();
|
const t = useAFFiNEI18N();
|
||||||
const { jumpToTags, jumpToCollection } = useNavigateHelper();
|
const { jumpToTags, jumpToCollection } = useNavigateHelper();
|
||||||
const collectionService = useService(CollectionService);
|
const collectionService = useService(CollectionService);
|
||||||
@@ -153,16 +153,16 @@ export const TagPageListHeader = ({
|
|||||||
avoidCollisions: false,
|
avoidCollisions: false,
|
||||||
className: styles.tagsMenu,
|
className: styles.tagsMenu,
|
||||||
}}
|
}}
|
||||||
items={<TagsEditor options={options} onClick={setOpenMenu} />}
|
items={<SwitchTag onClick={setOpenMenu} />}
|
||||||
>
|
>
|
||||||
<div className={styles.tagSticky}>
|
<div className={styles.tagSticky}>
|
||||||
<div
|
<div
|
||||||
className={styles.tagIndicator}
|
className={styles.tagIndicator}
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: tagColorMap(tag.color),
|
backgroundColor: tagColor,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<div className={styles.tagLabel}>{tag.value}</div>
|
<div className={styles.tagLabel}>{tagTitle}</div>
|
||||||
<ArrowDownSmallIcon className={styles.arrowDownSmallIcon} />
|
<ArrowDownSmallIcon className={styles.arrowDownSmallIcon} />
|
||||||
</div>
|
</div>
|
||||||
</Menu>
|
</Menu>
|
||||||
@@ -175,25 +175,21 @@ export const TagPageListHeader = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const filterOption = (option: Tag, inputValue?: string) => {
|
interface SwitchTagProps {
|
||||||
const trimmedValue = inputValue?.trim().toLowerCase() ?? '';
|
|
||||||
const trimmedOptionValue = option.value.trim().toLowerCase();
|
|
||||||
return trimmedOptionValue.includes(trimmedValue);
|
|
||||||
};
|
|
||||||
|
|
||||||
interface TagsEditorProps {
|
|
||||||
options: Tag[];
|
|
||||||
onClick: (open: boolean) => void;
|
onClick: (open: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TagsEditor = ({ options, onClick }: TagsEditorProps) => {
|
export const SwitchTag = ({ onClick }: SwitchTagProps) => {
|
||||||
const t = useAFFiNEI18N();
|
const t = useAFFiNEI18N();
|
||||||
const [inputValue, setInputValue] = useState('');
|
const [inputValue, setInputValue] = useState('');
|
||||||
const filteredOptions = useMemo(
|
const tagService = useService(TagService);
|
||||||
() =>
|
const filteredLiveData = useMemo(() => {
|
||||||
options.filter(o => (inputValue ? filterOption(o, inputValue) : true)),
|
if (inputValue) {
|
||||||
[inputValue, options]
|
return tagService.filterTagsByName(inputValue);
|
||||||
);
|
}
|
||||||
|
return tagService.tags;
|
||||||
|
}, [inputValue, tagService]);
|
||||||
|
const filteredTags = useLiveData(filteredLiveData);
|
||||||
|
|
||||||
const onInputChange = useCallback(
|
const onInputChange = useCallback(
|
||||||
(e: React.ChangeEvent<HTMLInputElement>) => {
|
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
@@ -225,25 +221,10 @@ export const TagsEditor = ({ options, onClick }: TagsEditorProps) => {
|
|||||||
<Scrollable.Viewport
|
<Scrollable.Viewport
|
||||||
className={styles.tagSelectorTagsScrollContainer}
|
className={styles.tagSelectorTagsScrollContainer}
|
||||||
>
|
>
|
||||||
{filteredOptions.map(tag => {
|
{filteredTags.map(tag => {
|
||||||
return (
|
return <TagLink key={tag.id} tag={tag} onClick={handleClick} />;
|
||||||
<Link
|
|
||||||
key={tag.id}
|
|
||||||
className={styles.tagSelectorItem}
|
|
||||||
data-tag-id={tag.id}
|
|
||||||
data-tag-value={tag.value}
|
|
||||||
to={`/tag/${tag.id}`}
|
|
||||||
onClick={handleClick}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className={styles.tagIcon}
|
|
||||||
style={{ background: tag.color }}
|
|
||||||
/>
|
|
||||||
<div className={styles.tagSelectorItemText}>{tag.value}</div>
|
|
||||||
</Link>
|
|
||||||
);
|
|
||||||
})}
|
})}
|
||||||
{filteredOptions.length === 0 ? (
|
{filteredTags.length === 0 ? (
|
||||||
<div className={clsx(styles.tagSelectorItem, 'disable')}>
|
<div className={clsx(styles.tagSelectorItem, 'disable')}>
|
||||||
{t['Find 0 result']()}
|
{t['Find 0 result']()}
|
||||||
</div>
|
</div>
|
||||||
@@ -255,3 +236,21 @@ export const TagsEditor = ({ options, onClick }: TagsEditorProps) => {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const TagLink = ({ tag, onClick }: { tag: Tag; onClick: () => void }) => {
|
||||||
|
const tagColor = useLiveData(tag.color);
|
||||||
|
const tagTitle = useLiveData(tag.value);
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
key={tag.id}
|
||||||
|
className={styles.tagSelectorItem}
|
||||||
|
data-tag-id={tag.id}
|
||||||
|
data-tag-value={tagTitle}
|
||||||
|
to={`/tag/${tag.id}`}
|
||||||
|
onClick={onClick}
|
||||||
|
>
|
||||||
|
<div className={styles.tagIcon} style={{ background: tagColor }} />
|
||||||
|
<div className={styles.tagSelectorItemText}>{tagTitle}</div>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import { Checkbox } from '@affine/component';
|
import { Checkbox } from '@affine/component';
|
||||||
|
import { TagService } from '@affine/core/modules/tag';
|
||||||
import { useDraggable } from '@dnd-kit/core';
|
import { useDraggable } from '@dnd-kit/core';
|
||||||
|
import { useLiveData, useService } from '@toeverything/infra';
|
||||||
import { type PropsWithChildren, useCallback, useMemo } from 'react';
|
import { type PropsWithChildren, useCallback, useMemo } from 'react';
|
||||||
|
|
||||||
import { WorkbenchLink } from '../../../modules/workbench/view/workbench-link';
|
import { WorkbenchLink } from '../../../modules/workbench/view/workbench-link';
|
||||||
@@ -65,7 +67,11 @@ const PageSelectionCell = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const PageTagsCell = ({ tags }: Pick<PageListItemProps, 'tags'>) => {
|
export const PageTagsCell = ({ pageId }: Pick<PageListItemProps, 'pageId'>) => {
|
||||||
|
const tagsService = useService(TagService);
|
||||||
|
const tagsLiveData = tagsService.tagsByPageId(pageId);
|
||||||
|
const tags = useLiveData(tagsLiveData);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div data-testid="page-list-item-tags" className={styles.tagsCell}>
|
<div data-testid="page-list-item-tags" className={styles.tagsCell}>
|
||||||
<PageTags
|
<PageTags
|
||||||
@@ -177,7 +183,7 @@ export const PageListItem = (props: PageListItemProps) => {
|
|||||||
<ListTitleCell title={props.title} preview={props.preview} />
|
<ListTitleCell title={props.title} preview={props.preview} />
|
||||||
</ColWrapper>
|
</ColWrapper>
|
||||||
<ColWrapper flex={4} alignment="end" style={{ overflow: 'visible' }}>
|
<ColWrapper flex={4} alignment="end" style={{ overflow: 'visible' }}>
|
||||||
<PageTagsCell tags={props.tags} />
|
<PageTagsCell pageId={props.pageId} />
|
||||||
</ColWrapper>
|
</ColWrapper>
|
||||||
</ColWrapper>
|
</ColWrapper>
|
||||||
<ColWrapper flex={1} alignment="end" hideInSmallContainer>
|
<ColWrapper flex={1} alignment="end" hideInSmallContainer>
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import { Menu } from '@affine/component';
|
import { Menu } from '@affine/component';
|
||||||
import type { Tag } from '@affine/env/filter';
|
import { type Tag } from '@affine/core/modules/tag';
|
||||||
import { CloseIcon, MoreHorizontalIcon } from '@blocksuite/icons';
|
import { CloseIcon, MoreHorizontalIcon } from '@blocksuite/icons';
|
||||||
|
import { LiveData, useLiveData } from '@toeverything/infra';
|
||||||
import { assignInlineVars } from '@vanilla-extract/dynamic';
|
import { assignInlineVars } from '@vanilla-extract/dynamic';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import { type MouseEventHandler, useCallback, useMemo } from 'react';
|
import { type MouseEventHandler, useCallback, useMemo } from 'react';
|
||||||
|
|
||||||
import { stopPropagation, tagColorMap } from '../utils';
|
import { stopPropagation } from '../utils';
|
||||||
import * as styles from './page-tags.css';
|
import * as styles from './page-tags.css';
|
||||||
|
|
||||||
export interface PageTagsProps {
|
export interface PageTagsProps {
|
||||||
@@ -16,7 +17,7 @@ export interface PageTagsProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface TagItemProps {
|
interface TagItemProps {
|
||||||
tag: Tag;
|
tag?: Tag;
|
||||||
idx?: number;
|
idx?: number;
|
||||||
maxWidth?: number | string;
|
maxWidth?: number | string;
|
||||||
mode: 'inline' | 'list-item';
|
mode: 'inline' | 'list-item';
|
||||||
@@ -24,6 +25,30 @@ interface TagItemProps {
|
|||||||
style?: React.CSSProperties;
|
style?: React.CSSProperties;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const TempTagItem = ({
|
||||||
|
value,
|
||||||
|
color,
|
||||||
|
maxWidth = '100%',
|
||||||
|
}: {
|
||||||
|
value: string;
|
||||||
|
color: string;
|
||||||
|
maxWidth?: number | string;
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div className={styles.tag} title={value}>
|
||||||
|
<div style={{ maxWidth: maxWidth }} className={styles.tagInline}>
|
||||||
|
<div
|
||||||
|
className={styles.tagIndicator}
|
||||||
|
style={{
|
||||||
|
backgroundColor: color,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div className={styles.tagLabel}>{value}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export const TagItem = ({
|
export const TagItem = ({
|
||||||
tag,
|
tag,
|
||||||
idx,
|
idx,
|
||||||
@@ -32,6 +57,8 @@ export const TagItem = ({
|
|||||||
style,
|
style,
|
||||||
maxWidth,
|
maxWidth,
|
||||||
}: TagItemProps) => {
|
}: TagItemProps) => {
|
||||||
|
const value = useLiveData(tag?.value);
|
||||||
|
const color = useLiveData(tag?.color);
|
||||||
const handleRemove: MouseEventHandler = useCallback(
|
const handleRemove: MouseEventHandler = useCallback(
|
||||||
e => {
|
e => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
@@ -44,9 +71,9 @@ export const TagItem = ({
|
|||||||
data-testid="page-tag"
|
data-testid="page-tag"
|
||||||
className={styles.tag}
|
className={styles.tag}
|
||||||
data-idx={idx}
|
data-idx={idx}
|
||||||
data-tag-id={tag.id}
|
data-tag-id={tag?.id}
|
||||||
data-tag-value={tag.value}
|
data-tag-value={value}
|
||||||
title={tag.value}
|
title={value}
|
||||||
style={style}
|
style={style}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
@@ -56,10 +83,10 @@ export const TagItem = ({
|
|||||||
<div
|
<div
|
||||||
className={styles.tagIndicator}
|
className={styles.tagIndicator}
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: tagColorMap(tag.color),
|
backgroundColor: color,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<div className={styles.tagLabel}>{tag.value}</div>
|
<div className={styles.tagLabel}>{value}</div>
|
||||||
{onRemoved ? (
|
{onRemoved ? (
|
||||||
<div
|
<div
|
||||||
data-testid="remove-tag-button"
|
data-testid="remove-tag-button"
|
||||||
@@ -74,6 +101,34 @@ export const TagItem = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const TagItemNormal = ({
|
||||||
|
tags,
|
||||||
|
maxItems,
|
||||||
|
}: {
|
||||||
|
tags: Tag[];
|
||||||
|
maxItems?: number;
|
||||||
|
}) => {
|
||||||
|
const nTags = useMemo(() => {
|
||||||
|
return maxItems ? tags.slice(0, maxItems) : tags;
|
||||||
|
}, [maxItems, tags]);
|
||||||
|
|
||||||
|
const tagsOrderedLiveData = useMemo(() => {
|
||||||
|
return LiveData.computed(get =>
|
||||||
|
[...nTags].sort((a, b) => get(a.value).length - get(b.value).length)
|
||||||
|
);
|
||||||
|
}, [nTags]);
|
||||||
|
|
||||||
|
const tagsOrdered = useLiveData(tagsOrderedLiveData);
|
||||||
|
|
||||||
|
return useMemo(
|
||||||
|
() =>
|
||||||
|
tagsOrdered.map((tag, idx) => (
|
||||||
|
<TagItem key={tag.id} tag={tag} idx={idx} mode="inline" />
|
||||||
|
)),
|
||||||
|
[tagsOrdered]
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export const PageTags = ({
|
export const PageTags = ({
|
||||||
tags,
|
tags,
|
||||||
widthOnHover,
|
widthOnHover,
|
||||||
@@ -97,16 +152,6 @@ export const PageTags = ({
|
|||||||
);
|
);
|
||||||
}, [maxItems, tags]);
|
}, [maxItems, tags]);
|
||||||
|
|
||||||
const tagsNormal = useMemo(() => {
|
|
||||||
const nTags = maxItems ? tags.slice(0, maxItems) : tags;
|
|
||||||
|
|
||||||
// sort tags by length
|
|
||||||
nTags.sort((a, b) => a.value.length - b.value.length);
|
|
||||||
|
|
||||||
return nTags.map((tag, idx) => (
|
|
||||||
<TagItem key={tag.id} tag={tag} idx={idx} mode="inline" />
|
|
||||||
));
|
|
||||||
}, [maxItems, tags]);
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
data-testid="page-tags"
|
data-testid="page-tags"
|
||||||
@@ -123,7 +168,9 @@ export const PageTags = ({
|
|||||||
className={clsx(styles.innerContainer)}
|
className={clsx(styles.innerContainer)}
|
||||||
>
|
>
|
||||||
<div className={styles.innerBackdrop} />
|
<div className={styles.innerBackdrop} />
|
||||||
<div className={styles.tagsScrollContainer}>{tagsNormal}</div>
|
<div className={styles.tagsScrollContainer}>
|
||||||
|
<TagItemNormal tags={tags} maxItems={maxItems} />
|
||||||
|
</div>
|
||||||
{maxItems && tags.length > maxItems ? (
|
{maxItems && tags.length > maxItems ? (
|
||||||
<Menu
|
<Menu
|
||||||
items={tagsInPopover}
|
items={tagsInPopover}
|
||||||
|
|||||||
@@ -2,11 +2,12 @@ import { toast } from '@affine/component';
|
|||||||
import { useBlockSuiteMetaHelper } from '@affine/core/hooks/affine/use-block-suite-meta-helper';
|
import { useBlockSuiteMetaHelper } from '@affine/core/hooks/affine/use-block-suite-meta-helper';
|
||||||
import { useTrashModalHelper } from '@affine/core/hooks/affine/use-trash-modal-helper';
|
import { useTrashModalHelper } from '@affine/core/hooks/affine/use-trash-modal-helper';
|
||||||
import { useBlockSuiteDocMeta } from '@affine/core/hooks/use-block-suite-page-meta';
|
import { useBlockSuiteDocMeta } from '@affine/core/hooks/use-block-suite-page-meta';
|
||||||
|
import type { Tag } from '@affine/core/modules/tag';
|
||||||
import { Workbench } from '@affine/core/modules/workbench';
|
import { Workbench } from '@affine/core/modules/workbench';
|
||||||
import type { Collection, Filter } from '@affine/env/filter';
|
import type { Collection, Filter } from '@affine/env/filter';
|
||||||
import { Trans } from '@affine/i18n';
|
import { Trans } from '@affine/i18n';
|
||||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
import type { DocMeta, Tag } from '@blocksuite/store';
|
import type { DocMeta } from '@blocksuite/store';
|
||||||
import { useService } from '@toeverything/infra';
|
import { useService } from '@toeverything/infra';
|
||||||
import { Workspace } from '@toeverything/infra';
|
import { Workspace } from '@toeverything/infra';
|
||||||
import { useCallback, useMemo, useRef, useState } from 'react';
|
import { useCallback, useMemo, useRef, useState } from 'react';
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ export * from './tags';
|
|||||||
export * from './types';
|
export * from './types';
|
||||||
export * from './use-collection-manager';
|
export * from './use-collection-manager';
|
||||||
export * from './use-filtered-page-metas';
|
export * from './use-filtered-page-metas';
|
||||||
export * from './use-tag-metas';
|
|
||||||
export * from './utils';
|
export * from './utils';
|
||||||
export * from './view';
|
export * from './view';
|
||||||
export * from './virtualized-list';
|
export * from './virtualized-list';
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
import { Button, Input, Menu, toast } from '@affine/component';
|
import { Button, Input, Menu, toast } from '@affine/component';
|
||||||
import { WorkspaceLegacyProperties } from '@affine/core/modules/workspace';
|
import { TagService } from '@affine/core/modules/tag';
|
||||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
import { useLiveData, useService } from '@toeverything/infra';
|
import { useLiveData, useService } from '@toeverything/infra';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import { nanoid } from 'nanoid';
|
|
||||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
|
|
||||||
import { tagColors } from '../../affine/page-properties/common';
|
import { tagColors } from '../../affine/page-properties/common';
|
||||||
@@ -21,7 +20,7 @@ const TagIcon = ({ color, large }: { color: string; large?: boolean }) => (
|
|||||||
|
|
||||||
const randomTagColor = () => {
|
const randomTagColor = () => {
|
||||||
const randomIndex = Math.floor(Math.random() * tagColors.length);
|
const randomIndex = Math.floor(Math.random() * tagColors.length);
|
||||||
return tagColors[randomIndex];
|
return tagColors[randomIndex][1];
|
||||||
};
|
};
|
||||||
|
|
||||||
export const CreateOrEditTag = ({
|
export const CreateOrEditTag = ({
|
||||||
@@ -33,39 +32,44 @@ export const CreateOrEditTag = ({
|
|||||||
onOpenChange: (open: boolean) => void;
|
onOpenChange: (open: boolean) => void;
|
||||||
tagMeta?: TagMeta;
|
tagMeta?: TagMeta;
|
||||||
}) => {
|
}) => {
|
||||||
const legacyProperties = useService(WorkspaceLegacyProperties);
|
const tagService = useService(TagService);
|
||||||
const tagOptions = useLiveData(legacyProperties.tagOptions$);
|
const tagOptions = useLiveData(tagService.tagMetas);
|
||||||
|
const tag = useLiveData(tagService.tagByTagId(tagMeta?.id));
|
||||||
const t = useAFFiNEI18N();
|
const t = useAFFiNEI18N();
|
||||||
const [menuOpen, setMenuOpen] = useState(false);
|
const [menuOpen, setMenuOpen] = useState(false);
|
||||||
const [tagName, setTagName] = useState(tagMeta?.title || '');
|
|
||||||
const [activeTagIcon, setActiveTagIcon] = useState(() => {
|
const [tagName, setTagName] = useState(tagMeta?.title);
|
||||||
return (
|
const handleChangeName = useCallback((value: string) => {
|
||||||
tagColors.find(([_, color]) => color === tagMeta?.color) ||
|
setTagName(value);
|
||||||
randomTagColor()
|
}, []);
|
||||||
);
|
|
||||||
});
|
const [tagIcon, setTagIcon] = useState(tagMeta?.color || randomTagColor());
|
||||||
|
|
||||||
|
const handleChangeIcon = useCallback((value: string) => {
|
||||||
|
setTagIcon(value);
|
||||||
|
}, []);
|
||||||
|
|
||||||
const tags = useMemo(() => {
|
const tags = useMemo(() => {
|
||||||
return tagColors.map(([name, color]) => {
|
return tagColors.map(([_, color]) => {
|
||||||
return {
|
return {
|
||||||
name: name,
|
name: name,
|
||||||
color: color,
|
color: color,
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
setActiveTagIcon([name, color]);
|
handleChangeIcon(color);
|
||||||
setMenuOpen(false);
|
setMenuOpen(false);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}, []);
|
}, [handleChangeIcon]);
|
||||||
|
|
||||||
const items = useMemo(() => {
|
const items = useMemo(() => {
|
||||||
const tagItems = tags.map(item => {
|
const tagItems = tags.map(item => {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={item.name}
|
key={item.color}
|
||||||
onClick={item.onClick}
|
onClick={item.onClick}
|
||||||
className={clsx(styles.tagItem, {
|
className={clsx(styles.tagItem, {
|
||||||
['active']: item.name === activeTagIcon[0],
|
['active']: item.color === tagIcon,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<TagIcon color={item.color} large={true} />
|
<TagIcon color={item.color} large={true} />
|
||||||
@@ -73,52 +77,38 @@ export const CreateOrEditTag = ({
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
return <div className={styles.tagItemsWrapper}>{tagItems}</div>;
|
return <div className={styles.tagItemsWrapper}>{tagItems}</div>;
|
||||||
}, [activeTagIcon, tags]);
|
}, [tagIcon, tags]);
|
||||||
|
|
||||||
const onClose = useCallback(() => {
|
const onClose = useCallback(() => {
|
||||||
if (!tagMeta) {
|
if (!tagMeta) {
|
||||||
setActiveTagIcon(randomTagColor);
|
handleChangeIcon(randomTagColor());
|
||||||
setTagName('');
|
setTagName('');
|
||||||
}
|
}
|
||||||
onOpenChange(false);
|
onOpenChange(false);
|
||||||
}, [onOpenChange, tagMeta]);
|
}, [handleChangeIcon, onOpenChange, tagMeta]);
|
||||||
|
|
||||||
const onConfirm = useCallback(() => {
|
const onConfirm = useCallback(() => {
|
||||||
if (!tagName.trim()) return;
|
if (!tagName?.trim()) return;
|
||||||
if (tagOptions.some(tag => tag.value === tagName.trim()) && !tagMeta) {
|
if (
|
||||||
|
tagOptions.some(
|
||||||
|
tag => tag.title === tagName.trim() && tag.id !== tagMeta?.id
|
||||||
|
)
|
||||||
|
) {
|
||||||
return toast(t['com.affine.tags.create-tag.toast.exist']());
|
return toast(t['com.affine.tags.create-tag.toast.exist']());
|
||||||
}
|
}
|
||||||
if (!tagMeta) {
|
if (!tagMeta) {
|
||||||
const newTag = {
|
tagService.createTag(tagName.trim(), tagIcon);
|
||||||
id: nanoid(),
|
|
||||||
value: tagName.trim(),
|
|
||||||
color: activeTagIcon[1] || tagColors[0][1],
|
|
||||||
};
|
|
||||||
|
|
||||||
legacyProperties.updateTagOptions([...tagOptions, newTag]);
|
|
||||||
toast(t['com.affine.tags.create-tag.toast.success']());
|
toast(t['com.affine.tags.create-tag.toast.success']());
|
||||||
onClose();
|
onClose();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
tag?.rename(tagName.trim());
|
||||||
|
tag?.changeColor(tagIcon);
|
||||||
|
|
||||||
const updatedTag = {
|
|
||||||
id: tagMeta.id,
|
|
||||||
value: tagName.trim(),
|
|
||||||
color: activeTagIcon[1] || tagColors[0][1],
|
|
||||||
};
|
|
||||||
legacyProperties.updateTagOption(tagMeta.id, updatedTag);
|
|
||||||
toast(t['com.affine.tags.edit-tag.toast.success']());
|
toast(t['com.affine.tags.edit-tag.toast.success']());
|
||||||
onClose();
|
onClose();
|
||||||
return;
|
return;
|
||||||
}, [
|
}, [onClose, t, tag, tagIcon, tagMeta, tagName, tagOptions, tagService]);
|
||||||
activeTagIcon,
|
|
||||||
legacyProperties,
|
|
||||||
onClose,
|
|
||||||
t,
|
|
||||||
tagMeta,
|
|
||||||
tagName,
|
|
||||||
tagOptions,
|
|
||||||
]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!open) return;
|
if (!open) return;
|
||||||
@@ -137,6 +127,11 @@ export const CreateOrEditTag = ({
|
|||||||
};
|
};
|
||||||
}, [open, onOpenChange, menuOpen, onClose]);
|
}, [open, onOpenChange, menuOpen, onClose]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setTagName(tagMeta?.title);
|
||||||
|
setTagIcon(tagMeta?.color || randomTagColor());
|
||||||
|
}, [tagMeta?.color, tagMeta?.title]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.createTagWrapper} data-show={open}>
|
<div className={styles.createTagWrapper} data-show={open}>
|
||||||
<Menu
|
<Menu
|
||||||
@@ -147,7 +142,7 @@ export const CreateOrEditTag = ({
|
|||||||
items={items}
|
items={items}
|
||||||
>
|
>
|
||||||
<Button className={styles.menuBtn}>
|
<Button className={styles.menuBtn}>
|
||||||
<TagIcon color={activeTagIcon[1] || ''} />
|
<TagIcon color={tagIcon} />
|
||||||
</Button>
|
</Button>
|
||||||
</Menu>
|
</Menu>
|
||||||
|
|
||||||
@@ -156,7 +151,7 @@ export const CreateOrEditTag = ({
|
|||||||
inputStyle={{ fontSize: 'var(--affine-font-xs)' }}
|
inputStyle={{ fontSize: 'var(--affine-font-xs)' }}
|
||||||
onEnter={onConfirm}
|
onEnter={onConfirm}
|
||||||
value={tagName}
|
value={tagName}
|
||||||
onChange={setTagName}
|
onChange={handleChangeName}
|
||||||
/>
|
/>
|
||||||
<Button className={styles.cancelBtn} onClick={onClose}>
|
<Button className={styles.cancelBtn} onClick={onClose}>
|
||||||
{t['Cancel']()}
|
{t['Cancel']()}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { type PropsWithChildren, useCallback, useMemo } from 'react';
|
|||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
import type { DraggableTitleCellData, TagListItemProps } from '../types';
|
import type { DraggableTitleCellData, TagListItemProps } from '../types';
|
||||||
import { ColWrapper, stopPropagation, tagColorMap } from '../utils';
|
import { ColWrapper, stopPropagation } from '../utils';
|
||||||
import * as styles from './tag-list-item.css';
|
import * as styles from './tag-list-item.css';
|
||||||
|
|
||||||
const TagListTitleCell = ({
|
const TagListTitleCell = ({
|
||||||
@@ -37,7 +37,7 @@ const ListIconCell = ({ color }: Pick<TagListItemProps, 'color'>) => {
|
|||||||
<div
|
<div
|
||||||
className={styles.tagIndicator}
|
className={styles.tagIndicator}
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: tagColorMap(color),
|
backgroundColor: color,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { toast } from '@affine/component';
|
import { toast } from '@affine/component';
|
||||||
|
import type { Tag } from '@affine/core/modules/tag';
|
||||||
import { Trans } from '@affine/i18n';
|
import { Trans } from '@affine/i18n';
|
||||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
import type { Tag } from '@blocksuite/store';
|
|
||||||
import { useService } from '@toeverything/infra';
|
import { useService } from '@toeverything/infra';
|
||||||
import { Workspace } from '@toeverything/infra';
|
import { Workspace } from '@toeverything/infra';
|
||||||
import { useCallback, useMemo, useRef, useState } from 'react';
|
import { useCallback, useMemo, useRef, useState } from 'react';
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ export type ListItem = DocMeta | CollectionMeta | TagMeta;
|
|||||||
|
|
||||||
export interface CollectionMeta extends Collection {
|
export interface CollectionMeta extends Collection {
|
||||||
title: string;
|
title: string;
|
||||||
createDate?: Date;
|
createDate?: Date | number;
|
||||||
updatedDate?: Date;
|
updatedDate?: Date | number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TagMeta = {
|
export type TagMeta = {
|
||||||
@@ -16,8 +16,8 @@ export type TagMeta = {
|
|||||||
title: string;
|
title: string;
|
||||||
color: string;
|
color: string;
|
||||||
pageCount?: number;
|
pageCount?: number;
|
||||||
createDate?: Date;
|
createDate?: Date | number;
|
||||||
updatedDate?: Date;
|
updatedDate?: Date | number;
|
||||||
};
|
};
|
||||||
// TODO: consider reducing the number of props here
|
// TODO: consider reducing the number of props here
|
||||||
// using type instead of interface to make it Record compatible
|
// using type instead of interface to make it Record compatible
|
||||||
@@ -59,8 +59,8 @@ export type TagListItemProps = {
|
|||||||
color: string;
|
color: string;
|
||||||
title: ReactNode; // using ReactNode to allow for rich content rendering
|
title: ReactNode; // using ReactNode to allow for rich content rendering
|
||||||
pageCount?: number;
|
pageCount?: number;
|
||||||
createDate?: Date;
|
createDate?: Date | number;
|
||||||
updatedDate?: Date;
|
updatedDate?: Date | number;
|
||||||
to?: To; // whether or not to render this item as a Link
|
to?: To; // whether or not to render this item as a Link
|
||||||
draggable?: boolean; // whether or not to allow dragging this item
|
draggable?: boolean; // whether or not to allow dragging this item
|
||||||
selectable?: boolean; // show selection checkbox
|
selectable?: boolean; // show selection checkbox
|
||||||
|
|||||||
@@ -1,71 +0,0 @@
|
|||||||
import { WorkspaceLegacyProperties } from '@affine/core/modules/workspace';
|
|
||||||
import type { DocMeta } from '@blocksuite/store';
|
|
||||||
import { useLiveData, useService } from '@toeverything/infra';
|
|
||||||
import { useCallback, useMemo } from 'react';
|
|
||||||
|
|
||||||
interface TagUsageCounts {
|
|
||||||
[key: string]: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useTagMetas(pageMetas: DocMeta[]) {
|
|
||||||
const legacyProperties = useService(WorkspaceLegacyProperties);
|
|
||||||
const tags = useLiveData(legacyProperties.tagOptions$);
|
|
||||||
|
|
||||||
const [tagMetas, tagUsageCounts] = useMemo(() => {
|
|
||||||
const tagUsageCounts: TagUsageCounts = {};
|
|
||||||
tags.forEach(tag => {
|
|
||||||
tagUsageCounts[tag.id] = 0;
|
|
||||||
});
|
|
||||||
|
|
||||||
pageMetas.forEach(page => {
|
|
||||||
if (!page.tags) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
page.tags.forEach(tagId => {
|
|
||||||
if (Object.prototype.hasOwnProperty.call(tagUsageCounts, tagId)) {
|
|
||||||
tagUsageCounts[tagId]++;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const tagsList = tags.map(tag => {
|
|
||||||
return {
|
|
||||||
...tag,
|
|
||||||
title: tag.value,
|
|
||||||
color: tag.color,
|
|
||||||
pageCount: tagUsageCounts[tag.id] || 0,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
return [tagsList, tagUsageCounts];
|
|
||||||
}, [tags, pageMetas]);
|
|
||||||
|
|
||||||
const filterPageMetaByTag = useCallback(
|
|
||||||
(tagId: string) => {
|
|
||||||
return pageMetas.filter(page => {
|
|
||||||
if (!page.tags) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return page.tags.includes(tagId);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
[pageMetas]
|
|
||||||
);
|
|
||||||
|
|
||||||
const deleteTags = useCallback(
|
|
||||||
(tagIds: string[]) => {
|
|
||||||
tagIds.forEach(tagId => {
|
|
||||||
legacyProperties.removeTagOption(tagId);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
[legacyProperties]
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
tags,
|
|
||||||
tagMetas,
|
|
||||||
tagUsageCounts,
|
|
||||||
filterPageMetaByTag,
|
|
||||||
deleteTags,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -161,20 +161,3 @@ export function shallowEqual(objA: any, objB: any) {
|
|||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// hack: map var(--affine-tag-xxx) colors to var(--affine-palette-line-xxx)
|
|
||||||
export const tagColorMap = (color: string) => {
|
|
||||||
const mapping: Record<string, string> = {
|
|
||||||
'var(--affine-tag-red)': 'var(--affine-palette-line-red)',
|
|
||||||
'var(--affine-tag-teal)': 'var(--affine-palette-line-green)',
|
|
||||||
'var(--affine-tag-blue)': 'var(--affine-palette-line-blue)',
|
|
||||||
'var(--affine-tag-yellow)': 'var(--affine-palette-line-yellow)',
|
|
||||||
'var(--affine-tag-pink)': 'var(--affine-palette-line-magenta)',
|
|
||||||
'var(--affine-tag-white)': 'var(--affine-palette-line-grey)',
|
|
||||||
'var(--affine-tag-gray)': 'var(--affine-palette-line-grey)',
|
|
||||||
'var(--affine-tag-orange)': 'var(--affine-palette-line-orange)',
|
|
||||||
'var(--affine-tag-purple)': 'var(--affine-palette-line-purple)',
|
|
||||||
'var(--affine-tag-green)': 'var(--affine-palette-line-green)',
|
|
||||||
};
|
|
||||||
return mapping[color] || color;
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
GlobalCache,
|
GlobalCache,
|
||||||
GlobalState,
|
GlobalState,
|
||||||
|
PageRecordList,
|
||||||
type ServiceCollection,
|
type ServiceCollection,
|
||||||
Workspace,
|
Workspace,
|
||||||
WorkspaceScope,
|
WorkspaceScope,
|
||||||
@@ -13,6 +14,7 @@ import {
|
|||||||
} from './infra-web/storage';
|
} from './infra-web/storage';
|
||||||
import { Navigator } from './navigation';
|
import { Navigator } from './navigation';
|
||||||
import { RightSidebar } from './right-sidebar/entities/right-sidebar';
|
import { RightSidebar } from './right-sidebar/entities/right-sidebar';
|
||||||
|
import { TagService } from './tag';
|
||||||
import { Workbench } from './workbench';
|
import { Workbench } from './workbench';
|
||||||
import {
|
import {
|
||||||
CurrentWorkspaceService,
|
CurrentWorkspaceService,
|
||||||
@@ -29,7 +31,8 @@ export function configureBusinessServices(services: ServiceCollection) {
|
|||||||
.add(RightSidebar)
|
.add(RightSidebar)
|
||||||
.add(WorkspacePropertiesAdapter, [Workspace])
|
.add(WorkspacePropertiesAdapter, [Workspace])
|
||||||
.add(CollectionService, [Workspace])
|
.add(CollectionService, [Workspace])
|
||||||
.add(WorkspaceLegacyProperties, [Workspace]);
|
.add(WorkspaceLegacyProperties, [Workspace])
|
||||||
|
.add(TagService, [WorkspaceLegacyProperties, PageRecordList]);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function configureWebInfraServices(services: ServiceCollection) {
|
export function configureWebInfraServices(services: ServiceCollection) {
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
import type { Tag as TagSchema } from '@affine/env/filter';
|
||||||
|
import { LiveData, type PageRecordList } from '@toeverything/infra';
|
||||||
|
|
||||||
|
import type { WorkspaceLegacyProperties } from '../../workspace';
|
||||||
|
|
||||||
|
export class Tag {
|
||||||
|
constructor(
|
||||||
|
readonly id: string,
|
||||||
|
private readonly properties: WorkspaceLegacyProperties,
|
||||||
|
private readonly pageRecordList: PageRecordList
|
||||||
|
) {}
|
||||||
|
|
||||||
|
private readonly tagOption = this.properties.tagOptions$.map(
|
||||||
|
tags => tags.find(tag => tag.id === this.id) as TagSchema
|
||||||
|
);
|
||||||
|
|
||||||
|
value = this.tagOption.map(tag => tag?.value || '');
|
||||||
|
|
||||||
|
color = this.tagOption.map(tag => tag?.color || '');
|
||||||
|
|
||||||
|
createDate = this.tagOption.map(tag => tag?.createDate || Date.now());
|
||||||
|
|
||||||
|
updateDate = this.tagOption.map(tag => tag?.updateDate || Date.now());
|
||||||
|
|
||||||
|
rename(value: string) {
|
||||||
|
this.properties.updateTagOption(this.id, {
|
||||||
|
id: this.id,
|
||||||
|
value,
|
||||||
|
color: this.color.value,
|
||||||
|
createDate: this.createDate.value,
|
||||||
|
updateDate: Date.now(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
changeColor(color: string) {
|
||||||
|
this.properties.updateTagOption(this.id, {
|
||||||
|
id: this.id,
|
||||||
|
value: this.value.value,
|
||||||
|
color,
|
||||||
|
createDate: this.createDate.value,
|
||||||
|
updateDate: Date.now(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
tag(pageId: string) {
|
||||||
|
const pageRecord = this.pageRecordList.record(pageId).value;
|
||||||
|
if (!pageRecord) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
pageRecord?.setMeta({
|
||||||
|
tags: [...pageRecord.meta.value.tags, this.id],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
untag(pageId: string) {
|
||||||
|
const pageRecord = this.pageRecordList.record(pageId).value;
|
||||||
|
if (!pageRecord) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
pageRecord?.setMeta({
|
||||||
|
tags: pageRecord.meta.value.tags.filter(tagId => tagId !== this.id),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
readonly pageIds = LiveData.computed(get => {
|
||||||
|
const pages = get(this.pageRecordList.records);
|
||||||
|
return pages
|
||||||
|
.filter(page => get(page.meta).tags.includes(this.id))
|
||||||
|
.map(page => page.id);
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
// hack: map var(--affine-tag-xxx) colors to var(--affine-palette-line-xxx)
|
||||||
|
export const tagColorMap = (color: string) => {
|
||||||
|
const mapping: Record<string, string> = {
|
||||||
|
'var(--affine-tag-red)': 'var(--affine-palette-line-red)',
|
||||||
|
'var(--affine-tag-teal)': 'var(--affine-palette-line-green)',
|
||||||
|
'var(--affine-tag-blue)': 'var(--affine-palette-line-blue)',
|
||||||
|
'var(--affine-tag-yellow)': 'var(--affine-palette-line-yellow)',
|
||||||
|
'var(--affine-tag-pink)': 'var(--affine-palette-line-magenta)',
|
||||||
|
'var(--affine-tag-white)': 'var(--affine-palette-line-grey)',
|
||||||
|
'var(--affine-tag-gray)': 'var(--affine-palette-line-grey)',
|
||||||
|
'var(--affine-tag-orange)': 'var(--affine-palette-line-orange)',
|
||||||
|
'var(--affine-tag-purple)': 'var(--affine-palette-line-purple)',
|
||||||
|
'var(--affine-tag-green)': 'var(--affine-palette-line-green)',
|
||||||
|
};
|
||||||
|
return mapping[color] || color;
|
||||||
|
};
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export { Tag } from './entities/tag';
|
||||||
|
export { tagColorMap } from './entities/utils';
|
||||||
|
export { TagService } from './service/tag';
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
import { LiveData, type PageRecordList } from '@toeverything/infra';
|
||||||
|
import { nanoid } from 'nanoid';
|
||||||
|
|
||||||
|
import type { WorkspaceLegacyProperties } from '../../workspace';
|
||||||
|
import { Tag } from '../entities/tag';
|
||||||
|
|
||||||
|
export class TagService {
|
||||||
|
constructor(
|
||||||
|
private readonly properties: WorkspaceLegacyProperties,
|
||||||
|
private readonly pageRecordList: PageRecordList
|
||||||
|
) {}
|
||||||
|
|
||||||
|
readonly tags = this.properties.tagOptions$.map(tags =>
|
||||||
|
tags.map(tag => new Tag(tag.id, this.properties, this.pageRecordList))
|
||||||
|
);
|
||||||
|
|
||||||
|
createTag(value: string, color: string) {
|
||||||
|
const newId = nanoid();
|
||||||
|
this.properties.updateTagOptions([
|
||||||
|
...this.properties.tagOptions$.value,
|
||||||
|
{
|
||||||
|
id: newId,
|
||||||
|
value,
|
||||||
|
color,
|
||||||
|
createDate: Date.now(),
|
||||||
|
updateDate: Date.now(),
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
const newTag = new Tag(newId, this.properties, this.pageRecordList);
|
||||||
|
return newTag;
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteTag(tagId: string) {
|
||||||
|
this.properties.removeTagOption(tagId);
|
||||||
|
}
|
||||||
|
|
||||||
|
tagsByPageId(pageId: string) {
|
||||||
|
return LiveData.computed(get => {
|
||||||
|
const pageRecord = get(this.pageRecordList.record(pageId));
|
||||||
|
if (!pageRecord) return [];
|
||||||
|
const tagIds = get(pageRecord.meta).tags;
|
||||||
|
|
||||||
|
return get(this.tags).filter(tag => tagIds.includes(tag.id));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
tagIdsByPageId(pageId: string) {
|
||||||
|
return this.tagsByPageId(pageId).map(tags => tags.map(tag => tag.id));
|
||||||
|
}
|
||||||
|
|
||||||
|
tagByTagId(tagId?: string) {
|
||||||
|
return this.tags.map(tags => tags.find(tag => tag.id === tagId));
|
||||||
|
}
|
||||||
|
|
||||||
|
tagMetas = LiveData.computed(get => {
|
||||||
|
return get(this.tags).map(tag => {
|
||||||
|
return {
|
||||||
|
id: tag.id,
|
||||||
|
title: get(tag.value),
|
||||||
|
color: get(tag.color),
|
||||||
|
pageCount: get(tag.pageIds).length,
|
||||||
|
createDate: get(tag.createDate),
|
||||||
|
updatedDate: get(tag.updateDate),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
private filterFn(value: string, query?: string) {
|
||||||
|
const trimmedQuery = query?.trim().toLowerCase() ?? '';
|
||||||
|
const trimmedValue = value.trim().toLowerCase();
|
||||||
|
return trimmedValue.includes(trimmedQuery);
|
||||||
|
}
|
||||||
|
|
||||||
|
filterTagsByName(name: string) {
|
||||||
|
return LiveData.computed(get => {
|
||||||
|
return get(this.tags).filter(tag => this.filterFn(get(tag.value), name));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
tagByTagValue(value: string) {
|
||||||
|
return LiveData.computed(get => {
|
||||||
|
return get(this.tags).find(tag => this.filterFn(get(tag.value), value));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { DocsPropertiesMeta, Tag } from '@blocksuite/store';
|
import type { Tag } from '@affine/env/filter';
|
||||||
|
import type { DocsPropertiesMeta } from '@blocksuite/store';
|
||||||
import { LiveData } from '@toeverything/infra/livedata';
|
import { LiveData } from '@toeverything/infra/livedata';
|
||||||
import type { Workspace } from '@toeverything/infra/workspace';
|
import type { Workspace } from '@toeverything/infra/workspace';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
import { useTagMetas } from '@affine/core/components/page-list';
|
|
||||||
import {
|
import {
|
||||||
TagListHeader,
|
TagListHeader,
|
||||||
VirtualizedTagList,
|
VirtualizedTagList,
|
||||||
} from '@affine/core/components/page-list/tags';
|
} from '@affine/core/components/page-list/tags';
|
||||||
import { CreateOrEditTag } from '@affine/core/components/page-list/tags/create-tag';
|
import { CreateOrEditTag } from '@affine/core/components/page-list/tags/create-tag';
|
||||||
import { useBlockSuiteDocMeta } from '@affine/core/hooks/use-block-suite-page-meta';
|
import type { TagMeta } from '@affine/core/components/page-list/types';
|
||||||
import { useService } from '@toeverything/infra';
|
import { TagService } from '@affine/core/modules/tag';
|
||||||
import { Workspace } from '@toeverything/infra';
|
import { useLiveData, useService } from '@toeverything/infra';
|
||||||
import { useCallback, useState } from 'react';
|
import { useCallback, useState } from 'react';
|
||||||
|
|
||||||
import { ViewBodyIsland, ViewHeaderIsland } from '../../../modules/workbench';
|
import { ViewBodyIsland, ViewHeaderIsland } from '../../../modules/workbench';
|
||||||
@@ -32,10 +31,19 @@ const EmptyTagListHeader = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const AllTag = () => {
|
export const AllTag = () => {
|
||||||
const currentWorkspace = useService(Workspace);
|
const tagService = useService(TagService);
|
||||||
const pageMetas = useBlockSuiteDocMeta(currentWorkspace.docCollection);
|
const tags = useLiveData(tagService.tags);
|
||||||
|
|
||||||
const { tags, tagMetas, deleteTags } = useTagMetas(pageMetas);
|
const tagMetas: TagMeta[] = useLiveData(tagService.tagMetas);
|
||||||
|
|
||||||
|
const handleDelete = useCallback(
|
||||||
|
(tagIds: string[]) => {
|
||||||
|
tagIds.forEach(tagId => {
|
||||||
|
tagService.deleteTag(tagId);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[tagService]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -48,7 +56,7 @@ export const AllTag = () => {
|
|||||||
<VirtualizedTagList
|
<VirtualizedTagList
|
||||||
tags={tags}
|
tags={tags}
|
||||||
tagMetas={tagMetas}
|
tagMetas={tagMetas}
|
||||||
onTagDelete={deleteTags}
|
onTagDelete={handleDelete}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<EmptyTagList heading={<EmptyTagListHeader />} />
|
<EmptyTagList heading={<EmptyTagListHeader />} />
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
import {
|
import {
|
||||||
TagPageListHeader,
|
TagPageListHeader,
|
||||||
useTagMetas,
|
|
||||||
VirtualizedPageList,
|
VirtualizedPageList,
|
||||||
} from '@affine/core/components/page-list';
|
} from '@affine/core/components/page-list';
|
||||||
import { useBlockSuiteDocMeta } from '@affine/core/hooks/use-block-suite-page-meta';
|
import { useBlockSuiteDocMeta } from '@affine/core/hooks/use-block-suite-page-meta';
|
||||||
|
import { TagService } from '@affine/core/modules/tag';
|
||||||
import {
|
import {
|
||||||
ViewBodyIsland,
|
ViewBodyIsland,
|
||||||
ViewHeaderIsland,
|
ViewHeaderIsland,
|
||||||
} from '@affine/core/modules/workbench';
|
} from '@affine/core/modules/workbench';
|
||||||
import { useService } from '@toeverything/infra';
|
import { LiveData, useLiveData, useService } from '@toeverything/infra';
|
||||||
import { Workspace } from '@toeverything/infra';
|
import { Workspace } from '@toeverything/infra';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
@@ -22,18 +22,27 @@ export const TagDetail = ({ tagId }: { tagId?: string }) => {
|
|||||||
const currentWorkspace = useService(Workspace);
|
const currentWorkspace = useService(Workspace);
|
||||||
const pageMetas = useBlockSuiteDocMeta(currentWorkspace.docCollection);
|
const pageMetas = useBlockSuiteDocMeta(currentWorkspace.docCollection);
|
||||||
|
|
||||||
const { tags, filterPageMetaByTag } = useTagMetas(pageMetas);
|
const tagService = useService(TagService);
|
||||||
const tagPageMetas = useMemo(() => {
|
const currentTagLiveData = tagService.tagByTagId(tagId);
|
||||||
if (tagId) {
|
const currentTag = useLiveData(currentTagLiveData);
|
||||||
return filterPageMetaByTag(tagId);
|
|
||||||
}
|
|
||||||
return [];
|
|
||||||
}, [filterPageMetaByTag, tagId]);
|
|
||||||
|
|
||||||
const currentTag = useMemo(
|
const pageIdsLiveData = useMemo(
|
||||||
() => tags.find(tag => tag.id === tagId),
|
() =>
|
||||||
[tagId, tags]
|
LiveData.computed(get => {
|
||||||
|
const liveTag = get(currentTagLiveData);
|
||||||
|
if (liveTag?.pageIds) {
|
||||||
|
return get(liveTag.pageIds);
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}),
|
||||||
|
[currentTagLiveData]
|
||||||
);
|
);
|
||||||
|
const pageIds = useLiveData(pageIdsLiveData);
|
||||||
|
|
||||||
|
const filteredPageMetas = useMemo(() => {
|
||||||
|
const pageIdsSet = new Set(pageIds);
|
||||||
|
return pageMetas.filter(page => pageIdsSet.has(page.id));
|
||||||
|
}, [pageIds, pageMetas]);
|
||||||
|
|
||||||
if (!currentTag) {
|
if (!currentTag) {
|
||||||
return <PageNotFound />;
|
return <PageNotFound />;
|
||||||
@@ -46,8 +55,11 @@ export const TagDetail = ({ tagId }: { tagId?: string }) => {
|
|||||||
</ViewHeaderIsland>
|
</ViewHeaderIsland>
|
||||||
<ViewBodyIsland>
|
<ViewBodyIsland>
|
||||||
<div className={styles.body}>
|
<div className={styles.body}>
|
||||||
{tagPageMetas.length > 0 ? (
|
{filteredPageMetas.length > 0 ? (
|
||||||
<VirtualizedPageList tag={currentTag} listItem={tagPageMetas} />
|
<VirtualizedPageList
|
||||||
|
tag={currentTag}
|
||||||
|
listItem={filteredPageMetas}
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
<EmptyPageList
|
<EmptyPageList
|
||||||
type="all"
|
type="all"
|
||||||
|
|||||||
@@ -201,7 +201,8 @@ export const ListItemTags: StoryFn<PageTagsProps> = props => (
|
|||||||
);
|
);
|
||||||
|
|
||||||
ListItemTags.args = {
|
ListItemTags.args = {
|
||||||
tags: testTags,
|
// FIXME: this is a hack to make the storybook work
|
||||||
|
// tags: testTags,
|
||||||
hoverExpandDirection: 'left',
|
hoverExpandDirection: 'left',
|
||||||
widthOnHover: 600,
|
widthOnHover: 600,
|
||||||
maxItems: 5,
|
maxItems: 5,
|
||||||
|
|||||||
Reference in New Issue
Block a user