From e37328c83b19b6a70cc724f12d3a1a98bb67371c Mon Sep 17 00:00:00 2001 From: CatsJuice Date: Thu, 20 Mar 2025 23:20:58 +0000 Subject: [PATCH] feat(core): readwise integration tags setting (#10946) close AF-2307, AF-2262 --- .../core/src/components/tags/tags-editor.tsx | 11 +- .../readwise/setting-dialog.css.ts | 25 ++++ .../integration/readwise/setting-dialog.tsx | 108 +++++++++++++++++- .../modules/integration/entities/readwise.ts | 6 +- .../modules/integration/entities/writer.ts | 26 ++++- .../core/src/modules/integration/index.ts | 3 +- .../core/src/modules/integration/type.ts | 4 + packages/frontend/i18n/src/i18n.gen.ts | 8 ++ packages/frontend/i18n/src/resources/en.json | 2 + 9 files changed, 185 insertions(+), 8 deletions(-) diff --git a/packages/frontend/core/src/components/tags/tags-editor.tsx b/packages/frontend/core/src/components/tags/tags-editor.tsx index 6bfe4e2d38..e7880f447d 100644 --- a/packages/frontend/core/src/components/tags/tags-editor.tsx +++ b/packages/frontend/core/src/components/tags/tags-editor.tsx @@ -27,10 +27,12 @@ export interface TagsEditorProps { } export interface TagsInlineEditorProps extends TagsEditorProps { - placeholder?: string; + placeholder?: ReactNode; className?: string; readonly?: boolean; title?: ReactNode; // only used for mobile + modalMenu?: boolean; + menuClassName?: string; } type TagOption = TagLike | { readonly create: true; readonly value: string }; @@ -364,6 +366,8 @@ const DesktopTagsInlineEditor = ({ readonly, placeholder, className, + modalMenu, + menuClassName, ...props }: TagsInlineEditorProps) => { const empty = !props.selectedTags || props.selectedTags.length === 0; @@ -379,11 +383,14 @@ const DesktopTagsInlineEditor = ({ align: 'start', sideOffset: 0, avoidCollisions: false, - className: styles.tagsMenu, + className: clsx(styles.tagsMenu, menuClassName), onClick(e) { e.stopPropagation(); }, }} + rootOptions={{ + modal: modalMenu, + }} items={} >
    + + @@ -174,3 +178,105 @@ const StartImport = ({ onImport }: { onImport: () => void }) => { ); }; + +const TagsSetting = () => { + const t = useI18n(); + const tagService = useService(TagService); + const readwise = useService(IntegrationService).readwise; + const allTags = useLiveData(tagService.tagList.tags$); + const tagColors = tagService.tagColors; + const tagIds = useLiveData( + useMemo(() => readwise.setting$('tags'), [readwise]) + ); + const adaptedTags = useLiveData( + useMemo(() => { + return LiveData.computed(get => { + return allTags.map(tag => ({ + id: tag.id, + value: get(tag.value$), + color: get(tag.color$), + })); + }); + }, [allTags]) + ); + const adaptedTagColors = useMemo(() => { + return tagColors.map(color => ({ + id: color[0], + value: color[1], + name: color[0], + })); + }, [tagColors]); + + const updateReadwiseTags = useCallback( + (tagIds: string[]) => { + readwise.updateSetting( + 'tags', + tagIds.filter(id => !!allTags.some(tag => tag.id === id)) + ); + }, + [allTags, readwise] + ); + + const onCreateTag = useCallback( + (name: string, color: string) => { + const tag = tagService.tagList.createTag(name, color); + return { id: tag.id, value: tag.value$.value, color: tag.color$.value }; + }, + [tagService.tagList] + ); + const onSelectTag = useCallback( + (tagId: string) => { + updateReadwiseTags([...(tagIds ?? []), tagId]); + }, + [tagIds, updateReadwiseTags] + ); + const onDeselectTag = useCallback( + (tagId: string) => { + updateReadwiseTags(tagIds?.filter(id => id !== tagId) ?? []); + }, + [tagIds, updateReadwiseTags] + ); + const onDeleteTag = useCallback( + (tagId: string) => { + tagService.tagList.deleteTag(tagId); + updateReadwiseTags(tagIds ?? []); + }, + [tagIds, updateReadwiseTags, tagService.tagList] + ); + const onTagChange = useCallback( + (id: string, property: keyof TagLike, value: string) => { + if (property === 'value') { + tagService.tagList.tagByTagId$(id).value?.rename(value); + } else if (property === 'color') { + tagService.tagList.tagByTagId$(id).value?.changeColor(value); + } + }, + [tagService.tagList] + ); + return ( +
  • +
    + {t['com.affine.integration.readwise.setting.tags-label']()} +
    + + {t['com.affine.integration.readwise.setting.tags-placeholder']()} + + } + className={styles.tagsEditor} + tagMode="inline-tag" + tags={adaptedTags} + selectedTags={tagIds ?? []} + onCreateTag={onCreateTag} + onSelectTag={onSelectTag} + onDeselectTag={onDeselectTag} + tagColors={adaptedTagColors} + onTagChange={onTagChange} + onDeleteTag={onDeleteTag} + modalMenu={true} + menuClassName={styles.tagsMenu} + /> +
  • + ); +}; diff --git a/packages/frontend/core/src/modules/integration/entities/readwise.ts b/packages/frontend/core/src/modules/integration/entities/readwise.ts index ca262587e4..b60a2693a2 100644 --- a/packages/frontend/core/src/modules/integration/entities/readwise.ts +++ b/packages/frontend/core/src/modules/integration/entities/readwise.ts @@ -86,6 +86,7 @@ export class ReadwiseIntegration extends Entity<{ writer: IntegrationWriter }> { const updateStrategy = this.readwiseStore.getSetting('updateStrategy'); const syncNewHighlights = this.readwiseStore.getSetting('syncNewHighlights'); + const tags = this.readwiseStore.getSetting('tags'); const chunks = chunk(highlights, 2); const total = highlights.length; let finished = 0; @@ -120,6 +121,7 @@ export class ReadwiseIntegration extends Entity<{ writer: IntegrationWriter }> { updateStrategy, integrationId, userId, + tags, }); } finished++; @@ -144,15 +146,17 @@ export class ReadwiseIntegration extends Entity<{ writer: IntegrationWriter }> { integrationId: string; userId: string; updateStrategy?: ReadwiseConfig['updateStrategy']; + tags?: string[]; } ) { - const { updateStrategy, integrationId } = options; + const { updateStrategy, integrationId, tags } = options; const { text, ...highlightWithoutText } = highlight; const writtenDocId = await this.writer.writeDoc({ content: text, title: book.title, docId, + tags, comment: highlight.note, updateStrategy: updateStrategy ?? 'append', }); diff --git a/packages/frontend/core/src/modules/integration/entities/writer.ts b/packages/frontend/core/src/modules/integration/entities/writer.ts index 3ad501a7bc..5e89c62ca0 100644 --- a/packages/frontend/core/src/modules/integration/entities/writer.ts +++ b/packages/frontend/core/src/modules/integration/entities/writer.ts @@ -1,13 +1,17 @@ import { MarkdownTransformer } from '@blocksuite/affine/blocks/root'; import { Entity } from '@toeverything/infra'; +import type { TagService } from '../../tag'; import { getAFFiNEWorkspaceSchema, type WorkspaceService, } from '../../workspace'; export class IntegrationWriter extends Entity { - constructor(private readonly workspaceService: WorkspaceService) { + constructor( + private readonly workspaceService: WorkspaceService, + private readonly tagService: TagService + ) { super(); } @@ -32,18 +36,24 @@ export class IntegrationWriter extends Entity { * Update strategy, default is `override` */ updateStrategy?: 'override' | 'append'; + /** + * Tags to apply to the doc + */ + tags?: string[]; }) { const { title, content, comment, docId, + tags, updateStrategy = 'override', } = options; const workspace = this.workspaceService.workspace; let markdown = comment ? `${content}\n---\n${comment}` : content; + let finalDocId: string; if (!docId) { const newDocId = await MarkdownTransformer.importMarkdownToDoc({ collection: workspace.docCollection, @@ -52,7 +62,8 @@ export class IntegrationWriter extends Entity { fileName: title, }); - return newDocId; + if (!newDocId) throw new Error('Failed to create a new doc'); + finalDocId = newDocId; } else { const collection = workspace.docCollection; @@ -88,7 +99,16 @@ export class IntegrationWriter extends Entity { } else { throw new Error('Invalid update strategy'); } - return doc.id; + finalDocId = doc.id; } + await this.applyTags(finalDocId, tags); + return finalDocId; + } + + public async applyTags(docId: string, tags?: string[]) { + if (!tags?.length) return; + tags.forEach(tag => { + this.tagService.tagList.tagByTagId$(tag).value?.tag(docId); + }); } } diff --git a/packages/frontend/core/src/modules/integration/index.ts b/packages/frontend/core/src/modules/integration/index.ts index 82106f5749..1d74466920 100644 --- a/packages/frontend/core/src/modules/integration/index.ts +++ b/packages/frontend/core/src/modules/integration/index.ts @@ -4,6 +4,7 @@ import { WorkspaceServerService } from '../cloud'; import { WorkspaceDBService } from '../db'; import { DocScope, DocService, DocsService } from '../doc'; import { GlobalState } from '../storage'; +import { TagService } from '../tag'; import { WorkspaceScope, WorkspaceService } from '../workspace'; import { ReadwiseIntegration } from './entities/readwise'; import { ReadwiseCrawler } from './entities/readwise-crawler'; @@ -27,8 +28,8 @@ export function configureIntegrationModule(framework: Framework) { WorkspaceServerService, ]) .service(IntegrationService) - .entity(IntegrationWriter, [WorkspaceService]) .entity(ReadwiseCrawler, [ReadwiseStore]) + .entity(IntegrationWriter, [WorkspaceService, TagService]) .entity(ReadwiseIntegration, [ IntegrationRefStore, ReadwiseStore, diff --git a/packages/frontend/core/src/modules/integration/type.ts b/packages/frontend/core/src/modules/integration/type.ts index 38e95fdc57..0ef9c85fa9 100644 --- a/packages/frontend/core/src/modules/integration/type.ts +++ b/packages/frontend/core/src/modules/integration/type.ts @@ -87,6 +87,10 @@ export interface ReadwiseConfig { * The update strategy */ updateStrategy?: 'override' | 'append'; + /** + * Tag id list to be used when creating highlights + */ + tags?: string[]; } // =============================== // Zotero diff --git a/packages/frontend/i18n/src/i18n.gen.ts b/packages/frontend/i18n/src/i18n.gen.ts index acdd7153d8..301b0bdb01 100644 --- a/packages/frontend/i18n/src/i18n.gen.ts +++ b/packages/frontend/i18n/src/i18n.gen.ts @@ -7365,6 +7365,14 @@ export function useAFFiNEI18N(): { * `Import` */ ["com.affine.integration.readwise.setting.start-import-button"](): string; + /** + * `Apply tags to highlight imports` + */ + ["com.affine.integration.readwise.setting.tags-label"](): string; + /** + * `Click to add tags` + */ + ["com.affine.integration.readwise.setting.tags-placeholder"](): string; /** * `Author` */ diff --git a/packages/frontend/i18n/src/resources/en.json b/packages/frontend/i18n/src/resources/en.json index af11c0188b..4c1620bcea 100644 --- a/packages/frontend/i18n/src/resources/en.json +++ b/packages/frontend/i18n/src/resources/en.json @@ -1834,6 +1834,8 @@ "com.affine.integration.readwise.setting.start-import-name": "Start Importing", "com.affine.integration.readwise.setting.start-import-desc": "Using the settings above", "com.affine.integration.readwise.setting.start-import-button": "Import", + "com.affine.integration.readwise.setting.tags-label": "Apply tags to highlight imports", + "com.affine.integration.readwise.setting.tags-placeholder": "Click to add tags", "com.affine.integration.readwise-prop.author": "Author", "com.affine.integration.readwise-prop.source": "Source", "com.affine.integration.readwise-prop.created": "Created",