From f1c8a88a7cb77bad9f56ae3c3ad0c92d291f742c Mon Sep 17 00:00:00 2001 From: CatsJuice Date: Thu, 20 Mar 2025 23:20:57 +0000 Subject: [PATCH] feat(core): readwise import settings ui (#10913) close AF-2308 --- .../workspace-setting/integration/card.css.ts | 3 + .../integration/readwise/connect.tsx | 20 +- .../integration/readwise/connected.tsx | 12 +- .../integration/readwise/import-dialog.tsx | 16 +- .../integration/readwise/index.tsx | 43 ++++- .../readwise/setting-dialog.css.ts | 81 ++++++++ .../integration/readwise/setting-dialog.tsx | 176 ++++++++++++++++++ .../integration/setting.css.ts | 62 ++++++ .../workspace-setting/integration/setting.tsx | 89 +++++++++ .../modules/integration/entities/readwise.ts | 58 +++++- .../src/modules/integration/store/readwise.ts | 7 + .../core/src/modules/integration/type.ts | 4 + .../i18n/src/i18n-completenesses.json | 4 +- packages/frontend/i18n/src/i18n.gen.ts | 52 ++++++ packages/frontend/i18n/src/resources/en.json | 13 ++ 15 files changed, 615 insertions(+), 25 deletions(-) create mode 100644 packages/frontend/core/src/desktop/dialogs/setting/workspace-setting/integration/readwise/setting-dialog.css.ts create mode 100644 packages/frontend/core/src/desktop/dialogs/setting/workspace-setting/integration/readwise/setting-dialog.tsx create mode 100644 packages/frontend/core/src/desktop/dialogs/setting/workspace-setting/integration/setting.css.ts create mode 100644 packages/frontend/core/src/desktop/dialogs/setting/workspace-setting/integration/setting.tsx diff --git a/packages/frontend/core/src/desktop/dialogs/setting/workspace-setting/integration/card.css.ts b/packages/frontend/core/src/desktop/dialogs/setting/workspace-setting/integration/card.css.ts index 38df09210..aab84c008 100644 --- a/packages/frontend/core/src/desktop/dialogs/setting/workspace-setting/integration/card.css.ts +++ b/packages/frontend/core/src/desktop/dialogs/setting/workspace-setting/integration/card.css.ts @@ -30,6 +30,9 @@ export const cardIcon = style({ fontSize: 24, padding: 4, lineHeight: 0, + display: 'flex', + alignItems: 'center', + justifyContent: 'center', }); export const cardContent = style([ spaceY, diff --git a/packages/frontend/core/src/desktop/dialogs/setting/workspace-setting/integration/readwise/connect.tsx b/packages/frontend/core/src/desktop/dialogs/setting/workspace-setting/integration/readwise/connect.tsx index fc000c6be..6b71aa547 100644 --- a/packages/frontend/core/src/desktop/dialogs/setting/workspace-setting/integration/readwise/connect.tsx +++ b/packages/frontend/core/src/desktop/dialogs/setting/workspace-setting/integration/readwise/connect.tsx @@ -18,7 +18,13 @@ import { inputErrorMsg, } from './index.css'; -const ConnectDialog = ({ onClose }: { onClose: () => void }) => { +const ConnectDialog = ({ + onClose, + onSuccess, +}: { + onClose: () => void; + onSuccess: (token: string) => void; +}) => { const t = useI18n(); const [status, setStatus] = useState<'idle' | 'verifying' | 'error'>('idle'); const [token, setToken] = useState(''); @@ -43,7 +49,7 @@ const ConnectDialog = ({ onClose }: { onClose: () => void }) => { const handleResult = useCallback( (success: boolean, token: string) => { if (success) { - readwise.updateSetting('token', token); + onSuccess(token); } else { setStatus('error'); notify.error({ @@ -54,7 +60,7 @@ const ConnectDialog = ({ onClose }: { onClose: () => void }) => { }); } }, - [readwise, t] + [onSuccess, t] ); const handleConnect = useAsyncCallback( @@ -129,7 +135,11 @@ const ConnectDialog = ({ onClose }: { onClose: () => void }) => { ); }; -export const ConnectButton = () => { +export const ConnectButton = ({ + onSuccess, +}: { + onSuccess: (token: string) => void; +}) => { const t = useI18n(); const [open, setOpen] = useState(false); @@ -143,7 +153,7 @@ export const ConnectButton = () => { return ( <> - {open && } + {open && } diff --git a/packages/frontend/core/src/desktop/dialogs/setting/workspace-setting/integration/readwise/connected.tsx b/packages/frontend/core/src/desktop/dialogs/setting/workspace-setting/integration/readwise/connected.tsx index 9b689b1a2..bf223bdaf 100644 --- a/packages/frontend/core/src/desktop/dialogs/setting/workspace-setting/integration/readwise/connected.tsx +++ b/packages/frontend/core/src/desktop/dialogs/setting/workspace-setting/integration/readwise/connected.tsx @@ -5,7 +5,6 @@ import { useService } from '@toeverything/infra'; import { useCallback, useState } from 'react'; import * as styles from './connected.css'; -import { ImportDialog } from './import-dialog'; import { actionButton } from './index.css'; export const DisconnectDialog = ({ onClose }: { onClose: () => void }) => { @@ -54,23 +53,16 @@ export const DisconnectDialog = ({ onClose }: { onClose: () => void }) => { ); }; -export const ConnectedActions = () => { +export const ConnectedActions = ({ onImport }: { onImport: () => void }) => { const t = useI18n(); const [showDisconnectDialog, setShowDisconnectDialog] = useState(false); - const [showImportDialog, setShowImportDialog] = useState(false); return ( <> {showDisconnectDialog && ( setShowDisconnectDialog(false)} /> )} - {showImportDialog && ( - setShowImportDialog(false)} /> - )} - + + ); +}; diff --git a/packages/frontend/core/src/desktop/dialogs/setting/workspace-setting/integration/setting.css.ts b/packages/frontend/core/src/desktop/dialogs/setting/workspace-setting/integration/setting.css.ts new file mode 100644 index 000000000..07bcc2d32 --- /dev/null +++ b/packages/frontend/core/src/desktop/dialogs/setting/workspace-setting/integration/setting.css.ts @@ -0,0 +1,62 @@ +import { cssVarV2 } from '@toeverything/theme/v2'; +import { style } from '@vanilla-extract/css'; + +export const settingItem = style({ + width: '100%', + display: 'flex', + alignItems: 'flex-start', + justifyContent: 'space-between', + gap: 8, +}); + +export const settingName = style({ + fontSize: 14, + lineHeight: '22px', + fontWeight: 500, + color: cssVarV2.text.primary, +}); + +export const settingDesc = style({ + fontSize: 12, + lineHeight: '20px', + fontWeight: 400, + color: cssVarV2.text.secondary, +}); + +export const textRadioGroup = style({ + borderRadius: 4, + border: `1px solid ${cssVarV2.layer.insideBorder.border}`, +}); +export const textRadioGroupItem = style({ + padding: '8px 16px', + display: 'flex', + alignItems: 'flex-start', + justifyContent: 'space-between', + gap: 8, + cursor: 'pointer', + borderBottom: `1px solid ${cssVarV2.layer.insideBorder.border}`, + selectors: { + '&:last-child': { + borderBottom: 'none', + }, + }, +}); +export const textRadioGroupItemName = style({ + fontSize: 14, + lineHeight: '22px', + fontWeight: 500, + color: cssVarV2.text.primary, +}); +export const textRadioGroupItemDesc = style({ + fontSize: 12, + lineHeight: '20px', + fontWeight: 400, + color: cssVarV2.text.secondary, +}); +export const textRadioGroupItemCheckWrapper = style({ + width: 24, + height: 24, + fontSize: 24, + color: cssVarV2.icon.activated, + flexShrink: 0, +}); diff --git a/packages/frontend/core/src/desktop/dialogs/setting/workspace-setting/integration/setting.tsx b/packages/frontend/core/src/desktop/dialogs/setting/workspace-setting/integration/setting.tsx new file mode 100644 index 000000000..eaf7889ec --- /dev/null +++ b/packages/frontend/core/src/desktop/dialogs/setting/workspace-setting/integration/setting.tsx @@ -0,0 +1,89 @@ +import { Switch } from '@affine/component'; +import { DoneIcon } from '@blocksuite/icons/rc'; +import clsx from 'clsx'; +import type { HTMLAttributes, ReactNode } from 'react'; + +import * as styles from './setting.css'; + +// universal +export interface IntegrationSettingItemProps + extends HTMLAttributes { + name?: ReactNode; + desc?: ReactNode; +} +export const IntegrationSettingItem = ({ + name, + desc, + children, + className, + ...props +}: IntegrationSettingItemProps) => { + return ( +
+
+ {name &&
{name}
} + {desc &&

{desc}

} +
+
{children}
+
+ ); +}; + +// toggle +export interface IntegrationSettingToggleProps { + name: string; + desc?: string; + checked: boolean; + onChange: (checked: boolean) => void; +} +export const IntegrationSettingToggle = ({ + name, + desc, + checked, + onChange, +}: IntegrationSettingToggleProps) => { + return ( + + + + ); +}; + +// text-radio-group +export interface IntegrationSettingTextRadioGroupItem { + name: string; + desc?: string; + value: any; +} +export interface IntegrationSettingTextRadioGroupProps { + items: IntegrationSettingTextRadioGroupItem[]; + checked: any; + onChange: (value: any) => void; +} +export const IntegrationSettingTextRadioGroup = ({ + items, + checked, + onChange, +}: IntegrationSettingTextRadioGroupProps) => { + return ( +
+ {items.map(item => ( +
onChange(item.value)} + key={item.value} + className={styles.textRadioGroupItem} + > +
+
{item.name}
+ {item.desc && ( +
{item.desc}
+ )} +
+
+ {checked === item.value ? : null} +
+
+ ))} +
+ ); +}; diff --git a/packages/frontend/core/src/modules/integration/entities/readwise.ts b/packages/frontend/core/src/modules/integration/entities/readwise.ts index 9bb467e7f..ca262587e 100644 --- a/packages/frontend/core/src/modules/integration/entities/readwise.ts +++ b/packages/frontend/core/src/modules/integration/entities/readwise.ts @@ -30,6 +30,13 @@ export class ReadwiseIntegration extends Entity<{ writer: IntegrationWriter }> { importing$ = new LiveData(false); settings$ = LiveData.from(this.readwiseStore.watchSetting(), undefined); + + setting$( + key: T + ): LiveData { + return this.settings$.selector(setting => setting?.[key]); + } + updateSetting( key: T, value: ReadwiseConfig[T] @@ -77,6 +84,8 @@ export class ReadwiseIntegration extends Entity<{ writer: IntegrationWriter }> { localRefs.map(ref => [ref.refMeta.highlightId, ref]) ); const updateStrategy = this.readwiseStore.getSetting('updateStrategy'); + const syncNewHighlights = + this.readwiseStore.getSetting('syncNewHighlights'); const chunks = chunk(highlights, 2); const total = highlights.length; let finished = 0; @@ -99,8 +108,14 @@ export class ReadwiseIntegration extends Entity<{ writer: IntegrationWriter }> { const refMeta = localRef?.refMeta; const localUpdatedAt = refMeta?.updatedAt; const localDocId = localRef?.id; + const action = this.getAction({ + localUpdatedAt, + remoteUpdatedAt: highlight.updated_at, + updateStrategy, + syncNewHighlights, + }); // write if not matched - if (localUpdatedAt !== highlight.updated_at && !signal?.aborted) { + if (action !== 'skip' && !signal?.aborted) { await this.highlightToAffineDoc(highlight, book, localDocId, { updateStrategy, integrationId, @@ -139,7 +154,7 @@ export class ReadwiseIntegration extends Entity<{ writer: IntegrationWriter }> { title: book.title, docId, comment: highlight.note, - updateStrategy, + updateStrategy: updateStrategy ?? 'append', }); // write failed @@ -168,8 +183,43 @@ export class ReadwiseIntegration extends Entity<{ writer: IntegrationWriter }> { }); } + getAction(info: { + localUpdatedAt?: string; + remoteUpdatedAt?: string; + updateStrategy?: ReadwiseConfig['updateStrategy']; + syncNewHighlights?: ReadwiseConfig['syncNewHighlights']; + }) { + const { + localUpdatedAt, + remoteUpdatedAt, + updateStrategy, + syncNewHighlights, + } = info; + + return !localUpdatedAt + ? syncNewHighlights + ? 'new' + : 'skip' + : localUpdatedAt !== remoteUpdatedAt + ? updateStrategy + ? 'update' + : 'skip' + : 'skip'; + } + + connect(token: string) { + this.readwiseStore.setSettings({ + token, + updateStrategy: 'append', + syncNewHighlights: true, + }); + } + disconnect() { - this.readwiseStore.setSetting('token', undefined); - this.readwiseStore.setSetting('lastImportedAt', undefined); + this.readwiseStore.setSettings({ + token: undefined, + updateStrategy: undefined, + syncNewHighlights: undefined, + }); } } diff --git a/packages/frontend/core/src/modules/integration/store/readwise.ts b/packages/frontend/core/src/modules/integration/store/readwise.ts index b767da64e..762166e12 100644 --- a/packages/frontend/core/src/modules/integration/store/readwise.ts +++ b/packages/frontend/core/src/modules/integration/store/readwise.ts @@ -81,4 +81,11 @@ export class ReadwiseStore extends Store { [key]: value, }); } + + setSettings(settings: Partial) { + this.globalState.set(this.getStorageKey(), { + ...this.getSetting(), + ...settings, + }); + } } diff --git a/packages/frontend/core/src/modules/integration/type.ts b/packages/frontend/core/src/modules/integration/type.ts index 795e96105..38e95fdc5 100644 --- a/packages/frontend/core/src/modules/integration/type.ts +++ b/packages/frontend/core/src/modules/integration/type.ts @@ -79,6 +79,10 @@ export interface ReadwiseConfig { * The last import time */ lastImportedAt?: string; + /** + * Whether to sync new highlights + */ + syncNewHighlights?: boolean; /** * The update strategy */ diff --git a/packages/frontend/i18n/src/i18n-completenesses.json b/packages/frontend/i18n/src/i18n-completenesses.json index 06b4cc9bc..2683fe530 100644 --- a/packages/frontend/i18n/src/i18n-completenesses.json +++ b/packages/frontend/i18n/src/i18n-completenesses.json @@ -1,12 +1,12 @@ { "ar": 95, "ca": 4, - "da": 5, + "da": 4, "de": 95, "el-GR": 95, "en": 100, "es-AR": 95, - "es-CL": 97, + "es-CL": 96, "es": 95, "fa": 95, "fr": 95, diff --git a/packages/frontend/i18n/src/i18n.gen.ts b/packages/frontend/i18n/src/i18n.gen.ts index 566472649..acdd7153d 100644 --- a/packages/frontend/i18n/src/i18n.gen.ts +++ b/packages/frontend/i18n/src/i18n.gen.ts @@ -7313,6 +7313,58 @@ export function useAFFiNEI18N(): { ["com.affine.integration.readwise.import.abort-notify-desc"](options: { readonly finished: string; }): string; + /** + * `Configuration` + */ + ["com.affine.integration.readwise.setting.caption"](): string; + /** + * `New Readwise highlights will be imported to AFFiNE ` + */ + ["com.affine.integration.readwise.setting.sync-new-name"](): string; + /** + * `New highlights in Readwise will be synced to AFFiNE ` + */ + ["com.affine.integration.readwise.setting.sync-new-desc"](): string; + /** + * `Updates to Readwise highlights will be imported` + */ + ["com.affine.integration.readwise.setting.update-name"](): string; + /** + * `Enable this, so that we will process updates of existing highlights from Readwise ` + */ + ["com.affine.integration.readwise.setting.update-desc"](): string; + /** + * `How do we handle updates` + */ + ["com.affine.integration.readwise.setting.update-strategy"](): string; + /** + * `Append new version to the end` + */ + ["com.affine.integration.readwise.setting.update-append-name"](): string; + /** + * `Cited or modified highlights will have future versions added to the end of them` + */ + ["com.affine.integration.readwise.setting.update-append-desc"](): string; + /** + * `Overwrite with new version` + */ + ["com.affine.integration.readwise.setting.update-override-name"](): string; + /** + * `Cited or modified highlights will be overwritten if there are future updates` + */ + ["com.affine.integration.readwise.setting.update-override-desc"](): string; + /** + * `Start Importing` + */ + ["com.affine.integration.readwise.setting.start-import-name"](): string; + /** + * `Using the settings above` + */ + ["com.affine.integration.readwise.setting.start-import-desc"](): string; + /** + * `Import` + */ + ["com.affine.integration.readwise.setting.start-import-button"](): string; /** * `Author` */ diff --git a/packages/frontend/i18n/src/resources/en.json b/packages/frontend/i18n/src/resources/en.json index 11124017e..af11c0188 100644 --- a/packages/frontend/i18n/src/resources/en.json +++ b/packages/frontend/i18n/src/resources/en.json @@ -1821,6 +1821,19 @@ "com.affine.integration.readwise.import.importing-stop": "Stop Importing", "com.affine.integration.readwise.import.abort-notify-title": "Importing aborted", "com.affine.integration.readwise.import.abort-notify-desc": "Import aborted, with {{finished}} highlights processed", + "com.affine.integration.readwise.setting.caption": "Configuration", + "com.affine.integration.readwise.setting.sync-new-name": "New Readwise highlights will be imported to AFFiNE ", + "com.affine.integration.readwise.setting.sync-new-desc": "New highlights in Readwise will be synced to AFFiNE ", + "com.affine.integration.readwise.setting.update-name": "Updates to Readwise highlights will be imported", + "com.affine.integration.readwise.setting.update-desc": "Enable this, so that we will process updates of existing highlights from Readwise ", + "com.affine.integration.readwise.setting.update-strategy": "How do we handle updates", + "com.affine.integration.readwise.setting.update-append-name": "Append new version to the end", + "com.affine.integration.readwise.setting.update-append-desc": "Cited or modified highlights will have future versions added to the end of them", + "com.affine.integration.readwise.setting.update-override-name": "Overwrite with new version", + "com.affine.integration.readwise.setting.update-override-desc": "Cited or modified highlights will be overwritten if there are future updates", + "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-prop.author": "Author", "com.affine.integration.readwise-prop.source": "Source", "com.affine.integration.readwise-prop.created": "Created",