mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-12 04:18:54 +00:00
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 {
|
||||
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
|
||||
|
||||
@@ -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))',
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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',
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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`
|
||||
*/
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user