mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-08-03 02:20:19 +08:00
feat: add more tracking events (#6866)
Added most tracking events what is missing: - still need a way to track events in blocksuite - some events may not 100% accurate of the one defined in the PRD
This commit is contained in:
@@ -37,6 +37,10 @@ export class EventBus {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get root(): EventBus {
|
||||||
|
return this.parent?.root ?? this;
|
||||||
|
}
|
||||||
|
|
||||||
on<T>(id: string, listener: (event: FrameworkEvent<T>) => void) {
|
on<T>(id: string, listener: (event: FrameworkEvent<T>) => void) {
|
||||||
if (!this.listeners[id]) {
|
if (!this.listeners[id]) {
|
||||||
this.listeners[id] = [];
|
this.listeners[id] = [];
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import type { createStore } from 'jotai';
|
|||||||
|
|
||||||
import { openSettingModalAtom, openWorkspaceListModalAtom } from '../atoms';
|
import { openSettingModalAtom, openWorkspaceListModalAtom } from '../atoms';
|
||||||
import type { useNavigateHelper } from '../hooks/use-navigate-helper';
|
import type { useNavigateHelper } from '../hooks/use-navigate-helper';
|
||||||
|
import { mixpanel } from '../utils/mixpanel';
|
||||||
|
|
||||||
export function registerAffineNavigationCommands({
|
export function registerAffineNavigationCommands({
|
||||||
t,
|
t,
|
||||||
@@ -76,6 +77,10 @@ export function registerAffineNavigationCommands({
|
|||||||
label: t['com.affine.cmdk.affine.navigation.open-settings'](),
|
label: t['com.affine.cmdk.affine.navigation.open-settings'](),
|
||||||
keyBinding: '$mod+,',
|
keyBinding: '$mod+,',
|
||||||
run() {
|
run() {
|
||||||
|
mixpanel.track('SettingsViewed', {
|
||||||
|
// page:
|
||||||
|
segment: 'cmdk',
|
||||||
|
});
|
||||||
store.set(openSettingModalAtom, s => ({
|
store.set(openSettingModalAtom, s => ({
|
||||||
activeTab: 'appearance',
|
activeTab: 'appearance',
|
||||||
open: !s.open,
|
open: !s.open,
|
||||||
@@ -84,6 +89,25 @@ export function registerAffineNavigationCommands({
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
unsubs.push(
|
||||||
|
registerAffineCommand({
|
||||||
|
id: 'affine:open-account',
|
||||||
|
category: 'affine:navigation',
|
||||||
|
icon: <ArrowRightBigIcon />,
|
||||||
|
label: t['com.affine.cmdk.affine.navigation.open-account-settings'](),
|
||||||
|
run() {
|
||||||
|
mixpanel.track('AccountSettingsViewed', {
|
||||||
|
// page:
|
||||||
|
segment: 'cmdk',
|
||||||
|
});
|
||||||
|
store.set(openSettingModalAtom, s => ({
|
||||||
|
activeTab: 'account',
|
||||||
|
open: !s.open,
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
unsubs.push(
|
unsubs.push(
|
||||||
registerAffineCommand({
|
registerAffineCommand({
|
||||||
id: 'affine:goto-trash',
|
id: 'affine:goto-trash',
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import type { useTheme } from 'next-themes';
|
|||||||
|
|
||||||
import { openQuickSearchModalAtom } from '../atoms';
|
import { openQuickSearchModalAtom } from '../atoms';
|
||||||
import type { useLanguageHelper } from '../hooks/affine/use-language-helper';
|
import type { useLanguageHelper } from '../hooks/affine/use-language-helper';
|
||||||
|
import { mixpanel } from '../utils';
|
||||||
|
|
||||||
export function registerAffineSettingsCommands({
|
export function registerAffineSettingsCommands({
|
||||||
t,
|
t,
|
||||||
@@ -38,6 +39,9 @@ export function registerAffineSettingsCommands({
|
|||||||
label: '',
|
label: '',
|
||||||
icon: <SettingsIcon />,
|
icon: <SettingsIcon />,
|
||||||
run() {
|
run() {
|
||||||
|
mixpanel.track('QuickSearchOpened', {
|
||||||
|
control: 'shortcut',
|
||||||
|
});
|
||||||
const quickSearchModalState = store.get(openQuickSearchModalAtom);
|
const quickSearchModalState = store.get(openQuickSearchModalAtom);
|
||||||
|
|
||||||
if (!editor) {
|
if (!editor) {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Button, FlexWrapper, notify } from '@affine/component';
|
import { Button, FlexWrapper, notify } from '@affine/component';
|
||||||
import { openSettingModalAtom } from '@affine/core/atoms';
|
import { openSettingModalAtom } from '@affine/core/atoms';
|
||||||
import { SubscriptionService } from '@affine/core/modules/cloud';
|
import { SubscriptionService } from '@affine/core/modules/cloud';
|
||||||
|
import { mixpanel } from '@affine/core/utils';
|
||||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
import { AiIcon } from '@blocksuite/icons';
|
import { AiIcon } from '@blocksuite/icons';
|
||||||
@@ -69,6 +70,11 @@ export const AIOnboardingEdgeless = ({
|
|||||||
const mode = useLiveData(doc.mode$);
|
const mode = useLiveData(doc.mode$);
|
||||||
|
|
||||||
const goToPricingPlans = useCallback(() => {
|
const goToPricingPlans = useCallback(() => {
|
||||||
|
mixpanel.track('PlansViewed', {
|
||||||
|
page: 'whiteboard editor',
|
||||||
|
segment: 'ai onboarding',
|
||||||
|
module: 'whiteboard dialog',
|
||||||
|
});
|
||||||
setSettingModal({
|
setSettingModal({
|
||||||
open: true,
|
open: true,
|
||||||
activeTab: 'plans',
|
activeTab: 'plans',
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { Button, IconButton, Modal } from '@affine/component';
|
|||||||
import { openSettingModalAtom } from '@affine/core/atoms';
|
import { openSettingModalAtom } from '@affine/core/atoms';
|
||||||
import { useBlurRoot } from '@affine/core/hooks/use-blur-root';
|
import { useBlurRoot } from '@affine/core/hooks/use-blur-root';
|
||||||
import { SubscriptionService } from '@affine/core/modules/cloud';
|
import { SubscriptionService } from '@affine/core/modules/cloud';
|
||||||
|
import { mixpanel } from '@affine/core/utils';
|
||||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||||
import { Trans } from '@affine/i18n';
|
import { Trans } from '@affine/i18n';
|
||||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
@@ -122,6 +123,11 @@ export const AIOnboardingGeneral = ({
|
|||||||
activeTab: 'plans',
|
activeTab: 'plans',
|
||||||
scrollAnchor: 'aiPricingPlan',
|
scrollAnchor: 'aiPricingPlan',
|
||||||
});
|
});
|
||||||
|
mixpanel.track('PlansViewed', {
|
||||||
|
page: 'whiteboard-editor',
|
||||||
|
segment: 'ai onboarding',
|
||||||
|
module: 'general',
|
||||||
|
});
|
||||||
closeAndDismiss();
|
closeAndDismiss();
|
||||||
}, [closeAndDismiss, setSettingModal]);
|
}, [closeAndDismiss, setSettingModal]);
|
||||||
const onPrev = useCallback(() => {
|
const onPrev = useCallback(() => {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Tooltip } from '@affine/component/ui/tooltip';
|
import { Tooltip } from '@affine/component/ui/tooltip';
|
||||||
|
import { mixpanel } from '@affine/core/utils';
|
||||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
import { useLiveData, useServices } from '@toeverything/infra';
|
import { useLiveData, useServices } from '@toeverything/infra';
|
||||||
import { useSetAtom } from 'jotai';
|
import { useSetAtom } from 'jotai';
|
||||||
@@ -40,6 +41,10 @@ export const UserPlanButton = () => {
|
|||||||
open: true,
|
open: true,
|
||||||
activeTab: 'plans',
|
activeTab: 'plans',
|
||||||
});
|
});
|
||||||
|
mixpanel.track('PlansViewed', {
|
||||||
|
segment: 'settings panel',
|
||||||
|
module: 'profile and badge',
|
||||||
|
});
|
||||||
},
|
},
|
||||||
[setSettingModalAtom]
|
[setSettingModalAtom]
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -225,6 +225,9 @@ const PlanPrompt = () => {
|
|||||||
open: true,
|
open: true,
|
||||||
activeTab: 'plans',
|
activeTab: 'plans',
|
||||||
});
|
});
|
||||||
|
mixpanel.track('PlansViewed', {
|
||||||
|
segment: 'doc history',
|
||||||
|
});
|
||||||
}, [setSettingModalAtom]);
|
}, [setSettingModalAtom]);
|
||||||
|
|
||||||
const t = useAFFiNEI18N();
|
const t = useAFFiNEI18N();
|
||||||
@@ -233,7 +236,7 @@ const PlanPrompt = () => {
|
|||||||
return (
|
return (
|
||||||
<div className={styles.planPromptTitle}>
|
<div className={styles.planPromptTitle}>
|
||||||
{
|
{
|
||||||
isProWorkspace === null
|
isProWorkspace !== null
|
||||||
? !isProWorkspace
|
? !isProWorkspace
|
||||||
? t[
|
? t[
|
||||||
'com.affine.history.confirm-restore-modal.plan-prompt.limited-title'
|
'com.affine.history.confirm-restore-modal.plan-prompt.limited-title'
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { openQuotaModalAtom, openSettingModalAtom } from '@affine/core/atoms';
|
|||||||
import { UserQuotaService } from '@affine/core/modules/cloud';
|
import { UserQuotaService } from '@affine/core/modules/cloud';
|
||||||
import { WorkspacePermissionService } from '@affine/core/modules/permissions';
|
import { WorkspacePermissionService } from '@affine/core/modules/permissions';
|
||||||
import { WorkspaceQuotaService } from '@affine/core/modules/quota';
|
import { WorkspaceQuotaService } from '@affine/core/modules/quota';
|
||||||
|
import { mixpanel } from '@affine/core/utils';
|
||||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
import { useLiveData, useService, WorkspaceService } from '@toeverything/infra';
|
import { useLiveData, useService, WorkspaceService } from '@toeverything/infra';
|
||||||
import bytes from 'bytes';
|
import bytes from 'bytes';
|
||||||
@@ -48,6 +49,11 @@ export const CloudQuotaModal = () => {
|
|||||||
activeTab: 'plans',
|
activeTab: 'plans',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
mixpanel.track('PlansViewed', {
|
||||||
|
segment: 'payment wall',
|
||||||
|
category: 'payment wall storage',
|
||||||
|
});
|
||||||
|
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
}, [setOpen, setSettingModalAtom]);
|
}, [setOpen, setSettingModalAtom]);
|
||||||
|
|
||||||
|
|||||||
+7
@@ -6,6 +6,7 @@ import {
|
|||||||
SubscriptionService,
|
SubscriptionService,
|
||||||
UserCopilotQuotaService,
|
UserCopilotQuotaService,
|
||||||
} from '@affine/core/modules/cloud';
|
} from '@affine/core/modules/cloud';
|
||||||
|
import { mixpanel } from '@affine/core/utils';
|
||||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
import { useLiveData, useService } from '@toeverything/infra';
|
import { useLiveData, useService } from '@toeverything/infra';
|
||||||
import { cssVar } from '@toeverything/theme';
|
import { cssVar } from '@toeverything/theme';
|
||||||
@@ -46,6 +47,12 @@ export const AIUsagePanel = () => {
|
|||||||
open: true,
|
open: true,
|
||||||
activeTab: 'billing',
|
activeTab: 'billing',
|
||||||
});
|
});
|
||||||
|
mixpanel.track('BillingViewed', {
|
||||||
|
segment: 'settings panel',
|
||||||
|
module: 'account usage list',
|
||||||
|
control: 'change plan button',
|
||||||
|
type: 'ai subscription',
|
||||||
|
});
|
||||||
}, [setOpenSettingModal]);
|
}, [setOpenSettingModal]);
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
|
|||||||
+5
-2
@@ -162,8 +162,11 @@ const StoragePanel = () => {
|
|||||||
|
|
||||||
const setSettingModalAtom = useSetAtom(openSettingModalAtom);
|
const setSettingModalAtom = useSetAtom(openSettingModalAtom);
|
||||||
const onUpgrade = useCallback(() => {
|
const onUpgrade = useCallback(() => {
|
||||||
mixpanel.track('Button', {
|
mixpanel.track('PlansViewed', {
|
||||||
resolve: 'UpgradeStorage',
|
segment: 'settings panel',
|
||||||
|
module: 'account usage list',
|
||||||
|
control: 'cloud storage upgrade button',
|
||||||
|
type: 'cloud subscription',
|
||||||
});
|
});
|
||||||
setSettingModalAtom({
|
setSettingModalAtom({
|
||||||
open: true,
|
open: true,
|
||||||
|
|||||||
+9
-4
@@ -108,17 +108,22 @@ const SubscriptionSettings = () => {
|
|||||||
|
|
||||||
const openPlans = useCallback(
|
const openPlans = useCallback(
|
||||||
(scrollAnchor?: string) => {
|
(scrollAnchor?: string) => {
|
||||||
mixpanel.track('Button', {
|
mixpanel.track('PlansViewed', {
|
||||||
resolve: 'ChangePlan',
|
type: proSubscription?.plan,
|
||||||
currentPlan: proSubscription?.plan,
|
category: proSubscription?.recurring,
|
||||||
|
// page:
|
||||||
|
segment: 'settings panel',
|
||||||
|
module: 'billing subscription list',
|
||||||
|
control: 'change plan button',
|
||||||
});
|
});
|
||||||
|
|
||||||
setOpenSettingModalAtom({
|
setOpenSettingModalAtom({
|
||||||
open: true,
|
open: true,
|
||||||
activeTab: 'plans',
|
activeTab: 'plans',
|
||||||
scrollAnchor: scrollAnchor,
|
scrollAnchor: scrollAnchor,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[proSubscription?.plan, setOpenSettingModalAtom]
|
[proSubscription?.plan, proSubscription?.recurring, setOpenSettingModalAtom]
|
||||||
);
|
);
|
||||||
const gotoCloudPlansSetting = useCallback(() => openPlans(), [openPlans]);
|
const gotoCloudPlansSetting = useCallback(() => openPlans(), [openPlans]);
|
||||||
const gotoAiPlanSetting = useCallback(
|
const gotoAiPlanSetting = useCallback(
|
||||||
|
|||||||
+5
-1
@@ -1,7 +1,7 @@
|
|||||||
import { Button, type ButtonProps } from '@affine/component';
|
import { Button, type ButtonProps } from '@affine/component';
|
||||||
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
|
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
|
||||||
import { SubscriptionService } from '@affine/core/modules/cloud';
|
import { SubscriptionService } from '@affine/core/modules/cloud';
|
||||||
import { popupWindow } from '@affine/core/utils';
|
import { mixpanel, popupWindow } from '@affine/core/utils';
|
||||||
import { SubscriptionPlan, SubscriptionRecurring } from '@affine/graphql';
|
import { SubscriptionPlan, SubscriptionRecurring } from '@affine/graphql';
|
||||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
import { useLiveData, useService } from '@toeverything/infra';
|
import { useLiveData, useService } from '@toeverything/infra';
|
||||||
@@ -42,6 +42,10 @@ export const AISubscribe = ({ ...btnProps }: AISubscribeProps) => {
|
|||||||
|
|
||||||
const subscribe = useAsyncCallback(async () => {
|
const subscribe = useAsyncCallback(async () => {
|
||||||
setMutating(true);
|
setMutating(true);
|
||||||
|
mixpanel.track('plan upgrade started', {
|
||||||
|
category: SubscriptionRecurring.Yearly,
|
||||||
|
type: SubscriptionPlan.AI,
|
||||||
|
});
|
||||||
try {
|
try {
|
||||||
const session = await subscriptionService.createCheckoutSession({
|
const session = await subscriptionService.createCheckoutSession({
|
||||||
recurring: SubscriptionRecurring.Yearly,
|
recurring: SubscriptionRecurring.Yearly,
|
||||||
|
|||||||
+26
-7
@@ -115,15 +115,22 @@ export const SettingSidebar = ({
|
|||||||
const loginStatus = useLiveData(useService(AuthService).session.status$);
|
const loginStatus = useLiveData(useService(AuthService).session.status$);
|
||||||
const generalList = useGeneralSettingList();
|
const generalList = useGeneralSettingList();
|
||||||
const onAccountSettingClick = useCallback(() => {
|
const onAccountSettingClick = useCallback(() => {
|
||||||
mixpanel.track('Button', {
|
mixpanel.track('AccountSettingsViewed', {
|
||||||
resolve: 'AccountSetting',
|
// page:
|
||||||
|
segment: 'settings panel',
|
||||||
|
module: 'settings menu',
|
||||||
|
control: 'menu item',
|
||||||
});
|
});
|
||||||
onTabChange('account', null);
|
onTabChange('account', null);
|
||||||
}, [onTabChange]);
|
}, [onTabChange]);
|
||||||
const onWorkspaceSettingClick = useCallback(
|
const onWorkspaceSettingClick = useCallback(
|
||||||
(subTab: WorkspaceSubTab, workspaceMetadata: WorkspaceMetadata) => {
|
(subTab: WorkspaceSubTab, workspaceMetadata: WorkspaceMetadata) => {
|
||||||
mixpanel.track('Button', {
|
mixpanel.track(`view workspace setting`, {
|
||||||
resolve: 'WorkspaceSetting',
|
// page:
|
||||||
|
segment: 'settings panel',
|
||||||
|
module: 'settings menu',
|
||||||
|
control: 'menu item',
|
||||||
|
type: subTab,
|
||||||
workspaceId: workspaceMetadata.id,
|
workspaceId: workspaceMetadata.id,
|
||||||
});
|
});
|
||||||
onTabChange(`workspace:${subTab}`, workspaceMetadata);
|
onTabChange(`workspace:${subTab}`, workspaceMetadata);
|
||||||
@@ -148,9 +155,21 @@ export const SettingSidebar = ({
|
|||||||
key={key}
|
key={key}
|
||||||
title={title}
|
title={title}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
mixpanel.track('Button', {
|
if (key === 'billing') {
|
||||||
resolve: key,
|
mixpanel.track('BillingViewed', {
|
||||||
});
|
// page:
|
||||||
|
segment: 'settings panel',
|
||||||
|
module: 'settings menu',
|
||||||
|
control: 'menu item',
|
||||||
|
});
|
||||||
|
} else if (key === 'plans') {
|
||||||
|
mixpanel.track('PlansViewed', {
|
||||||
|
// page:
|
||||||
|
segment: 'settings panel',
|
||||||
|
module: 'settings menu',
|
||||||
|
control: 'menu item',
|
||||||
|
});
|
||||||
|
}
|
||||||
onTabChange(key, null);
|
onTabChange(key, null);
|
||||||
}}
|
}}
|
||||||
data-testid={testId}
|
data-testid={testId}
|
||||||
|
|||||||
+7
@@ -23,6 +23,7 @@ import { useMembers } from '@affine/core/hooks/affine/use-members';
|
|||||||
import { useRevokeMemberPermission } from '@affine/core/hooks/affine/use-revoke-member-permission';
|
import { useRevokeMemberPermission } from '@affine/core/hooks/affine/use-revoke-member-permission';
|
||||||
import { WorkspacePermissionService } from '@affine/core/modules/permissions';
|
import { WorkspacePermissionService } from '@affine/core/modules/permissions';
|
||||||
import { WorkspaceQuotaService } from '@affine/core/modules/quota';
|
import { WorkspaceQuotaService } from '@affine/core/modules/quota';
|
||||||
|
import { mixpanel } from '@affine/core/utils';
|
||||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||||
import { Permission } from '@affine/graphql';
|
import { Permission } from '@affine/graphql';
|
||||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
@@ -144,6 +145,12 @@ export const CloudWorkspaceMembersPanel = () => {
|
|||||||
open: true,
|
open: true,
|
||||||
activeTab: 'plans',
|
activeTab: 'plans',
|
||||||
});
|
});
|
||||||
|
mixpanel.track('PlansViewed', {
|
||||||
|
// page:
|
||||||
|
segment: 'settings panel',
|
||||||
|
module: 'workspace setting',
|
||||||
|
control: 'invite member',
|
||||||
|
});
|
||||||
}, [setSettingModalAtom]);
|
}, [setSettingModalAtom]);
|
||||||
|
|
||||||
const listContainerRef = useRef<HTMLDivElement | null>(null);
|
const listContainerRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
|||||||
+7
@@ -11,6 +11,7 @@ import { Button } from '@affine/component/ui/button';
|
|||||||
import { Menu, MenuItem, MenuTrigger } from '@affine/component/ui/menu';
|
import { Menu, MenuItem, MenuTrigger } from '@affine/component/ui/menu';
|
||||||
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
|
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
|
||||||
import { ShareService } from '@affine/core/modules/share-doc';
|
import { ShareService } from '@affine/core/modules/share-doc';
|
||||||
|
import { mixpanel } from '@affine/core/utils';
|
||||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||||
import { PublicPageMode } from '@affine/graphql';
|
import { PublicPageMode } from '@affine/graphql';
|
||||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
@@ -101,6 +102,12 @@ export const AffineSharePage = (props: ShareMenuProps) => {
|
|||||||
await shareService.share.enableShare(
|
await shareService.share.enableShare(
|
||||||
mode === 'edgeless' ? PublicPageMode.Edgeless : PublicPageMode.Page
|
mode === 'edgeless' ? PublicPageMode.Edgeless : PublicPageMode.Page
|
||||||
);
|
);
|
||||||
|
mixpanel.track('ShareCreated', {
|
||||||
|
segment: 'sharing panel',
|
||||||
|
module: 'public share',
|
||||||
|
control: 'share panel',
|
||||||
|
type: mode,
|
||||||
|
});
|
||||||
notify.success({
|
notify.success({
|
||||||
title:
|
title:
|
||||||
t[
|
t[
|
||||||
|
|||||||
+6
-1
@@ -1,5 +1,6 @@
|
|||||||
import { toast } from '@affine/component';
|
import { toast } from '@affine/component';
|
||||||
import { getAffineCloudBaseUrl } from '@affine/core/modules/cloud/services/fetch';
|
import { getAffineCloudBaseUrl } from '@affine/core/modules/cloud/services/fetch';
|
||||||
|
import { mixpanel } from '@affine/core/utils';
|
||||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
import { useCallback, useMemo } from 'react';
|
import { useCallback, useMemo } from 'react';
|
||||||
|
|
||||||
@@ -52,10 +53,14 @@ export const useSharingUrl = ({
|
|||||||
.catch(err => {
|
.catch(err => {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
});
|
});
|
||||||
|
mixpanel.track('ShareLinkCopied', {
|
||||||
|
module: urlType === 'share' ? 'public share' : 'private share',
|
||||||
|
type: 'link',
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
toast('Network not available');
|
toast('Network not available');
|
||||||
}
|
}
|
||||||
}, [sharingUrl, t]);
|
}, [sharingUrl, t, urlType]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
sharingUrl,
|
sharingUrl,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { notify } from '@affine/component';
|
import { notify } from '@affine/component';
|
||||||
import { authAtom, openSettingModalAtom } from '@affine/core/atoms';
|
import { authAtom, openSettingModalAtom } from '@affine/core/atoms';
|
||||||
|
import { mixpanel } from '@affine/core/utils';
|
||||||
import { getBaseUrl } from '@affine/graphql';
|
import { getBaseUrl } from '@affine/graphql';
|
||||||
import { Trans } from '@affine/i18n';
|
import { Trans } from '@affine/i18n';
|
||||||
import { UnauthorizedError } from '@blocksuite/blocks';
|
import { UnauthorizedError } from '@blocksuite/blocks';
|
||||||
@@ -345,6 +346,11 @@ Could you make a new website based on these notes and send back just the html fi
|
|||||||
getCurrentStore().set(openSettingModalAtom, {
|
getCurrentStore().set(openSettingModalAtom, {
|
||||||
activeTab: 'billing',
|
activeTab: 'billing',
|
||||||
open: true,
|
open: true,
|
||||||
|
scrollAnchor: 'aiPricingPlan',
|
||||||
|
});
|
||||||
|
mixpanel.track('PlansViewed', {
|
||||||
|
segment: 'payment wall',
|
||||||
|
category: 'payment wall ai action count',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ import { Export, MoveToTrash } from '@affine/core/components/page-list';
|
|||||||
import { useBlockSuiteMetaHelper } from '@affine/core/hooks/affine/use-block-suite-meta-helper';
|
import { useBlockSuiteMetaHelper } from '@affine/core/hooks/affine/use-block-suite-meta-helper';
|
||||||
import { useExportPage } from '@affine/core/hooks/affine/use-export-page';
|
import { useExportPage } from '@affine/core/hooks/affine/use-export-page';
|
||||||
import { useTrashModalHelper } from '@affine/core/hooks/affine/use-trash-modal-helper';
|
import { useTrashModalHelper } from '@affine/core/hooks/affine/use-trash-modal-helper';
|
||||||
|
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
|
||||||
|
import { mixpanel } from '@affine/core/utils';
|
||||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
import {
|
import {
|
||||||
@@ -97,8 +99,34 @@ export const PageHeaderMenuButton = ({
|
|||||||
|
|
||||||
const handleDuplicate = useCallback(() => {
|
const handleDuplicate = useCallback(() => {
|
||||||
duplicate(pageId);
|
duplicate(pageId);
|
||||||
|
mixpanel.track('DocCreated', {
|
||||||
|
segment: 'editor header',
|
||||||
|
module: 'header menu',
|
||||||
|
control: 'copy doc',
|
||||||
|
type: 'doc duplicate',
|
||||||
|
category: 'doc',
|
||||||
|
});
|
||||||
}, [duplicate, pageId]);
|
}, [duplicate, pageId]);
|
||||||
|
|
||||||
|
const onImportFile = useAsyncCallback(async () => {
|
||||||
|
const options = await importFile();
|
||||||
|
if (options.isWorkspaceFile) {
|
||||||
|
mixpanel.track('WorkspaceCreated', {
|
||||||
|
segment: 'editor header',
|
||||||
|
module: 'header menu',
|
||||||
|
control: 'import button',
|
||||||
|
type: 'imported workspace',
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
mixpanel.track('DocCreated', {
|
||||||
|
segment: 'editor header',
|
||||||
|
module: 'header menu',
|
||||||
|
control: 'import button',
|
||||||
|
type: 'imported doc',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [importFile]);
|
||||||
|
|
||||||
const EditMenu = (
|
const EditMenu = (
|
||||||
<>
|
<>
|
||||||
{!isJournal && (
|
{!isJournal && (
|
||||||
@@ -179,7 +207,7 @@ export const PageHeaderMenuButton = ({
|
|||||||
</MenuIcon>
|
</MenuIcon>
|
||||||
}
|
}
|
||||||
data-testid="editor-option-menu-import"
|
data-testid="editor-option-menu-import"
|
||||||
onSelect={importFile}
|
onSelect={onImportFile}
|
||||||
style={menuItemStyle}
|
style={menuItemStyle}
|
||||||
>
|
>
|
||||||
{t['Import']()}
|
{t['Import']()}
|
||||||
|
|||||||
@@ -36,30 +36,47 @@ export const usePageHelper = (docCollection: DocCollection) => {
|
|||||||
return createPageAndOpen('edgeless');
|
return createPageAndOpen('edgeless');
|
||||||
}, [createPageAndOpen]);
|
}, [createPageAndOpen]);
|
||||||
|
|
||||||
const importFileAndOpen = useAsyncCallback(async () => {
|
const importFileAndOpen = useMemo(
|
||||||
const { showImportModal } = await import('@blocksuite/blocks');
|
() => async () => {
|
||||||
const onSuccess = (
|
const { showImportModal } = await import('@blocksuite/blocks');
|
||||||
pageIds: string[],
|
const { promise, resolve, reject } =
|
||||||
options: { isWorkspaceFile: boolean; importedCount: number }
|
Promise.withResolvers<
|
||||||
) => {
|
Parameters<
|
||||||
toast(
|
NonNullable<Parameters<typeof showImportModal>[0]['onSuccess']>
|
||||||
`Successfully imported ${options.importedCount} Page${
|
>[1]
|
||||||
options.importedCount > 1 ? 's' : ''
|
>();
|
||||||
}.`
|
const onSuccess = (
|
||||||
);
|
pageIds: string[],
|
||||||
if (options.isWorkspaceFile) {
|
options: { isWorkspaceFile: boolean; importedCount: number }
|
||||||
jumpToSubPath(docCollection.id, WorkspaceSubPath.ALL);
|
) => {
|
||||||
return;
|
resolve(options);
|
||||||
}
|
toast(
|
||||||
|
`Successfully imported ${options.importedCount} Page${
|
||||||
|
options.importedCount > 1 ? 's' : ''
|
||||||
|
}.`
|
||||||
|
);
|
||||||
|
if (options.isWorkspaceFile) {
|
||||||
|
jumpToSubPath(docCollection.id, WorkspaceSubPath.ALL);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (pageIds.length === 0) {
|
if (pageIds.length === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const pageId = pageIds[0];
|
const pageId = pageIds[0];
|
||||||
openPage(docCollection.id, pageId);
|
openPage(docCollection.id, pageId);
|
||||||
};
|
};
|
||||||
showImportModal({ collection: docCollection, onSuccess });
|
showImportModal({
|
||||||
}, [docCollection, openPage, jumpToSubPath]);
|
collection: docCollection,
|
||||||
|
onSuccess,
|
||||||
|
onFail: message => {
|
||||||
|
reject(new Error(message));
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return await promise;
|
||||||
|
},
|
||||||
|
[docCollection, openPage, jumpToSubPath]
|
||||||
|
);
|
||||||
|
|
||||||
const createLinkedPageAndOpen = useAsyncCallback(
|
const createLinkedPageAndOpen = useAsyncCallback(
|
||||||
async (pageId: string) => {
|
async (pageId: string) => {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { DropdownButton, Menu } from '@affine/component';
|
import { DropdownButton, Menu } from '@affine/component';
|
||||||
import { BlockCard } from '@affine/component/card/block-card';
|
import { BlockCard } from '@affine/component/card/block-card';
|
||||||
|
import { mixpanel } from '@affine/core/utils';
|
||||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
import { EdgelessIcon, ImportIcon, PageIcon } from '@blocksuite/icons';
|
import { EdgelessIcon, ImportIcon, PageIcon } from '@blocksuite/icons';
|
||||||
import type { PropsWithChildren } from 'react';
|
import type { PropsWithChildren } from 'react';
|
||||||
@@ -69,11 +70,27 @@ export const NewPageButton = ({
|
|||||||
const handleCreateNewPage = useCallback(() => {
|
const handleCreateNewPage = useCallback(() => {
|
||||||
createNewPage();
|
createNewPage();
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
|
mixpanel.track('DocCreated', {
|
||||||
|
page: 'doc library',
|
||||||
|
segment: 'all doc',
|
||||||
|
module: 'doc list header',
|
||||||
|
control: 'new doc button',
|
||||||
|
type: 'doc',
|
||||||
|
category: 'page',
|
||||||
|
});
|
||||||
}, [createNewPage]);
|
}, [createNewPage]);
|
||||||
|
|
||||||
const handleCreateNewEdgeless = useCallback(() => {
|
const handleCreateNewEdgeless = useCallback(() => {
|
||||||
createNewEdgeless();
|
createNewEdgeless();
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
|
mixpanel.track('DocCreated', {
|
||||||
|
page: 'doc library',
|
||||||
|
segment: 'all doc',
|
||||||
|
module: 'doc list header',
|
||||||
|
control: 'new whiteboard button',
|
||||||
|
type: 'doc',
|
||||||
|
category: 'whiteboard',
|
||||||
|
});
|
||||||
}, [createNewEdgeless]);
|
}, [createNewEdgeless]);
|
||||||
|
|
||||||
const handleImportFile = useCallback(() => {
|
const handleImportFile = useCallback(() => {
|
||||||
@@ -104,10 +121,7 @@ export const NewPageButton = ({
|
|||||||
>
|
>
|
||||||
<DropdownButton
|
<DropdownButton
|
||||||
size={size}
|
size={size}
|
||||||
onClick={useCallback(() => {
|
onClick={handleCreateNewPage}
|
||||||
createNewPage();
|
|
||||||
setOpen(false);
|
|
||||||
}, [createNewPage])}
|
|
||||||
onClickDropDown={useCallback(() => setOpen(open => !open), [])}
|
onClickDropDown={useCallback(() => setOpen(open => !open), [])}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
|
|||||||
import { useNavigateHelper } from '@affine/core/hooks/use-navigate-helper';
|
import { useNavigateHelper } from '@affine/core/hooks/use-navigate-helper';
|
||||||
import type { Tag } from '@affine/core/modules/tag';
|
import type { Tag } from '@affine/core/modules/tag';
|
||||||
import { TagService } from '@affine/core/modules/tag';
|
import { TagService } from '@affine/core/modules/tag';
|
||||||
|
import { mixpanel } from '@affine/core/utils';
|
||||||
import type { Collection } from '@affine/env/filter';
|
import type { Collection } from '@affine/env/filter';
|
||||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
import {
|
import {
|
||||||
@@ -46,6 +47,28 @@ export const PageListHeader = () => {
|
|||||||
return t['com.affine.all-pages.header']();
|
return t['com.affine.all-pages.header']();
|
||||||
}, [t]);
|
}, [t]);
|
||||||
|
|
||||||
|
const onImportFile = useAsyncCallback(async () => {
|
||||||
|
const options = await importFile();
|
||||||
|
if (options.isWorkspaceFile) {
|
||||||
|
mixpanel.track('WorkspaceCreated', {
|
||||||
|
page: 'doc library',
|
||||||
|
segment: 'all doc',
|
||||||
|
module: 'doc list header',
|
||||||
|
control: 'import button',
|
||||||
|
type: 'imported workspace',
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
mixpanel.track('DocCreated', {
|
||||||
|
page: 'doc library',
|
||||||
|
segment: 'all doc',
|
||||||
|
module: 'doc list header',
|
||||||
|
control: 'import button',
|
||||||
|
type: 'imported doc',
|
||||||
|
// category
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [importFile]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.docListHeader}>
|
<div className={styles.docListHeader}>
|
||||||
<div className={styles.docListHeaderTitle}>{title}</div>
|
<div className={styles.docListHeaderTitle}>{title}</div>
|
||||||
@@ -54,7 +77,7 @@ export const PageListHeader = () => {
|
|||||||
testId="new-page-button-trigger"
|
testId="new-page-button-trigger"
|
||||||
onCreateEdgeless={createEdgeless}
|
onCreateEdgeless={createEdgeless}
|
||||||
onCreatePage={createPage}
|
onCreatePage={createPage}
|
||||||
onImportFile={importFile}
|
onImportFile={onImportFile}
|
||||||
>
|
>
|
||||||
<div className={styles.buttonText}>{t['New Page']()}</div>
|
<div className={styles.buttonText}>{t['New Page']()}</div>
|
||||||
</PageListNewPageButton>
|
</PageListNewPageButton>
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { useBlockSuiteMetaHelper } from '@affine/core/hooks/affine/use-block-sui
|
|||||||
import { useTrashModalHelper } from '@affine/core/hooks/affine/use-trash-modal-helper';
|
import { useTrashModalHelper } from '@affine/core/hooks/affine/use-trash-modal-helper';
|
||||||
import { FavoriteItemsAdapter } from '@affine/core/modules/properties';
|
import { FavoriteItemsAdapter } from '@affine/core/modules/properties';
|
||||||
import { WorkbenchService } from '@affine/core/modules/workbench';
|
import { WorkbenchService } from '@affine/core/modules/workbench';
|
||||||
|
import { mixpanel } from '@affine/core/utils';
|
||||||
import type { Collection, DeleteCollectionInfo } from '@affine/env/filter';
|
import type { Collection, DeleteCollectionInfo } from '@affine/env/filter';
|
||||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
import {
|
import {
|
||||||
@@ -96,6 +97,13 @@ export const PageOperationCell = ({
|
|||||||
|
|
||||||
const onDuplicate = useCallback(() => {
|
const onDuplicate = useCallback(() => {
|
||||||
duplicate(page.id, false);
|
duplicate(page.id, false);
|
||||||
|
mixpanel.track('DocCreated', {
|
||||||
|
segment: 'all doc',
|
||||||
|
module: 'doc item menu',
|
||||||
|
control: 'copy doc',
|
||||||
|
type: 'doc duplicate',
|
||||||
|
category: 'doc',
|
||||||
|
});
|
||||||
}, [duplicate, page.id]);
|
}, [duplicate, page.id]);
|
||||||
|
|
||||||
const OperationMenu = (
|
const OperationMenu = (
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { useGetDocCollectionPageTitle } from '@affine/core/hooks/use-block-suite
|
|||||||
import { useJournalHelper } from '@affine/core/hooks/use-journal';
|
import { useJournalHelper } from '@affine/core/hooks/use-journal';
|
||||||
import { CollectionService } from '@affine/core/modules/collection';
|
import { CollectionService } from '@affine/core/modules/collection';
|
||||||
import { WorkspaceSubPath } from '@affine/core/shared';
|
import { WorkspaceSubPath } from '@affine/core/shared';
|
||||||
|
import { mixpanel } from '@affine/core/utils';
|
||||||
import type { Collection } from '@affine/env/filter';
|
import type { Collection } from '@affine/env/filter';
|
||||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
import {
|
import {
|
||||||
@@ -235,6 +236,9 @@ export const usePageCommands = () => {
|
|||||||
page.id,
|
page.id,
|
||||||
blockId
|
blockId
|
||||||
);
|
);
|
||||||
|
mixpanel.track('AppendToJournal', {
|
||||||
|
control: 'cmdk',
|
||||||
|
});
|
||||||
},
|
},
|
||||||
icon: <TodayIcon />,
|
icon: <TodayIcon />,
|
||||||
});
|
});
|
||||||
@@ -250,6 +254,10 @@ export const usePageCommands = () => {
|
|||||||
const page = pageHelper.createPage();
|
const page = pageHelper.createPage();
|
||||||
page.load();
|
page.load();
|
||||||
pageMetaHelper.setDocTitle(page.id, query);
|
pageMetaHelper.setDocTitle(page.id, query);
|
||||||
|
mixpanel.track('DocCreated', {
|
||||||
|
control: 'cmdk',
|
||||||
|
type: 'doc',
|
||||||
|
});
|
||||||
},
|
},
|
||||||
icon: <PageIcon />,
|
icon: <PageIcon />,
|
||||||
});
|
});
|
||||||
@@ -265,6 +273,10 @@ export const usePageCommands = () => {
|
|||||||
const page = pageHelper.createEdgeless();
|
const page = pageHelper.createEdgeless();
|
||||||
page.load();
|
page.load();
|
||||||
pageMetaHelper.setDocTitle(page.id, query);
|
pageMetaHelper.setDocTitle(page.id, query);
|
||||||
|
mixpanel.track('DocCreated', {
|
||||||
|
control: 'cmdk',
|
||||||
|
type: 'whiteboard',
|
||||||
|
});
|
||||||
},
|
},
|
||||||
icon: <EdgelessIcon />,
|
icon: <EdgelessIcon />,
|
||||||
});
|
});
|
||||||
|
|||||||
+17
@@ -1,6 +1,7 @@
|
|||||||
import { IconButton } from '@affine/component/ui/button';
|
import { IconButton } from '@affine/component/ui/button';
|
||||||
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
|
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
|
||||||
import { FavoriteItemsAdapter } from '@affine/core/modules/properties';
|
import { FavoriteItemsAdapter } from '@affine/core/modules/properties';
|
||||||
|
import { mixpanel } from '@affine/core/utils';
|
||||||
import { PlusIcon } from '@blocksuite/icons';
|
import { PlusIcon } from '@blocksuite/icons';
|
||||||
import type { DocCollection } from '@blocksuite/store';
|
import type { DocCollection } from '@blocksuite/store';
|
||||||
import { useService } from '@toeverything/infra';
|
import { useService } from '@toeverything/infra';
|
||||||
@@ -24,10 +25,26 @@ export const AddFavouriteButton = ({
|
|||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
createLinkedPage(pageId);
|
createLinkedPage(pageId);
|
||||||
|
mixpanel.track('DocCreated', {
|
||||||
|
// page:
|
||||||
|
segment: 'all doc',
|
||||||
|
module: 'favorite',
|
||||||
|
control: 'new fav sub doc',
|
||||||
|
type: 'doc',
|
||||||
|
category: 'page',
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
const page = createPage();
|
const page = createPage();
|
||||||
page.load();
|
page.load();
|
||||||
favAdapter.set(page.id, 'doc', true);
|
favAdapter.set(page.id, 'doc', true);
|
||||||
|
mixpanel.track('DocCreated', {
|
||||||
|
// page:
|
||||||
|
segment: 'all doc',
|
||||||
|
module: 'favorite',
|
||||||
|
control: 'new fav doc',
|
||||||
|
type: 'doc',
|
||||||
|
category: 'page',
|
||||||
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[pageId, createLinkedPage, createPage, favAdapter]
|
[pageId, createLinkedPage, createPage, favAdapter]
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
|
||||||
|
import { mixpanel } from '@affine/core/utils';
|
||||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
import { ImportIcon } from '@blocksuite/icons';
|
import { ImportIcon } from '@blocksuite/icons';
|
||||||
|
|
||||||
@@ -8,8 +10,31 @@ import { usePageHelper } from '../blocksuite/block-suite-page-list/utils';
|
|||||||
const ImportPage = ({ docCollection }: { docCollection: DocCollection }) => {
|
const ImportPage = ({ docCollection }: { docCollection: DocCollection }) => {
|
||||||
const t = useAFFiNEI18N();
|
const t = useAFFiNEI18N();
|
||||||
const { importFile } = usePageHelper(docCollection);
|
const { importFile } = usePageHelper(docCollection);
|
||||||
|
|
||||||
|
const onImportFile = useAsyncCallback(async () => {
|
||||||
|
const options = await importFile();
|
||||||
|
if (options.isWorkspaceFile) {
|
||||||
|
mixpanel.track('WorkspaceCreated', {
|
||||||
|
page: 'doc library',
|
||||||
|
segment: 'navigation panel',
|
||||||
|
module: 'doc list header',
|
||||||
|
control: 'import button',
|
||||||
|
type: 'imported workspace',
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
mixpanel.track('DocCreated', {
|
||||||
|
page: 'doc library',
|
||||||
|
segment: 'navigation panel',
|
||||||
|
module: 'doc list header',
|
||||||
|
control: 'import button',
|
||||||
|
type: 'imported doc',
|
||||||
|
// category
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [importFile]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MenuItem icon={<ImportIcon />} onClick={importFile}>
|
<MenuItem icon={<ImportIcon />} onClick={onImportFile}>
|
||||||
{t['Import']()}
|
{t['Import']()}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { AnimatedDeleteIcon } from '@affine/component';
|
|||||||
import { getDNDId } from '@affine/core/hooks/affine/use-global-dnd-helper';
|
import { getDNDId } from '@affine/core/hooks/affine/use-global-dnd-helper';
|
||||||
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
|
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
|
||||||
import { CollectionService } from '@affine/core/modules/collection';
|
import { CollectionService } from '@affine/core/modules/collection';
|
||||||
|
import { mixpanel } from '@affine/core/utils';
|
||||||
import { apis, events } from '@affine/electron-api';
|
import { apis, events } from '@affine/electron-api';
|
||||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
import { FolderIcon, SettingsIcon } from '@blocksuite/icons';
|
import { FolderIcon, SettingsIcon } from '@blocksuite/icons';
|
||||||
@@ -106,11 +107,23 @@ export const RootAppSidebar = ({
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const allPageActive = currentPath === '/all';
|
||||||
|
|
||||||
|
const trashActive = currentPath === '/trash';
|
||||||
|
|
||||||
const onClickNewPage = useAsyncCallback(async () => {
|
const onClickNewPage = useAsyncCallback(async () => {
|
||||||
const page = createPage();
|
const page = createPage();
|
||||||
page.load();
|
page.load();
|
||||||
openPage(page.id);
|
openPage(page.id);
|
||||||
}, [createPage, openPage]);
|
mixpanel.track('DocCreated', {
|
||||||
|
page: allPageActive ? 'all' : trashActive ? 'trash' : 'other',
|
||||||
|
segment: 'navigation panel',
|
||||||
|
module: 'bottom button',
|
||||||
|
control: 'new doc button',
|
||||||
|
category: 'page',
|
||||||
|
type: 'doc',
|
||||||
|
});
|
||||||
|
}, [allPageActive, createPage, openPage, trashActive]);
|
||||||
|
|
||||||
const { trashModal, setTrashModal, handleOnConfirm } =
|
const { trashModal, setTrashModal, handleOnConfirm } =
|
||||||
useTrashModalHelper(docCollection);
|
useTrashModalHelper(docCollection);
|
||||||
@@ -166,10 +179,6 @@ export const RootAppSidebar = ({
|
|||||||
});
|
});
|
||||||
}, [docCollection.id, collection, navigateHelper, open]);
|
}, [docCollection.id, collection, navigateHelper, open]);
|
||||||
|
|
||||||
const allPageActive = currentPath === '/all';
|
|
||||||
|
|
||||||
const trashActive = currentPath === '/trash';
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AppSidebar
|
<AppSidebar
|
||||||
clientBorder={appSettings.clientBorder}
|
clientBorder={appSettings.clientBorder}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
openSettingModalAtom,
|
openSettingModalAtom,
|
||||||
openSignOutModalAtom,
|
openSignOutModalAtom,
|
||||||
} from '@affine/core/atoms';
|
} from '@affine/core/atoms';
|
||||||
|
import { mixpanel } from '@affine/core/utils';
|
||||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
import { AccountIcon, SignOutIcon } from '@blocksuite/icons';
|
import { AccountIcon, SignOutIcon } from '@blocksuite/icons';
|
||||||
import { useLiveData, useService } from '@toeverything/infra';
|
import { useLiveData, useService } from '@toeverything/infra';
|
||||||
@@ -79,6 +80,12 @@ const AccountMenu = () => {
|
|||||||
const setOpenSignOutModalAtom = useSetAtom(openSignOutModalAtom);
|
const setOpenSignOutModalAtom = useSetAtom(openSignOutModalAtom);
|
||||||
|
|
||||||
const onOpenAccountSetting = useCallback(() => {
|
const onOpenAccountSetting = useCallback(() => {
|
||||||
|
mixpanel.track('AccountSettingsViewed', {
|
||||||
|
// page:
|
||||||
|
segment: 'navigation panel',
|
||||||
|
module: 'profile and badge',
|
||||||
|
control: 'profile and email',
|
||||||
|
});
|
||||||
setSettingModalAtom(prev => ({
|
setSettingModalAtom(prev => ({
|
||||||
...prev,
|
...prev,
|
||||||
open: true,
|
open: true,
|
||||||
|
|||||||
@@ -23,10 +23,12 @@ export const AppContainer = ({
|
|||||||
useNoisyBackground,
|
useNoisyBackground,
|
||||||
useBlurBackground,
|
useBlurBackground,
|
||||||
children,
|
children,
|
||||||
|
...rest
|
||||||
}: WorkspaceRootProps) => {
|
}: WorkspaceRootProps) => {
|
||||||
const noisyBackground = useNoisyBackground && environment.isDesktop;
|
const noisyBackground = useNoisyBackground && environment.isDesktop;
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
{...rest}
|
||||||
className={clsx(appStyle, {
|
className={clsx(appStyle, {
|
||||||
'noisy-background': noisyBackground,
|
'noisy-background': noisyBackground,
|
||||||
'blur-background': environment.isDesktop && useBlurBackground,
|
'blur-background': environment.isDesktop && useBlurBackground,
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import {
|
|||||||
pushGlobalLoadingEventAtom,
|
pushGlobalLoadingEventAtom,
|
||||||
resolveGlobalLoadingEventAtom,
|
resolveGlobalLoadingEventAtom,
|
||||||
} from '@affine/component/global-loading';
|
} from '@affine/component/global-loading';
|
||||||
|
import { mixpanel } from '@affine/core/utils';
|
||||||
import { apis } from '@affine/electron-api';
|
import { apis } from '@affine/electron-api';
|
||||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
import type { PageRootService, RootBlockModel } from '@blocksuite/blocks';
|
import type { PageRootService, RootBlockModel } from '@blocksuite/blocks';
|
||||||
@@ -25,6 +26,11 @@ async function exportHandler({ page, type }: ExportHandlerOptions) {
|
|||||||
if (editorRoot) {
|
if (editorRoot) {
|
||||||
pageService = editorRoot.spec.getService<PageRootService>('affine:page');
|
pageService = editorRoot.spec.getService<PageRootService>('affine:page');
|
||||||
}
|
}
|
||||||
|
mixpanel.track('ShareCreated', {
|
||||||
|
type,
|
||||||
|
segment: 'sharing panel',
|
||||||
|
module: 'export share',
|
||||||
|
});
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'html':
|
case 'html':
|
||||||
await HtmlTransformer.exportDoc(page);
|
await HtmlTransformer.exportDoc(page);
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { toast } from '@affine/component';
|
import { toast } from '@affine/component';
|
||||||
import { useDocMetaHelper } from '@affine/core/hooks/use-block-suite-page-meta';
|
import { useDocMetaHelper } from '@affine/core/hooks/use-block-suite-page-meta';
|
||||||
import { FavoriteItemsAdapter } from '@affine/core/modules/properties';
|
import { FavoriteItemsAdapter } from '@affine/core/modules/properties';
|
||||||
|
import { mixpanel } from '@affine/core/utils';
|
||||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
import { assertExists } from '@blocksuite/global/utils';
|
import { assertExists } from '@blocksuite/global/utils';
|
||||||
@@ -141,6 +142,11 @@ export function useRegisterBlocksuiteEditorCommands() {
|
|||||||
label: t['com.affine.header.option.duplicate'](),
|
label: t['com.affine.header.option.duplicate'](),
|
||||||
run() {
|
run() {
|
||||||
duplicate(docId);
|
duplicate(docId);
|
||||||
|
mixpanel.track('DocCreated', {
|
||||||
|
control: 'cmdk',
|
||||||
|
type: 'doc duplicate',
|
||||||
|
category: 'doc',
|
||||||
|
});
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ import {
|
|||||||
} from '../hooks/affine/use-global-dnd-helper';
|
} from '../hooks/affine/use-global-dnd-helper';
|
||||||
import { useNavigateHelper } from '../hooks/use-navigate-helper';
|
import { useNavigateHelper } from '../hooks/use-navigate-helper';
|
||||||
import { useRegisterWorkspaceCommands } from '../hooks/use-register-workspace-commands';
|
import { useRegisterWorkspaceCommands } from '../hooks/use-register-workspace-commands';
|
||||||
|
import { WorkbenchService } from '../modules/workbench';
|
||||||
import {
|
import {
|
||||||
AllWorkspaceModals,
|
AllWorkspaceModals,
|
||||||
CurrentWorkspaceModals,
|
CurrentWorkspaceModals,
|
||||||
@@ -62,7 +63,6 @@ export const QuickSearch = () => {
|
|||||||
|
|
||||||
const onToggleQuickSearch = useCallback(
|
const onToggleQuickSearch = useCallback(
|
||||||
(open: boolean) => {
|
(open: boolean) => {
|
||||||
mixpanel.track('QuickSearch', { open });
|
|
||||||
setOpenQuickSearchModalAtom(open);
|
setOpenQuickSearchModalAtom(open);
|
||||||
},
|
},
|
||||||
[setOpenQuickSearchModalAtom]
|
[setOpenQuickSearchModalAtom]
|
||||||
@@ -113,6 +113,14 @@ export const WorkspaceLayoutInner = ({ children }: PropsWithChildren) => {
|
|||||||
const upgrading = useLiveData(currentWorkspace.upgrade.upgrading$);
|
const upgrading = useLiveData(currentWorkspace.upgrade.upgrading$);
|
||||||
const needUpgrade = useLiveData(currentWorkspace.upgrade.needUpgrade$);
|
const needUpgrade = useLiveData(currentWorkspace.upgrade.needUpgrade$);
|
||||||
|
|
||||||
|
const workbench = useService(WorkbenchService).workbench;
|
||||||
|
|
||||||
|
const basename = useLiveData(workbench.basename$);
|
||||||
|
|
||||||
|
const currentPath = useLiveData(
|
||||||
|
workbench.location$.map(location => basename + location.pathname)
|
||||||
|
);
|
||||||
|
|
||||||
useRegisterWorkspaceCommands();
|
useRegisterWorkspaceCommands();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -143,6 +151,10 @@ export const WorkspaceLayoutInner = ({ children }: PropsWithChildren) => {
|
|||||||
const [, setOpenQuickSearchModalAtom] = useAtom(openQuickSearchModalAtom);
|
const [, setOpenQuickSearchModalAtom] = useAtom(openQuickSearchModalAtom);
|
||||||
const handleOpenQuickSearchModal = useCallback(() => {
|
const handleOpenQuickSearchModal = useCallback(() => {
|
||||||
setOpenQuickSearchModalAtom(true);
|
setOpenQuickSearchModalAtom(true);
|
||||||
|
mixpanel.track('QuickSearchOpened', {
|
||||||
|
segment: 'navigation panel',
|
||||||
|
control: 'search button',
|
||||||
|
});
|
||||||
}, [setOpenQuickSearchModalAtom]);
|
}, [setOpenQuickSearchModalAtom]);
|
||||||
|
|
||||||
const setOpenSettingModalAtom = useSetAtom(openSettingModalAtom);
|
const setOpenSettingModalAtom = useSetAtom(openSettingModalAtom);
|
||||||
@@ -152,6 +164,12 @@ export const WorkspaceLayoutInner = ({ children }: PropsWithChildren) => {
|
|||||||
activeTab: 'appearance',
|
activeTab: 'appearance',
|
||||||
open: true,
|
open: true,
|
||||||
});
|
});
|
||||||
|
mixpanel.track('SettingsViewed', {
|
||||||
|
// page:
|
||||||
|
segment: 'navigation panel',
|
||||||
|
module: 'general list',
|
||||||
|
control: 'settings button',
|
||||||
|
});
|
||||||
}, [setOpenSettingModalAtom]);
|
}, [setOpenSettingModalAtom]);
|
||||||
|
|
||||||
const resizing = useAtomValue(appSidebarResizingAtom);
|
const resizing = useAtomValue(appSidebarResizingAtom);
|
||||||
@@ -171,7 +189,7 @@ export const WorkspaceLayoutInner = ({ children }: PropsWithChildren) => {
|
|||||||
<>
|
<>
|
||||||
{/* This DndContext is used for drag page from all-pages list into a folder in sidebar */}
|
{/* This DndContext is used for drag page from all-pages list into a folder in sidebar */}
|
||||||
<DndContext sensors={sensors} onDragEnd={handleDragEnd}>
|
<DndContext sensors={sensors} onDragEnd={handleDragEnd}>
|
||||||
<AppContainer resizing={resizing}>
|
<AppContainer data-current-path={currentPath} resizing={resizing}>
|
||||||
<Suspense fallback={<AppSidebarFallback />}>
|
<Suspense fallback={<AppSidebarFallback />}>
|
||||||
<RootAppSidebar
|
<RootAppSidebar
|
||||||
isPublicWorkspace={false}
|
isPublicWorkspace={false}
|
||||||
|
|||||||
@@ -28,6 +28,13 @@ export class TelemetryService extends Service {
|
|||||||
track_pageview: true,
|
track_pageview: true,
|
||||||
persistence: 'localStorage',
|
persistence: 'localStorage',
|
||||||
});
|
});
|
||||||
|
mixpanel.register({
|
||||||
|
appVersion: runtimeConfig.appVersion,
|
||||||
|
environment: runtimeConfig.appBuildType,
|
||||||
|
editorVersion: runtimeConfig.editorVersion,
|
||||||
|
isSelfHosted: Boolean(runtimeConfig.isSelfHosted),
|
||||||
|
isDesktop: environment.isDesktop,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
const account = this.auth.session.account$.value;
|
const account = this.auth.session.account$.value;
|
||||||
this.updateIdentity(account);
|
this.updateIdentity(account);
|
||||||
|
|||||||
@@ -1,7 +1,29 @@
|
|||||||
import { Service } from '@toeverything/infra';
|
import { mixpanel } from '@affine/core/utils';
|
||||||
|
import { createEvent, Service } from '@toeverything/infra';
|
||||||
|
import { combineLatest, distinctUntilChanged, map, skip } from 'rxjs';
|
||||||
|
|
||||||
import { Workbench } from '../entities/workbench';
|
import { Workbench } from '../entities/workbench';
|
||||||
|
|
||||||
|
export const WorkbenchLocationChanged = createEvent<string>(
|
||||||
|
'WorkbenchLocationChanged'
|
||||||
|
);
|
||||||
|
|
||||||
export class WorkbenchService extends Service {
|
export class WorkbenchService extends Service {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
combineLatest([this.workbench.location$, this.workbench.basename$])
|
||||||
|
.pipe(
|
||||||
|
map(([location, basename]) => basename + location.pathname),
|
||||||
|
distinctUntilChanged(),
|
||||||
|
skip(1)
|
||||||
|
)
|
||||||
|
.subscribe(newLocation => {
|
||||||
|
this.eventBus.root.emit(WorkbenchLocationChanged, newLocation);
|
||||||
|
mixpanel.track_pageview({
|
||||||
|
location: newLocation,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
workbench = this.framework.createEntity(Workbench);
|
workbench = this.framework.createEntity(Workbench);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { EMPTY, mergeMap, switchMap } from 'rxjs';
|
|||||||
|
|
||||||
import { RouteLogic, useNavigateHelper } from '../hooks/use-navigate-helper';
|
import { RouteLogic, useNavigateHelper } from '../hooks/use-navigate-helper';
|
||||||
import { AuthService, SubscriptionService } from '../modules/cloud';
|
import { AuthService, SubscriptionService } from '../modules/cloud';
|
||||||
|
import { mixpanel } from '../utils';
|
||||||
import { container } from './subscribe.css';
|
import { container } from './subscribe.css';
|
||||||
|
|
||||||
export const Component = () => {
|
export const Component = () => {
|
||||||
@@ -68,6 +69,16 @@ export const Component = () => {
|
|||||||
});
|
});
|
||||||
setMessage('Redirecting...');
|
setMessage('Redirecting...');
|
||||||
location.href = checkout;
|
location.href = checkout;
|
||||||
|
mixpanel.track('PlanChangeSucceeded', {
|
||||||
|
type: plan,
|
||||||
|
category: recurring,
|
||||||
|
});
|
||||||
|
if (plan) {
|
||||||
|
mixpanel.people.set({
|
||||||
|
[SubscriptionPlan.AI === plan ? 'ai plan' : plan]: plan,
|
||||||
|
recurring: recurring,
|
||||||
|
});
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
setError('Something went wrong. Please try again.');
|
setError('Something went wrong. Please try again.');
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import {
|
|||||||
} from '@affine/core/components/page-list';
|
} from '@affine/core/components/page-list';
|
||||||
import { Header } from '@affine/core/components/pure/header';
|
import { Header } from '@affine/core/components/pure/header';
|
||||||
import { WorkspaceModeFilterTab } from '@affine/core/components/pure/workspace-mode-filter-tab';
|
import { WorkspaceModeFilterTab } from '@affine/core/components/pure/workspace-mode-filter-tab';
|
||||||
|
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
|
||||||
|
import { mixpanel } from '@affine/core/utils';
|
||||||
import type { Filter } from '@affine/env/filter';
|
import type { Filter } from '@affine/env/filter';
|
||||||
import { PlusIcon } from '@blocksuite/icons';
|
import { PlusIcon } from '@blocksuite/icons';
|
||||||
import { useService, WorkspaceService } from '@toeverything/infra';
|
import { useService, WorkspaceService } from '@toeverything/infra';
|
||||||
@@ -27,6 +29,28 @@ export const AllPageHeader = ({
|
|||||||
workspace.docCollection
|
workspace.docCollection
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const onImportFile = useAsyncCallback(async () => {
|
||||||
|
const options = await importFile();
|
||||||
|
if (options.isWorkspaceFile) {
|
||||||
|
mixpanel.track('WorkspaceCreated', {
|
||||||
|
page: 'doc library',
|
||||||
|
segment: 'all page',
|
||||||
|
module: 'doc list header',
|
||||||
|
control: 'import button',
|
||||||
|
type: 'imported workspace',
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
mixpanel.track('DocCreated', {
|
||||||
|
page: 'doc library',
|
||||||
|
segment: 'all page',
|
||||||
|
module: 'doc list header',
|
||||||
|
control: 'import button',
|
||||||
|
type: 'imported doc',
|
||||||
|
// category
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [importFile]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Header
|
<Header
|
||||||
left={
|
left={
|
||||||
@@ -46,7 +70,7 @@ export const AllPageHeader = ({
|
|||||||
)}
|
)}
|
||||||
onCreateEdgeless={createEdgeless}
|
onCreateEdgeless={createEdgeless}
|
||||||
onCreatePage={createPage}
|
onCreatePage={createPage}
|
||||||
onImportFile={importFile}
|
onImportFile={onImportFile}
|
||||||
>
|
>
|
||||||
<PlusIcon />
|
<PlusIcon />
|
||||||
</PageListNewPageButton>
|
</PageListNewPageButton>
|
||||||
|
|||||||
@@ -5,17 +5,13 @@ import {
|
|||||||
createBrowserRouter as reactRouterCreateBrowserRouter,
|
createBrowserRouter as reactRouterCreateBrowserRouter,
|
||||||
Outlet,
|
Outlet,
|
||||||
redirect,
|
redirect,
|
||||||
useLocation,
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-restricted-imports
|
// eslint-disable-next-line @typescript-eslint/no-restricted-imports
|
||||||
useNavigate,
|
useNavigate,
|
||||||
} from 'react-router-dom';
|
} from 'react-router-dom';
|
||||||
|
|
||||||
import { mixpanel } from './utils';
|
|
||||||
|
|
||||||
export const NavigateContext = createContext<NavigateFunction | null>(null);
|
export const NavigateContext = createContext<NavigateFunction | null>(null);
|
||||||
|
|
||||||
function RootRouter() {
|
function RootRouter() {
|
||||||
const location = useLocation();
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [ready, setReady] = useState(false);
|
const [ready, setReady] = useState(false);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -23,16 +19,6 @@ function RootRouter() {
|
|||||||
setReady(true);
|
setReady(true);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
mixpanel.track_pageview({
|
|
||||||
page: location.pathname,
|
|
||||||
appVersion: runtimeConfig.appVersion,
|
|
||||||
environment: runtimeConfig.appBuildType,
|
|
||||||
editorVersion: runtimeConfig.editorVersion,
|
|
||||||
isSelfHosted: Boolean(runtimeConfig.isSelfHosted),
|
|
||||||
isDesktop: environment.isDesktop,
|
|
||||||
});
|
|
||||||
}, [location]);
|
|
||||||
return (
|
return (
|
||||||
ready && (
|
ready && (
|
||||||
<NavigateContext.Provider value={navigate}>
|
<NavigateContext.Provider value={navigate}>
|
||||||
|
|||||||
@@ -574,6 +574,7 @@
|
|||||||
"com.affine.cmdk.affine.navigation.goto-trash": "Go to Trash",
|
"com.affine.cmdk.affine.navigation.goto-trash": "Go to Trash",
|
||||||
"com.affine.cmdk.affine.navigation.goto-workspace": "Go to Workspace",
|
"com.affine.cmdk.affine.navigation.goto-workspace": "Go to Workspace",
|
||||||
"com.affine.cmdk.affine.navigation.open-settings": "Go to Settings",
|
"com.affine.cmdk.affine.navigation.open-settings": "Go to Settings",
|
||||||
|
"com.affine.cmdk.affine.navigation.open-account-settings": "Go to Account Settings",
|
||||||
"com.affine.cmdk.affine.new-edgeless-page": "New Edgeless",
|
"com.affine.cmdk.affine.new-edgeless-page": "New Edgeless",
|
||||||
"com.affine.cmdk.affine.new-page": "New Doc",
|
"com.affine.cmdk.affine.new-page": "New Doc",
|
||||||
"com.affine.cmdk.affine.new-workspace": "New Workspace",
|
"com.affine.cmdk.affine.new-workspace": "New Workspace",
|
||||||
|
|||||||
Reference in New Issue
Block a user