feat(core): readwise integration tags setting (#10946)
close AF-2307, AF-2262
This commit is contained in:
@@ -27,10 +27,12 @@ export interface TagsEditorProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface TagsInlineEditorProps extends TagsEditorProps {
|
export interface TagsInlineEditorProps extends TagsEditorProps {
|
||||||
placeholder?: string;
|
placeholder?: ReactNode;
|
||||||
className?: string;
|
className?: string;
|
||||||
readonly?: boolean;
|
readonly?: boolean;
|
||||||
title?: ReactNode; // only used for mobile
|
title?: ReactNode; // only used for mobile
|
||||||
|
modalMenu?: boolean;
|
||||||
|
menuClassName?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
type TagOption = TagLike | { readonly create: true; readonly value: string };
|
type TagOption = TagLike | { readonly create: true; readonly value: string };
|
||||||
@@ -364,6 +366,8 @@ const DesktopTagsInlineEditor = ({
|
|||||||
readonly,
|
readonly,
|
||||||
placeholder,
|
placeholder,
|
||||||
className,
|
className,
|
||||||
|
modalMenu,
|
||||||
|
menuClassName,
|
||||||
...props
|
...props
|
||||||
}: TagsInlineEditorProps) => {
|
}: TagsInlineEditorProps) => {
|
||||||
const empty = !props.selectedTags || props.selectedTags.length === 0;
|
const empty = !props.selectedTags || props.selectedTags.length === 0;
|
||||||
@@ -379,11 +383,14 @@ const DesktopTagsInlineEditor = ({
|
|||||||
align: 'start',
|
align: 'start',
|
||||||
sideOffset: 0,
|
sideOffset: 0,
|
||||||
avoidCollisions: false,
|
avoidCollisions: false,
|
||||||
className: styles.tagsMenu,
|
className: clsx(styles.tagsMenu, menuClassName),
|
||||||
onClick(e) {
|
onClick(e) {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
|
rootOptions={{
|
||||||
|
modal: modalMenu,
|
||||||
|
}}
|
||||||
items={<TagsEditor {...props} />}
|
items={<TagsEditor {...props} />}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -79,3 +79,28 @@ export const updateStrategyGroup = style({
|
|||||||
export const updateStrategyGroupContent = style({
|
export const updateStrategyGroupContent = style({
|
||||||
overflow: 'hidden',
|
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))',
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
import { Button, Modal } from '@affine/component';
|
import { Button, Modal } from '@affine/component';
|
||||||
|
import { type TagLike, TagsInlineEditor } from '@affine/core/components/tags';
|
||||||
import {
|
import {
|
||||||
IntegrationService,
|
IntegrationService,
|
||||||
IntegrationTypeIcon,
|
IntegrationTypeIcon,
|
||||||
} from '@affine/core/modules/integration';
|
} from '@affine/core/modules/integration';
|
||||||
import type { ReadwiseConfig } from '@affine/core/modules/integration/type';
|
import type { ReadwiseConfig } from '@affine/core/modules/integration/type';
|
||||||
|
import { TagService } from '@affine/core/modules/tag';
|
||||||
import { useI18n } from '@affine/i18n';
|
import { useI18n } from '@affine/i18n';
|
||||||
import { useLiveData, useService } from '@toeverything/infra';
|
import { LiveData, useLiveData, useService } from '@toeverything/infra';
|
||||||
import { useCallback, useMemo } from 'react';
|
import { useCallback, useMemo } from 'react';
|
||||||
|
|
||||||
import { IntegrationCardIcon } from '../card';
|
import { IntegrationCardIcon } from '../card';
|
||||||
@@ -45,6 +47,8 @@ export const SettingDialog = ({
|
|||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<ul className={styles.settings}>
|
<ul className={styles.settings}>
|
||||||
|
<TagsSetting />
|
||||||
|
<Divider />
|
||||||
<NewHighlightSetting />
|
<NewHighlightSetting />
|
||||||
<Divider />
|
<Divider />
|
||||||
<UpdateStrategySetting />
|
<UpdateStrategySetting />
|
||||||
@@ -174,3 +178,105 @@ const StartImport = ({ onImport }: { onImport: () => void }) => {
|
|||||||
</IntegrationSettingItem>
|
</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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
@@ -86,6 +86,7 @@ export class ReadwiseIntegration extends Entity<{ writer: IntegrationWriter }> {
|
|||||||
const updateStrategy = this.readwiseStore.getSetting('updateStrategy');
|
const updateStrategy = this.readwiseStore.getSetting('updateStrategy');
|
||||||
const syncNewHighlights =
|
const syncNewHighlights =
|
||||||
this.readwiseStore.getSetting('syncNewHighlights');
|
this.readwiseStore.getSetting('syncNewHighlights');
|
||||||
|
const tags = this.readwiseStore.getSetting('tags');
|
||||||
const chunks = chunk(highlights, 2);
|
const chunks = chunk(highlights, 2);
|
||||||
const total = highlights.length;
|
const total = highlights.length;
|
||||||
let finished = 0;
|
let finished = 0;
|
||||||
@@ -120,6 +121,7 @@ export class ReadwiseIntegration extends Entity<{ writer: IntegrationWriter }> {
|
|||||||
updateStrategy,
|
updateStrategy,
|
||||||
integrationId,
|
integrationId,
|
||||||
userId,
|
userId,
|
||||||
|
tags,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
finished++;
|
finished++;
|
||||||
@@ -144,15 +146,17 @@ export class ReadwiseIntegration extends Entity<{ writer: IntegrationWriter }> {
|
|||||||
integrationId: string;
|
integrationId: string;
|
||||||
userId: string;
|
userId: string;
|
||||||
updateStrategy?: ReadwiseConfig['updateStrategy'];
|
updateStrategy?: ReadwiseConfig['updateStrategy'];
|
||||||
|
tags?: string[];
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
const { updateStrategy, integrationId } = options;
|
const { updateStrategy, integrationId, tags } = options;
|
||||||
const { text, ...highlightWithoutText } = highlight;
|
const { text, ...highlightWithoutText } = highlight;
|
||||||
|
|
||||||
const writtenDocId = await this.writer.writeDoc({
|
const writtenDocId = await this.writer.writeDoc({
|
||||||
content: text,
|
content: text,
|
||||||
title: book.title,
|
title: book.title,
|
||||||
docId,
|
docId,
|
||||||
|
tags,
|
||||||
comment: highlight.note,
|
comment: highlight.note,
|
||||||
updateStrategy: updateStrategy ?? 'append',
|
updateStrategy: updateStrategy ?? 'append',
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,13 +1,17 @@
|
|||||||
import { MarkdownTransformer } from '@blocksuite/affine/blocks/root';
|
import { MarkdownTransformer } from '@blocksuite/affine/blocks/root';
|
||||||
import { Entity } from '@toeverything/infra';
|
import { Entity } from '@toeverything/infra';
|
||||||
|
|
||||||
|
import type { TagService } from '../../tag';
|
||||||
import {
|
import {
|
||||||
getAFFiNEWorkspaceSchema,
|
getAFFiNEWorkspaceSchema,
|
||||||
type WorkspaceService,
|
type WorkspaceService,
|
||||||
} from '../../workspace';
|
} from '../../workspace';
|
||||||
|
|
||||||
export class IntegrationWriter extends Entity {
|
export class IntegrationWriter extends Entity {
|
||||||
constructor(private readonly workspaceService: WorkspaceService) {
|
constructor(
|
||||||
|
private readonly workspaceService: WorkspaceService,
|
||||||
|
private readonly tagService: TagService
|
||||||
|
) {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -32,18 +36,24 @@ export class IntegrationWriter extends Entity {
|
|||||||
* Update strategy, default is `override`
|
* Update strategy, default is `override`
|
||||||
*/
|
*/
|
||||||
updateStrategy?: 'override' | 'append';
|
updateStrategy?: 'override' | 'append';
|
||||||
|
/**
|
||||||
|
* Tags to apply to the doc
|
||||||
|
*/
|
||||||
|
tags?: string[];
|
||||||
}) {
|
}) {
|
||||||
const {
|
const {
|
||||||
title,
|
title,
|
||||||
content,
|
content,
|
||||||
comment,
|
comment,
|
||||||
docId,
|
docId,
|
||||||
|
tags,
|
||||||
updateStrategy = 'override',
|
updateStrategy = 'override',
|
||||||
} = options;
|
} = options;
|
||||||
|
|
||||||
const workspace = this.workspaceService.workspace;
|
const workspace = this.workspaceService.workspace;
|
||||||
let markdown = comment ? `${content}\n---\n${comment}` : content;
|
let markdown = comment ? `${content}\n---\n${comment}` : content;
|
||||||
|
|
||||||
|
let finalDocId: string;
|
||||||
if (!docId) {
|
if (!docId) {
|
||||||
const newDocId = await MarkdownTransformer.importMarkdownToDoc({
|
const newDocId = await MarkdownTransformer.importMarkdownToDoc({
|
||||||
collection: workspace.docCollection,
|
collection: workspace.docCollection,
|
||||||
@@ -52,7 +62,8 @@ export class IntegrationWriter extends Entity {
|
|||||||
fileName: title,
|
fileName: title,
|
||||||
});
|
});
|
||||||
|
|
||||||
return newDocId;
|
if (!newDocId) throw new Error('Failed to create a new doc');
|
||||||
|
finalDocId = newDocId;
|
||||||
} else {
|
} else {
|
||||||
const collection = workspace.docCollection;
|
const collection = workspace.docCollection;
|
||||||
|
|
||||||
@@ -88,7 +99,16 @@ export class IntegrationWriter extends Entity {
|
|||||||
} else {
|
} else {
|
||||||
throw new Error('Invalid update strategy');
|
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);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { WorkspaceServerService } from '../cloud';
|
|||||||
import { WorkspaceDBService } from '../db';
|
import { WorkspaceDBService } from '../db';
|
||||||
import { DocScope, DocService, DocsService } from '../doc';
|
import { DocScope, DocService, DocsService } from '../doc';
|
||||||
import { GlobalState } from '../storage';
|
import { GlobalState } from '../storage';
|
||||||
|
import { TagService } from '../tag';
|
||||||
import { WorkspaceScope, WorkspaceService } from '../workspace';
|
import { WorkspaceScope, WorkspaceService } from '../workspace';
|
||||||
import { ReadwiseIntegration } from './entities/readwise';
|
import { ReadwiseIntegration } from './entities/readwise';
|
||||||
import { ReadwiseCrawler } from './entities/readwise-crawler';
|
import { ReadwiseCrawler } from './entities/readwise-crawler';
|
||||||
@@ -27,8 +28,8 @@ export function configureIntegrationModule(framework: Framework) {
|
|||||||
WorkspaceServerService,
|
WorkspaceServerService,
|
||||||
])
|
])
|
||||||
.service(IntegrationService)
|
.service(IntegrationService)
|
||||||
.entity(IntegrationWriter, [WorkspaceService])
|
|
||||||
.entity(ReadwiseCrawler, [ReadwiseStore])
|
.entity(ReadwiseCrawler, [ReadwiseStore])
|
||||||
|
.entity(IntegrationWriter, [WorkspaceService, TagService])
|
||||||
.entity(ReadwiseIntegration, [
|
.entity(ReadwiseIntegration, [
|
||||||
IntegrationRefStore,
|
IntegrationRefStore,
|
||||||
ReadwiseStore,
|
ReadwiseStore,
|
||||||
|
|||||||
@@ -87,6 +87,10 @@ export interface ReadwiseConfig {
|
|||||||
* The update strategy
|
* The update strategy
|
||||||
*/
|
*/
|
||||||
updateStrategy?: 'override' | 'append';
|
updateStrategy?: 'override' | 'append';
|
||||||
|
/**
|
||||||
|
* Tag id list to be used when creating highlights
|
||||||
|
*/
|
||||||
|
tags?: string[];
|
||||||
}
|
}
|
||||||
// ===============================
|
// ===============================
|
||||||
// Zotero
|
// Zotero
|
||||||
|
|||||||
@@ -7365,6 +7365,14 @@ export function useAFFiNEI18N(): {
|
|||||||
* `Import`
|
* `Import`
|
||||||
*/
|
*/
|
||||||
["com.affine.integration.readwise.setting.start-import-button"](): string;
|
["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`
|
* `Author`
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1834,6 +1834,8 @@
|
|||||||
"com.affine.integration.readwise.setting.start-import-name": "Start Importing",
|
"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-desc": "Using the settings above",
|
||||||
"com.affine.integration.readwise.setting.start-import-button": "Import",
|
"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.author": "Author",
|
||||||
"com.affine.integration.readwise-prop.source": "Source",
|
"com.affine.integration.readwise-prop.source": "Source",
|
||||||
"com.affine.integration.readwise-prop.created": "Created",
|
"com.affine.integration.readwise-prop.created": "Created",
|
||||||
|
|||||||
Reference in New Issue
Block a user