mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-09-07 09:21:24 +08:00
feat(core): track for integration (#11128)
This commit is contained in:
+4
@@ -17,6 +17,7 @@ import {
|
|||||||
getTokenLink,
|
getTokenLink,
|
||||||
inputErrorMsg,
|
inputErrorMsg,
|
||||||
} from './index.css';
|
} from './index.css';
|
||||||
|
import { readwiseTrack } from './track';
|
||||||
|
|
||||||
const ConnectDialog = ({
|
const ConnectDialog = ({
|
||||||
onClose,
|
onClose,
|
||||||
@@ -48,6 +49,9 @@ const ConnectDialog = ({
|
|||||||
|
|
||||||
const handleResult = useCallback(
|
const handleResult = useCallback(
|
||||||
(success: boolean, token: string) => {
|
(success: boolean, token: string) => {
|
||||||
|
readwiseTrack.connectIntegration({
|
||||||
|
result: success ? 'success' : 'failed',
|
||||||
|
});
|
||||||
if (success) {
|
if (success) {
|
||||||
onSuccess(token);
|
onSuccess(token);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
+3
@@ -7,6 +7,7 @@ import { useCallback, useState } from 'react';
|
|||||||
|
|
||||||
import * as styles from './connected.css';
|
import * as styles from './connected.css';
|
||||||
import { actionButton } from './index.css';
|
import { actionButton } from './index.css';
|
||||||
|
import { readwiseTrack } from './track';
|
||||||
|
|
||||||
export const DisconnectDialog = ({ onClose }: { onClose: () => void }) => {
|
export const DisconnectDialog = ({ onClose }: { onClose: () => void }) => {
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
@@ -21,11 +22,13 @@ export const DisconnectDialog = ({ onClose }: { onClose: () => void }) => {
|
|||||||
const handleCancel = useCallback(() => onClose(), [onClose]);
|
const handleCancel = useCallback(() => onClose(), [onClose]);
|
||||||
const handleKeep = useCallback(() => {
|
const handleKeep = useCallback(() => {
|
||||||
readwise.disconnect();
|
readwise.disconnect();
|
||||||
|
readwiseTrack.disconnectIntegration({ method: 'keep' });
|
||||||
onClose();
|
onClose();
|
||||||
}, [onClose, readwise]);
|
}, [onClose, readwise]);
|
||||||
const handleDelete = useAsyncCallback(async () => {
|
const handleDelete = useAsyncCallback(async () => {
|
||||||
await readwise.deleteAll();
|
await readwise.deleteAll();
|
||||||
readwise.disconnect();
|
readwise.disconnect();
|
||||||
|
readwiseTrack.disconnectIntegration({ method: 'delete' });
|
||||||
onClose();
|
onClose();
|
||||||
}, [onClose, readwise]);
|
}, [onClose, readwise]);
|
||||||
|
|
||||||
|
|||||||
+45
-12
@@ -14,6 +14,7 @@ import { InformationFillDuotoneIcon } from '@blocksuite/icons/rc';
|
|||||||
import { useLiveData, useService } from '@toeverything/infra';
|
import { useLiveData, useService } from '@toeverything/infra';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import {
|
import {
|
||||||
|
type ChangeEvent,
|
||||||
type Dispatch,
|
type Dispatch,
|
||||||
forwardRef,
|
forwardRef,
|
||||||
type SetStateAction,
|
type SetStateAction,
|
||||||
@@ -26,6 +27,7 @@ import {
|
|||||||
import { Virtuoso } from 'react-virtuoso';
|
import { Virtuoso } from 'react-virtuoso';
|
||||||
|
|
||||||
import * as styles from './import-dialog.css';
|
import * as styles from './import-dialog.css';
|
||||||
|
import { readwiseTrack } from './track';
|
||||||
|
|
||||||
export const ImportDialog = ({ onClose }: { onClose: () => void }) => {
|
export const ImportDialog = ({ onClose }: { onClose: () => void }) => {
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
@@ -54,6 +56,13 @@ export const ImportDialog = ({ onClose }: { onClose: () => void }) => {
|
|||||||
);
|
);
|
||||||
const handleConfirmImport = useCallback(
|
const handleConfirmImport = useCallback(
|
||||||
(ids: string[]) => {
|
(ids: string[]) => {
|
||||||
|
readwiseTrack.confirmIntegrationImport({
|
||||||
|
control: 'Readwise import list',
|
||||||
|
method: readwise.setting$('lastImportedAt').value
|
||||||
|
? 'withtimestamp'
|
||||||
|
: 'new',
|
||||||
|
});
|
||||||
|
|
||||||
if (ids.length === 0) {
|
if (ids.length === 0) {
|
||||||
onClose();
|
onClose();
|
||||||
return;
|
return;
|
||||||
@@ -65,15 +74,26 @@ export const ImportDialog = ({ onClose }: { onClose: () => void }) => {
|
|||||||
abortControllerRef.current = abortController;
|
abortControllerRef.current = abortController;
|
||||||
const signal = abortController.signal;
|
const signal = abortController.signal;
|
||||||
|
|
||||||
|
const startTime = Date.now();
|
||||||
readwise
|
readwise
|
||||||
.highlightsToAffineDocs(selectedHighlights.reverse(), books, {
|
.highlightsToAffineDocs(selectedHighlights.reverse(), books, {
|
||||||
signal,
|
signal,
|
||||||
onProgress: setImportProgress,
|
onProgress: setImportProgress,
|
||||||
onComplete: () => {
|
onComplete: () => {
|
||||||
|
readwiseTrack.completeIntegrationImport({
|
||||||
|
total: selectedHighlights.length,
|
||||||
|
done: selectedHighlights.length,
|
||||||
|
time: (Date.now() - startTime) / 1000,
|
||||||
|
});
|
||||||
readwise.updateSetting('lastImportedAt', timestamp);
|
readwise.updateSetting('lastImportedAt', timestamp);
|
||||||
onClose();
|
onClose();
|
||||||
},
|
},
|
||||||
onAbort: finished => {
|
onAbort: finished => {
|
||||||
|
readwiseTrack.abortIntegrationImport({
|
||||||
|
total: selectedHighlights.length,
|
||||||
|
done: finished,
|
||||||
|
time: (Date.now() - startTime) / 1000,
|
||||||
|
});
|
||||||
notify({
|
notify({
|
||||||
icon: <InformationFillDuotoneIcon />,
|
icon: <InformationFillDuotoneIcon />,
|
||||||
style: 'normal',
|
style: 'normal',
|
||||||
@@ -184,6 +204,10 @@ const SelectStage = ({
|
|||||||
const [selected, setSelected] = useState<ReadwiseHighlight['id'][]>([]);
|
const [selected, setSelected] = useState<ReadwiseHighlight['id'][]>([]);
|
||||||
|
|
||||||
const handleResetLastImportedAt = useCallback(() => {
|
const handleResetLastImportedAt = useCallback(() => {
|
||||||
|
readwiseTrack.startIntegrationImport({
|
||||||
|
method: 'cleartimestamp',
|
||||||
|
control: 'Readwise import list',
|
||||||
|
});
|
||||||
readwise.updateSetting('lastImportedAt', undefined);
|
readwise.updateSetting('lastImportedAt', undefined);
|
||||||
onResetLastImportedAt();
|
onResetLastImportedAt();
|
||||||
}, [onResetLastImportedAt, readwise]);
|
}, [onResetLastImportedAt, readwise]);
|
||||||
@@ -315,11 +339,17 @@ const HighlightTable = ({
|
|||||||
.catch(console.error);
|
.catch(console.error);
|
||||||
}, [readwise]);
|
}, [readwise]);
|
||||||
|
|
||||||
const handleToggleSelectAll = useCallback(() => {
|
const handleToggleSelectAll = useCallback(
|
||||||
setSelected(prev =>
|
(_: ChangeEvent, checked: boolean) => {
|
||||||
prev.length === highlights.length ? [] : highlights.map(h => h.id)
|
readwiseTrack.selectIntegrationImport({
|
||||||
);
|
method: 'all',
|
||||||
}, [highlights, setSelected]);
|
option: checked ? 'on' : 'off',
|
||||||
|
control: 'Readwise import list',
|
||||||
|
});
|
||||||
|
setSelected(checked ? highlights.map(h => h.id) : []);
|
||||||
|
},
|
||||||
|
[highlights, setSelected]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.table}>
|
<div className={styles.table}>
|
||||||
@@ -358,14 +388,17 @@ const HighlightTable = ({
|
|||||||
<div className={styles.tableCellSelect}>
|
<div className={styles.tableCellSelect}>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={selected.includes(highlight.id)}
|
checked={selected.includes(highlight.id)}
|
||||||
onChange={() => {
|
onChange={(_: ChangeEvent, checked: boolean) => {
|
||||||
setSelected(prev => {
|
readwiseTrack.selectIntegrationImport({
|
||||||
if (prev.includes(highlight.id)) {
|
method: 'single',
|
||||||
return prev.filter(id => id !== highlight.id);
|
option: checked ? 'on' : 'off',
|
||||||
} else {
|
control: 'Readwise import list',
|
||||||
return [...prev, highlight.id];
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
setSelected(
|
||||||
|
checked
|
||||||
|
? [...selected, highlight.id]
|
||||||
|
: selected.filter(id => id !== highlight.id)
|
||||||
|
);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+10
-1
@@ -16,6 +16,7 @@ import { ConnectButton } from './connect';
|
|||||||
import { ConnectedActions } from './connected';
|
import { ConnectedActions } from './connected';
|
||||||
import { ImportDialog } from './import-dialog';
|
import { ImportDialog } from './import-dialog';
|
||||||
import { SettingDialog } from './setting-dialog';
|
import { SettingDialog } from './setting-dialog';
|
||||||
|
import { readwiseTrack } from './track';
|
||||||
|
|
||||||
export const ReadwiseIntegration = () => {
|
export const ReadwiseIntegration = () => {
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
@@ -42,6 +43,14 @@ export const ReadwiseIntegration = () => {
|
|||||||
setOpenImportDialog(true);
|
setOpenImportDialog(true);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const onImportClick = useCallback(() => {
|
||||||
|
readwiseTrack.startIntegrationImport({
|
||||||
|
method: settings?.lastImportedAt ? 'withtimestamp' : 'new',
|
||||||
|
control: 'Readwise Card',
|
||||||
|
});
|
||||||
|
handleImport();
|
||||||
|
}, [handleImport, settings?.lastImportedAt]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<IntegrationCard>
|
<IntegrationCard>
|
||||||
<IntegrationCardHeader
|
<IntegrationCardHeader
|
||||||
@@ -56,7 +65,7 @@ export const ReadwiseIntegration = () => {
|
|||||||
<IntegrationCardFooter>
|
<IntegrationCardFooter>
|
||||||
{token ? (
|
{token ? (
|
||||||
<>
|
<>
|
||||||
<ConnectedActions onImport={handleImport} />
|
<ConnectedActions onImport={onImportClick} />
|
||||||
{openSetting && (
|
{openSetting && (
|
||||||
<SettingDialog
|
<SettingDialog
|
||||||
onClose={handleCloseSetting}
|
onClose={handleCloseSetting}
|
||||||
|
|||||||
+33
-1
@@ -18,6 +18,7 @@ import {
|
|||||||
IntegrationSettingToggle,
|
IntegrationSettingToggle,
|
||||||
} from '../setting';
|
} from '../setting';
|
||||||
import * as styles from './setting-dialog.css';
|
import * as styles from './setting-dialog.css';
|
||||||
|
import { readwiseTrack } from './track';
|
||||||
|
|
||||||
export const SettingDialog = ({
|
export const SettingDialog = ({
|
||||||
onClose,
|
onClose,
|
||||||
@@ -59,6 +60,18 @@ export const SettingDialog = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const trackModifySetting = (
|
||||||
|
item: 'New' | 'Update' | 'Tag',
|
||||||
|
option: 'on' | 'off',
|
||||||
|
method?: 'append' | 'override'
|
||||||
|
) => {
|
||||||
|
readwiseTrack.modifyIntegrationSettings({
|
||||||
|
item,
|
||||||
|
option,
|
||||||
|
method,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const Divider = () => {
|
const Divider = () => {
|
||||||
return <li className={styles.divider} />;
|
return <li className={styles.divider} />;
|
||||||
};
|
};
|
||||||
@@ -72,6 +85,7 @@ const NewHighlightSetting = () => {
|
|||||||
|
|
||||||
const toggle = useCallback(
|
const toggle = useCallback(
|
||||||
(value: boolean) => {
|
(value: boolean) => {
|
||||||
|
trackModifySetting('New', value ? 'on' : 'off');
|
||||||
readwise.updateSetting('syncNewHighlights', value);
|
readwise.updateSetting('syncNewHighlights', value);
|
||||||
},
|
},
|
||||||
[readwise]
|
[readwise]
|
||||||
@@ -98,6 +112,7 @@ const UpdateStrategySetting = () => {
|
|||||||
|
|
||||||
const toggle = useCallback(
|
const toggle = useCallback(
|
||||||
(value: boolean) => {
|
(value: boolean) => {
|
||||||
|
trackModifySetting('Update', value ? 'on' : 'off', 'append');
|
||||||
if (!value) readwise.updateSetting('updateStrategy', undefined);
|
if (!value) readwise.updateSetting('updateStrategy', undefined);
|
||||||
else readwise.updateSetting('updateStrategy', 'append');
|
else readwise.updateSetting('updateStrategy', 'append');
|
||||||
},
|
},
|
||||||
@@ -106,6 +121,7 @@ const UpdateStrategySetting = () => {
|
|||||||
|
|
||||||
const handleUpdate = useCallback(
|
const handleUpdate = useCallback(
|
||||||
(value: ReadwiseConfig['updateStrategy']) => {
|
(value: ReadwiseConfig['updateStrategy']) => {
|
||||||
|
trackModifySetting('Update', 'on', value);
|
||||||
readwise.updateSetting('updateStrategy', value);
|
readwise.updateSetting('updateStrategy', value);
|
||||||
},
|
},
|
||||||
[readwise]
|
[readwise]
|
||||||
@@ -167,12 +183,23 @@ const UpdateStrategySetting = () => {
|
|||||||
|
|
||||||
const StartImport = ({ onImport }: { onImport: () => void }) => {
|
const StartImport = ({ onImport }: { onImport: () => void }) => {
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
|
const readwise = useService(IntegrationService).readwise;
|
||||||
|
|
||||||
|
const handleImport = useCallback(() => {
|
||||||
|
const lastImportedAt = readwise.setting$('lastImportedAt').value;
|
||||||
|
readwiseTrack.startIntegrationImport({
|
||||||
|
method: lastImportedAt ? 'withtimestamp' : 'new',
|
||||||
|
control: 'Readwise settings',
|
||||||
|
});
|
||||||
|
onImport();
|
||||||
|
}, [onImport, readwise]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<IntegrationSettingItem
|
<IntegrationSettingItem
|
||||||
name={t['com.affine.integration.readwise.setting.start-import-name']()}
|
name={t['com.affine.integration.readwise.setting.start-import-name']()}
|
||||||
desc={t['com.affine.integration.readwise.setting.start-import-desc']()}
|
desc={t['com.affine.integration.readwise.setting.start-import-desc']()}
|
||||||
>
|
>
|
||||||
<Button onClick={onImport}>
|
<Button onClick={handleImport}>
|
||||||
{t['com.affine.integration.readwise.setting.start-import-button']()}
|
{t['com.affine.integration.readwise.setting.start-import-button']()}
|
||||||
</Button>
|
</Button>
|
||||||
</IntegrationSettingItem>
|
</IntegrationSettingItem>
|
||||||
@@ -226,18 +253,23 @@ const TagsSetting = () => {
|
|||||||
);
|
);
|
||||||
const onSelectTag = useCallback(
|
const onSelectTag = useCallback(
|
||||||
(tagId: string) => {
|
(tagId: string) => {
|
||||||
|
trackModifySetting('Tag', 'on');
|
||||||
updateReadwiseTags([...(tagIds ?? []), tagId]);
|
updateReadwiseTags([...(tagIds ?? []), tagId]);
|
||||||
},
|
},
|
||||||
[tagIds, updateReadwiseTags]
|
[tagIds, updateReadwiseTags]
|
||||||
);
|
);
|
||||||
const onDeselectTag = useCallback(
|
const onDeselectTag = useCallback(
|
||||||
(tagId: string) => {
|
(tagId: string) => {
|
||||||
|
trackModifySetting('Tag', 'off');
|
||||||
updateReadwiseTags(tagIds?.filter(id => id !== tagId) ?? []);
|
updateReadwiseTags(tagIds?.filter(id => id !== tagId) ?? []);
|
||||||
},
|
},
|
||||||
[tagIds, updateReadwiseTags]
|
[tagIds, updateReadwiseTags]
|
||||||
);
|
);
|
||||||
const onDeleteTag = useCallback(
|
const onDeleteTag = useCallback(
|
||||||
(tagId: string) => {
|
(tagId: string) => {
|
||||||
|
if (tagIds?.includes(tagId)) {
|
||||||
|
trackModifySetting('Tag', 'off');
|
||||||
|
}
|
||||||
tagService.tagList.deleteTag(tagId);
|
tagService.tagList.deleteTag(tagId);
|
||||||
updateReadwiseTags(tagIds ?? []);
|
updateReadwiseTags(tagIds ?? []);
|
||||||
},
|
},
|
||||||
|
|||||||
+25
@@ -0,0 +1,25 @@
|
|||||||
|
import track from '@affine/track';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wrap the track function to add default properties to the first argument
|
||||||
|
*/
|
||||||
|
export const readwiseTrack = new Proxy(track.$.settingsPanel.integrationList, {
|
||||||
|
get(target, key, receiver) {
|
||||||
|
const original = Reflect.get(target, key, receiver);
|
||||||
|
|
||||||
|
if (typeof original !== 'function') {
|
||||||
|
return original;
|
||||||
|
}
|
||||||
|
|
||||||
|
return function (this: unknown, ...args: unknown[]) {
|
||||||
|
if (args.length > 0 && typeof args[0] === 'object' && args[0] !== null) {
|
||||||
|
args[0] = {
|
||||||
|
type: 'readwise',
|
||||||
|
control: 'Readwise Card',
|
||||||
|
...args[0],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return original.apply(this, args);
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -149,6 +149,18 @@ type TemplateEvents = 'openTemplateListMenu';
|
|||||||
type NotificationEvents = 'openInbox' | 'clickNotification';
|
type NotificationEvents = 'openInbox' | 'clickNotification';
|
||||||
// END SECTION
|
// END SECTION
|
||||||
|
|
||||||
|
// SECTION: Integration
|
||||||
|
type IntegrationEvents =
|
||||||
|
| 'connectIntegration'
|
||||||
|
| 'disconnectIntegration'
|
||||||
|
| 'modifyIntegrationSettings'
|
||||||
|
| 'startIntegrationImport'
|
||||||
|
| 'selectIntegrationImport'
|
||||||
|
| 'confirmIntegrationImport'
|
||||||
|
| 'abortIntegrationImport'
|
||||||
|
| 'completeIntegrationImport';
|
||||||
|
// END SECTION
|
||||||
|
|
||||||
type UserEvents =
|
type UserEvents =
|
||||||
| GeneralEvents
|
| GeneralEvents
|
||||||
| AppEvents
|
| AppEvents
|
||||||
@@ -167,8 +179,8 @@ type UserEvents =
|
|||||||
| DNDEvents
|
| DNDEvents
|
||||||
| AttachmentEvents
|
| AttachmentEvents
|
||||||
| TemplateEvents
|
| TemplateEvents
|
||||||
| NotificationEvents;
|
| NotificationEvents
|
||||||
|
| IntegrationEvents;
|
||||||
interface PageDivision {
|
interface PageDivision {
|
||||||
[page: string]: {
|
[page: string]: {
|
||||||
[segment: string]: {
|
[segment: string]: {
|
||||||
@@ -235,6 +247,16 @@ const PageEvents = {
|
|||||||
],
|
],
|
||||||
billing: ['viewPlans', 'bookDemo'],
|
billing: ['viewPlans', 'bookDemo'],
|
||||||
about: ['checkUpdates', 'downloadUpdate', 'changeAppSetting'],
|
about: ['checkUpdates', 'downloadUpdate', 'changeAppSetting'],
|
||||||
|
integrationList: [
|
||||||
|
'connectIntegration',
|
||||||
|
'disconnectIntegration',
|
||||||
|
'modifyIntegrationSettings',
|
||||||
|
'startIntegrationImport',
|
||||||
|
'selectIntegrationImport',
|
||||||
|
'confirmIntegrationImport',
|
||||||
|
'abortIntegrationImport',
|
||||||
|
'completeIntegrationImport',
|
||||||
|
],
|
||||||
},
|
},
|
||||||
cmdk: {
|
cmdk: {
|
||||||
recent: ['recentDocs'],
|
recent: ['recentDocs'],
|
||||||
@@ -485,6 +507,10 @@ type ImportArgs = {
|
|||||||
docCount: number;
|
docCount: number;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
type IntegrationArgs<T extends Record<string, any>> = {
|
||||||
|
type: string;
|
||||||
|
control: 'Readwise Card' | 'Readwise settings' | 'Readwise import list';
|
||||||
|
} & T;
|
||||||
|
|
||||||
export type EventArgs = {
|
export type EventArgs = {
|
||||||
createWorkspace: { flavour: string };
|
createWorkspace: { flavour: string };
|
||||||
@@ -556,6 +582,33 @@ export type EventArgs = {
|
|||||||
item: 'read' | 'button' | 'dismiss';
|
item: 'read' | 'button' | 'dismiss';
|
||||||
button?: string;
|
button?: string;
|
||||||
};
|
};
|
||||||
|
connectIntegration: IntegrationArgs<{ result: 'success' | 'failed' }>;
|
||||||
|
disconnectIntegration: IntegrationArgs<{ method: 'keep' | 'delete' }>;
|
||||||
|
modifyIntegrationSettings: IntegrationArgs<{
|
||||||
|
item: string;
|
||||||
|
option: any;
|
||||||
|
method: any;
|
||||||
|
}>;
|
||||||
|
startIntegrationImport: IntegrationArgs<{
|
||||||
|
method: 'new' | 'withtimestamp' | 'cleartimestamp';
|
||||||
|
}>;
|
||||||
|
selectIntegrationImport: IntegrationArgs<{
|
||||||
|
method: 'single' | 'all';
|
||||||
|
option: 'on' | 'off';
|
||||||
|
}>;
|
||||||
|
confirmIntegrationImport: IntegrationArgs<{
|
||||||
|
method: 'new' | 'withtimestamp';
|
||||||
|
}>;
|
||||||
|
abortIntegrationImport: IntegrationArgs<{
|
||||||
|
time: number;
|
||||||
|
done: number;
|
||||||
|
total: number;
|
||||||
|
}>;
|
||||||
|
completeIntegrationImport: IntegrationArgs<{
|
||||||
|
time: number;
|
||||||
|
done: number;
|
||||||
|
total: number;
|
||||||
|
}>;
|
||||||
};
|
};
|
||||||
|
|
||||||
// for type checking
|
// for type checking
|
||||||
|
|||||||
Reference in New Issue
Block a user