feat(core): readwise integration tags setting (#10946)

close AF-2307, AF-2262
This commit is contained in:
CatsJuice
2025-03-20 23:20:58 +00:00
parent f1c8a88a7c
commit e37328c83b
9 changed files with 185 additions and 8 deletions

View File

@@ -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={<TagsEditor {...props} />}
>
<div

View File

@@ -79,3 +79,28 @@ export const updateStrategyGroup = style({
export const updateStrategyGroupContent = style({
overflow: 'hidden',
});
export const tagsLabel = style({
fontSize: 14,
fontWeight: 500,
lineHeight: '22px',
color: cssVarV2.text.primary,
marginBottom: 2,
});
export const tagsEditor = style({
padding: '6px 8px',
borderRadius: 4,
border: `1px solid ${cssVarV2.layer.insideBorder.border}`,
fontSize: 14,
});
export const tagsPlaceholder = style({
fontSize: 14,
lineHeight: '22px',
color: cssVarV2.text.placeholder,
});
export const tagsMenu = style({
left: -1,
top: 'calc(-1px + var(--radix-popper-anchor-height) * -1)',
width: 'calc(2px + var(--radix-popper-anchor-width))',
});

View File

@@ -1,11 +1,13 @@
import { Button, Modal } from '@affine/component';
import { type TagLike, TagsInlineEditor } from '@affine/core/components/tags';
import {
IntegrationService,
IntegrationTypeIcon,
} from '@affine/core/modules/integration';
import type { ReadwiseConfig } from '@affine/core/modules/integration/type';
import { TagService } from '@affine/core/modules/tag';
import { useI18n } from '@affine/i18n';
import { useLiveData, useService } from '@toeverything/infra';
import { LiveData, useLiveData, useService } from '@toeverything/infra';
import { useCallback, useMemo } from 'react';
import { IntegrationCardIcon } from '../card';
@@ -45,6 +47,8 @@ export const SettingDialog = ({
</div>
</header>
<ul className={styles.settings}>
<TagsSetting />
<Divider />
<NewHighlightSetting />
<Divider />
<UpdateStrategySetting />
@@ -174,3 +178,105 @@ const StartImport = ({ onImport }: { onImport: () => void }) => {
</IntegrationSettingItem>
);
};
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 (
<li>
<h6 className={styles.tagsLabel}>
{t['com.affine.integration.readwise.setting.tags-label']()}
</h6>
<TagsInlineEditor
placeholder={
<span className={styles.tagsPlaceholder}>
{t['com.affine.integration.readwise.setting.tags-placeholder']()}
</span>
}
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}
/>
</li>
);
};

View File

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

View File

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

View File

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

View File

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

View File

@@ -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`
*/

View File

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