feat(core): track for integration (#11128)

This commit is contained in:
CatsJuice
2025-03-31 11:45:32 +00:00
parent f1882061a2
commit 731a4c952f
7 changed files with 175 additions and 16 deletions
@@ -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 {
@@ -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]);
@@ -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>
@@ -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}
@@ -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 ?? []);
}, },
@@ -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);
};
},
});
+55 -2
View File
@@ -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