mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-13 21:05:19 +00:00
refactor(core): desktop project struct (#8334)
This commit is contained in:
@@ -4,17 +4,17 @@ import type { DocMode } from '@blocksuite/affine/blocks';
|
||||
import { ImportIcon, PlusIcon } from '@blocksuite/icons/rc';
|
||||
|
||||
import type { usePageHelper } from '../components/blocksuite/block-suite-page-list/utils';
|
||||
import type { CreateWorkspaceDialogService } from '../modules/create-workspace';
|
||||
import type { GlobalDialogService } from '../modules/dialogs';
|
||||
import { registerAffineCommand } from './registry';
|
||||
|
||||
export function registerAffineCreationCommands({
|
||||
pageHelper,
|
||||
t,
|
||||
createWorkspaceDialogService,
|
||||
globalDialogService,
|
||||
}: {
|
||||
t: ReturnType<typeof useI18n>;
|
||||
pageHelper: ReturnType<typeof usePageHelper>;
|
||||
createWorkspaceDialogService: CreateWorkspaceDialogService;
|
||||
globalDialogService: GlobalDialogService;
|
||||
}) {
|
||||
const unsubs: Array<() => void> = [];
|
||||
unsubs.push(
|
||||
@@ -62,7 +62,7 @@ export function registerAffineCreationCommands({
|
||||
run() {
|
||||
track.$.cmdk.workspace.createWorkspace();
|
||||
|
||||
createWorkspaceDialogService.dialog.open('new');
|
||||
globalDialogService.open('create-workspace', undefined);
|
||||
},
|
||||
})
|
||||
);
|
||||
@@ -80,7 +80,7 @@ export function registerAffineCreationCommands({
|
||||
control: 'import',
|
||||
});
|
||||
|
||||
createWorkspaceDialogService.dialog.open('add');
|
||||
globalDialogService.open('import-workspace', undefined);
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
@@ -1,20 +1,19 @@
|
||||
import type { useI18n } from '@affine/i18n';
|
||||
import { track } from '@affine/track';
|
||||
import { ContactWithUsIcon, NewIcon } from '@blocksuite/icons/rc';
|
||||
import type { createStore } from 'jotai';
|
||||
|
||||
import { openSettingModalAtom } from '../components/atoms';
|
||||
import type { GlobalDialogService } from '../modules/dialogs';
|
||||
import type { UrlService } from '../modules/url';
|
||||
import { registerAffineCommand } from './registry';
|
||||
|
||||
export function registerAffineHelpCommands({
|
||||
t,
|
||||
store,
|
||||
urlService,
|
||||
globalDialogService,
|
||||
}: {
|
||||
t: ReturnType<typeof useI18n>;
|
||||
store: ReturnType<typeof createStore>;
|
||||
urlService: UrlService;
|
||||
globalDialogService: GlobalDialogService;
|
||||
}) {
|
||||
const unsubs: Array<() => void> = [];
|
||||
unsubs.push(
|
||||
@@ -37,8 +36,7 @@ export function registerAffineHelpCommands({
|
||||
label: t['com.affine.cmdk.affine.contact-us'](),
|
||||
run() {
|
||||
track.$.cmdk.help.contactUs();
|
||||
store.set(openSettingModalAtom, {
|
||||
open: true,
|
||||
globalDialogService.open('setting', {
|
||||
activeTab: 'about',
|
||||
workspaceMetadata: null,
|
||||
});
|
||||
|
||||
@@ -4,11 +4,9 @@ import type { DocCollection } from '@blocksuite/affine/store';
|
||||
import { ArrowRightBigIcon } from '@blocksuite/icons/rc';
|
||||
import type { createStore } from 'jotai';
|
||||
|
||||
import {
|
||||
openSettingModalAtom,
|
||||
openWorkspaceListModalAtom,
|
||||
} from '../components/atoms';
|
||||
import { openWorkspaceListModalAtom } from '../components/atoms';
|
||||
import type { useNavigateHelper } from '../components/hooks/use-navigate-helper';
|
||||
import type { GlobalDialogService } from '../modules/dialogs';
|
||||
import { registerAffineCommand } from './registry';
|
||||
|
||||
export function registerAffineNavigationCommands({
|
||||
@@ -16,11 +14,13 @@ export function registerAffineNavigationCommands({
|
||||
store,
|
||||
docCollection,
|
||||
navigationHelper,
|
||||
globalDialogService,
|
||||
}: {
|
||||
t: ReturnType<typeof useI18n>;
|
||||
store: ReturnType<typeof createStore>;
|
||||
navigationHelper: ReturnType<typeof useNavigateHelper>;
|
||||
docCollection: DocCollection;
|
||||
globalDialogService: GlobalDialogService;
|
||||
}) {
|
||||
const unsubs: Array<() => void> = [];
|
||||
unsubs.push(
|
||||
@@ -96,10 +96,9 @@ export function registerAffineNavigationCommands({
|
||||
keyBinding: '$mod+,',
|
||||
run() {
|
||||
track.$.cmdk.settings.openSettings();
|
||||
store.set(openSettingModalAtom, s => ({
|
||||
globalDialogService.open('setting', {
|
||||
activeTab: 'appearance',
|
||||
open: !s.open,
|
||||
}));
|
||||
});
|
||||
},
|
||||
})
|
||||
);
|
||||
@@ -112,10 +111,9 @@ export function registerAffineNavigationCommands({
|
||||
label: t['com.affine.cmdk.affine.navigation.open-account-settings'](),
|
||||
run() {
|
||||
track.$.cmdk.settings.openSettings({ to: 'account' });
|
||||
store.set(openSettingModalAtom, s => ({
|
||||
globalDialogService.open('setting', {
|
||||
activeTab: 'account',
|
||||
open: !s.open,
|
||||
}));
|
||||
});
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import { Button, FlexWrapper, notify } from '@affine/component';
|
||||
import { openSettingModalAtom } from '@affine/core/components/atoms';
|
||||
import { SubscriptionService } from '@affine/core/modules/cloud';
|
||||
import { GlobalDialogService } from '@affine/core/modules/dialogs';
|
||||
import { EditorService } from '@affine/core/modules/editor';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import { track } from '@affine/track';
|
||||
import { AiIcon } from '@blocksuite/icons/rc';
|
||||
import { useLiveData, useServices } from '@toeverything/infra';
|
||||
import { useLiveData, useService, useServices } from '@toeverything/infra';
|
||||
import { cssVar } from '@toeverything/theme';
|
||||
import { useAtomValue, useSetAtom } from 'jotai';
|
||||
import Lottie from 'lottie-react';
|
||||
import { useTheme } from 'next-themes';
|
||||
import { useCallback, useEffect, useMemo, useRef } from 'react';
|
||||
@@ -51,24 +50,20 @@ export const AIOnboardingEdgeless = () => {
|
||||
const notifyId = useLiveData(edgelessNotifyId$);
|
||||
const generalAIOnboardingOpened = useLiveData(showAIOnboardingGeneral$);
|
||||
const aiSubscription = useLiveData(subscriptionService.subscription.ai$);
|
||||
const settingModalOpen = useAtomValue(openSettingModalAtom);
|
||||
const globalDialogService = useService(GlobalDialogService);
|
||||
const timeoutRef = useRef<ReturnType<typeof setTimeout>>();
|
||||
|
||||
const setSettingModal = useSetAtom(openSettingModalAtom);
|
||||
|
||||
const mode = useLiveData(editorService.editor.mode$);
|
||||
|
||||
const goToPricingPlans = useCallback(() => {
|
||||
track.$.aiOnboarding.dialog.viewPlans();
|
||||
setSettingModal({
|
||||
open: true,
|
||||
globalDialogService.open('setting', {
|
||||
activeTab: 'plans',
|
||||
scrollAnchor: 'aiPricingPlan',
|
||||
});
|
||||
}, [setSettingModal]);
|
||||
}, [globalDialogService]);
|
||||
|
||||
useEffect(() => {
|
||||
if (settingModalOpen.open) return;
|
||||
if (generalAIOnboardingOpened) return;
|
||||
if (notifyId) return;
|
||||
if (mode !== 'edgeless') return;
|
||||
@@ -128,7 +123,6 @@ export const AIOnboardingEdgeless = () => {
|
||||
goToPricingPlans,
|
||||
mode,
|
||||
notifyId,
|
||||
settingModalOpen,
|
||||
t,
|
||||
]);
|
||||
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import { Button, IconButton, Modal } from '@affine/component';
|
||||
import { openSettingModalAtom } from '@affine/core/components/atoms';
|
||||
import { useBlurRoot } from '@affine/core/components/hooks/use-blur-root';
|
||||
import { AuthService, SubscriptionService } from '@affine/core/modules/cloud';
|
||||
import { GlobalDialogService } from '@affine/core/modules/dialogs';
|
||||
import { Trans, useI18n } from '@affine/i18n';
|
||||
import { track } from '@affine/track';
|
||||
import { ArrowLeftSmallIcon } from '@blocksuite/icons/rc';
|
||||
import { useLiveData, useServices } from '@toeverything/infra';
|
||||
import { useAtom } from 'jotai';
|
||||
import { useLiveData, useService, useServices } from '@toeverything/infra';
|
||||
import type { ReactNode } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
|
||||
@@ -96,8 +95,8 @@ export const AIOnboardingGeneral = () => {
|
||||
const aiSubscription = useLiveData(subscriptionService.subscription.ai$);
|
||||
const [index, setIndex] = useState(0);
|
||||
const list = useMemo(() => getPlayList(t), [t]);
|
||||
const [settingModal, setSettingModal] = useAtom(openSettingModalAtom);
|
||||
const readyToOpen = isLoggedIn && !settingModal.open;
|
||||
const globalDialogService = useService(GlobalDialogService);
|
||||
const readyToOpen = isLoggedIn;
|
||||
useBlurRoot(open && readyToOpen);
|
||||
|
||||
const isFirst = index === 0;
|
||||
@@ -111,14 +110,13 @@ export const AIOnboardingGeneral = () => {
|
||||
toggleGeneralAIOnboarding(false);
|
||||
}, []);
|
||||
const goToPricingPlans = useCallback(() => {
|
||||
setSettingModal({
|
||||
open: true,
|
||||
globalDialogService.open('setting', {
|
||||
activeTab: 'plans',
|
||||
scrollAnchor: 'aiPricingPlan',
|
||||
});
|
||||
track.$.aiOnboarding.dialog.viewPlans();
|
||||
closeAndDismiss();
|
||||
}, [closeAndDismiss, setSettingModal]);
|
||||
}, [closeAndDismiss, globalDialogService]);
|
||||
const onPrev = useCallback(() => {
|
||||
setIndex(i => Math.max(0, i - 1));
|
||||
}, []);
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
import { style } from '@vanilla-extract/css';
|
||||
|
||||
export const electronFallback = style({
|
||||
paddingTop: 52,
|
||||
});
|
||||
@@ -1,59 +0,0 @@
|
||||
import {
|
||||
AppSidebarFallback,
|
||||
ShellAppSidebarFallback,
|
||||
} from '@affine/core/modules/app-sidebar/views';
|
||||
import clsx from 'clsx';
|
||||
import type { PropsWithChildren, ReactElement } from 'react';
|
||||
|
||||
import { useAppSettingHelper } from '../../components/hooks/affine/use-app-setting-helper';
|
||||
import type { WorkspaceRootProps } from '../workspace';
|
||||
import {
|
||||
AppContainer as AppContainerWithoutSettings,
|
||||
MainContainerFallback,
|
||||
} from '../workspace';
|
||||
import * as styles from './app-container.css';
|
||||
|
||||
export const AppContainer = (props: WorkspaceRootProps) => {
|
||||
const { appSettings } = useAppSettingHelper();
|
||||
|
||||
return (
|
||||
<AppContainerWithoutSettings
|
||||
useNoisyBackground={appSettings.enableNoisyBackground}
|
||||
useBlurBackground={appSettings.enableBlurBackground}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const AppFallback = ({
|
||||
className,
|
||||
children,
|
||||
}: PropsWithChildren<{
|
||||
className?: string;
|
||||
}>): ReactElement => {
|
||||
return (
|
||||
<AppContainer
|
||||
className={clsx(
|
||||
className,
|
||||
BUILD_CONFIG.isElectron && styles.electronFallback
|
||||
)}
|
||||
>
|
||||
<AppSidebarFallback />
|
||||
<MainContainerFallback>{children}</MainContainerFallback>
|
||||
</AppContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export const ShellAppFallback = ({
|
||||
className,
|
||||
children,
|
||||
}: PropsWithChildren<{
|
||||
className?: string;
|
||||
}>): ReactElement => {
|
||||
return (
|
||||
<AppContainer className={className}>
|
||||
<ShellAppSidebarFallback />
|
||||
<MainContainerFallback>{children}</MainContainerFallback>
|
||||
</AppContainer>
|
||||
);
|
||||
};
|
||||
@@ -1,16 +1,15 @@
|
||||
import { Tooltip } from '@affine/component/ui/tooltip';
|
||||
import { useCatchEventCallback } from '@affine/core/components/hooks/use-catch-event-hook';
|
||||
import { GlobalDialogService } from '@affine/core/modules/dialogs';
|
||||
import { SubscriptionPlan } from '@affine/graphql';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import { useLiveData, useServices } from '@toeverything/infra';
|
||||
import { useSetAtom } from 'jotai';
|
||||
import { useLiveData, useService, useServices } from '@toeverything/infra';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import {
|
||||
ServerConfigService,
|
||||
SubscriptionService,
|
||||
} from '../../../modules/cloud';
|
||||
import { openSettingModalAtom } from '../../atoms';
|
||||
import * as styles from './style.css';
|
||||
|
||||
export const UserPlanButton = () => {
|
||||
@@ -35,14 +34,13 @@ export const UserPlanButton = () => {
|
||||
subscriptionService.subscription.revalidate();
|
||||
}, [subscriptionService]);
|
||||
|
||||
const setSettingModalAtom = useSetAtom(openSettingModalAtom);
|
||||
const globalDialogService = useService(GlobalDialogService);
|
||||
const handleClick = useCatchEventCallback(() => {
|
||||
setSettingModalAtom({
|
||||
open: true,
|
||||
globalDialogService.open('setting', {
|
||||
activeTab: 'plans',
|
||||
scrollAnchor: 'cloudPricingPlan',
|
||||
});
|
||||
}, [setSettingModalAtom]);
|
||||
}, [globalDialogService]);
|
||||
|
||||
const t = useI18n();
|
||||
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks';
|
||||
import { CollectionService } from '@affine/core/modules/collection';
|
||||
import { WorkspaceDialogService } from '@affine/core/modules/dialogs';
|
||||
import type { Collection } from '@affine/env/filter';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import { AllDocsIcon, FilterIcon } from '@blocksuite/icons/rc';
|
||||
import { useService } from '@toeverything/infra';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { useEditCollection } from '../../page-list';
|
||||
import { ActionButton } from './action-button';
|
||||
import collectionDetailDark from './assets/collection-detail.dark.png';
|
||||
import collectionDetailLight from './assets/collection-detail.light.png';
|
||||
@@ -41,18 +40,21 @@ export const EmptyCollectionDetail = ({
|
||||
|
||||
const Actions = ({ collection }: { collection: Collection }) => {
|
||||
const t = useI18n();
|
||||
const collectionService = useService(CollectionService);
|
||||
const { open } = useEditCollection();
|
||||
const workspaceDialogService = useService(WorkspaceDialogService);
|
||||
|
||||
const openAddDocs = useAsyncCallback(async () => {
|
||||
const ret = await open({ ...collection }, 'page');
|
||||
collectionService.updateCollection(ret.id, () => ret);
|
||||
}, [open, collection, collectionService]);
|
||||
const openAddDocs = useCallback(() => {
|
||||
workspaceDialogService.open('collection-editor', {
|
||||
collectionId: collection.id,
|
||||
mode: 'page',
|
||||
});
|
||||
}, [collection, workspaceDialogService]);
|
||||
|
||||
const openAddRules = useAsyncCallback(async () => {
|
||||
const ret = await open({ ...collection }, 'rule');
|
||||
collectionService.updateCollection(ret.id, () => ret);
|
||||
}, [collection, open, collectionService]);
|
||||
const openAddRules = useCallback(() => {
|
||||
workspaceDialogService.open('collection-editor', {
|
||||
collectionId: collection.id,
|
||||
mode: 'rule',
|
||||
});
|
||||
}, [collection, workspaceDialogService]);
|
||||
|
||||
return (
|
||||
<div className={actionGroup}>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { usePromptModal } from '@affine/component';
|
||||
import { useNavigateHelper } from '@affine/core/components/hooks/use-navigate-helper';
|
||||
import { CollectionService } from '@affine/core/modules/collection';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
@@ -6,7 +7,7 @@ import { useService, WorkspaceService } from '@toeverything/infra';
|
||||
import { nanoid } from 'nanoid';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { createEmptyCollection, useEditCollectionName } from '../../page-list';
|
||||
import { createEmptyCollection } from '../../page-list';
|
||||
import { ActionButton } from './action-button';
|
||||
import collectionListDark from './assets/collection-list.dark.png';
|
||||
import collectionListLight from './assets/collection-list.light.png';
|
||||
@@ -19,24 +20,36 @@ export const EmptyCollections = (props: UniversalEmptyProps) => {
|
||||
const currentWorkspace = useService(WorkspaceService).workspace;
|
||||
|
||||
const navigateHelper = useNavigateHelper();
|
||||
const { open } = useEditCollectionName({
|
||||
title: t['com.affine.editCollection.createCollection'](),
|
||||
showTips: true,
|
||||
});
|
||||
const { openPromptModal } = usePromptModal();
|
||||
|
||||
const showAction = true;
|
||||
|
||||
const handleCreateCollection = useCallback(() => {
|
||||
open('')
|
||||
.then(name => {
|
||||
openPromptModal({
|
||||
title: t['com.affine.editCollection.saveCollection'](),
|
||||
label: t['com.affine.editCollectionName.name'](),
|
||||
inputOptions: {
|
||||
placeholder: t['com.affine.editCollectionName.name.placeholder'](),
|
||||
},
|
||||
children: t['com.affine.editCollectionName.createTips'](),
|
||||
confirmText: t['com.affine.editCollection.save'](),
|
||||
cancelText: t['com.affine.editCollection.button.cancel'](),
|
||||
confirmButtonOptions: {
|
||||
variant: 'primary',
|
||||
},
|
||||
onConfirm(name) {
|
||||
const id = nanoid();
|
||||
collectionService.addCollection(createEmptyCollection(id, { name }));
|
||||
navigateHelper.jumpToCollection(currentWorkspace.id, id);
|
||||
})
|
||||
.catch(err => {
|
||||
console.error(err);
|
||||
});
|
||||
}, [collectionService, currentWorkspace, navigateHelper, open]);
|
||||
},
|
||||
});
|
||||
}, [
|
||||
collectionService,
|
||||
currentWorkspace.id,
|
||||
navigateHelper,
|
||||
openPromptModal,
|
||||
t,
|
||||
]);
|
||||
|
||||
return (
|
||||
<EmptyLayout
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
import { HelpIsland } from '../../pure/help-island';
|
||||
import { ToolContainer } from '../../workspace';
|
||||
|
||||
export const HubIsland = () => {
|
||||
return (
|
||||
<ToolContainer>
|
||||
<HelpIsland />
|
||||
</ToolContainer>
|
||||
);
|
||||
};
|
||||
@@ -2,8 +2,8 @@ import { Loading, Scrollable } from '@affine/component';
|
||||
import { EditorLoading } from '@affine/component/page-detail-skeleton';
|
||||
import { Button, IconButton } from '@affine/component/ui/button';
|
||||
import { Modal, useConfirmModal } from '@affine/component/ui/modal';
|
||||
import { openSettingModalAtom } from '@affine/core/components/atoms';
|
||||
import { useDocCollectionPageTitle } from '@affine/core/components/hooks/use-block-suite-workspace-page-title';
|
||||
import { GlobalDialogService } from '@affine/core/modules/dialogs';
|
||||
import { EditorService } from '@affine/core/modules/editor';
|
||||
import { WorkspacePermissionService } from '@affine/core/modules/permissions';
|
||||
import { WorkspaceQuotaService } from '@affine/core/modules/quota';
|
||||
@@ -18,7 +18,7 @@ import { CloseIcon, ToggleCollapseIcon } from '@blocksuite/icons/rc';
|
||||
import * as Collapsible from '@radix-ui/react-collapsible';
|
||||
import type { DialogContentProps } from '@radix-ui/react-dialog';
|
||||
import { useLiveData, useService, WorkspaceService } from '@toeverything/infra';
|
||||
import { atom, useAtom, useSetAtom } from 'jotai';
|
||||
import { atom, useAtom } from 'jotai';
|
||||
import type { PropsWithChildren } from 'react';
|
||||
import {
|
||||
Fragment,
|
||||
@@ -188,21 +188,19 @@ const PlanPrompt = () => {
|
||||
permissionService.permission.revalidate();
|
||||
}, [permissionService]);
|
||||
|
||||
const setSettingModalAtom = useSetAtom(openSettingModalAtom);
|
||||
const [planPromptClosed, setPlanPromptClosed] = useAtom(planPromptClosedAtom);
|
||||
|
||||
const globalDialogService = useService(GlobalDialogService);
|
||||
const closeFreePlanPrompt = useCallback(() => {
|
||||
setPlanPromptClosed(true);
|
||||
}, [setPlanPromptClosed]);
|
||||
|
||||
const onClickUpgrade = useCallback(() => {
|
||||
setSettingModalAtom({
|
||||
open: true,
|
||||
globalDialogService.open('setting', {
|
||||
activeTab: 'plans',
|
||||
scrollAnchor: 'cloudPricingPlan',
|
||||
});
|
||||
track.$.docHistory.$.viewPlans();
|
||||
}, [setSettingModalAtom]);
|
||||
}, [globalDialogService]);
|
||||
|
||||
const t = useI18n();
|
||||
|
||||
|
||||
@@ -1,16 +1,14 @@
|
||||
import { ConfirmModal } from '@affine/component/ui/modal';
|
||||
import {
|
||||
openQuotaModalAtom,
|
||||
openSettingModalAtom,
|
||||
} from '@affine/core/components/atoms';
|
||||
import { openQuotaModalAtom } from '@affine/core/components/atoms';
|
||||
import { UserQuotaService } from '@affine/core/modules/cloud';
|
||||
import { GlobalDialogService } from '@affine/core/modules/dialogs';
|
||||
import { WorkspacePermissionService } from '@affine/core/modules/permissions';
|
||||
import { WorkspaceQuotaService } from '@affine/core/modules/quota';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import { track } from '@affine/track';
|
||||
import { useLiveData, useService, WorkspaceService } from '@toeverything/infra';
|
||||
import bytes from 'bytes';
|
||||
import { useAtom, useSetAtom } from 'jotai';
|
||||
import { useAtom } from 'jotai';
|
||||
import { useCallback, useEffect, useMemo } from 'react';
|
||||
|
||||
export const CloudQuotaModal = () => {
|
||||
@@ -45,17 +43,16 @@ export const CloudQuotaModal = () => {
|
||||
return isOwner && userQuota?.name === 'free';
|
||||
}, [isOwner, userQuota]);
|
||||
|
||||
const setSettingModalAtom = useSetAtom(openSettingModalAtom);
|
||||
const globalDialogService = useService(GlobalDialogService);
|
||||
const handleUpgradeConfirm = useCallback(() => {
|
||||
setSettingModalAtom({
|
||||
open: true,
|
||||
globalDialogService.open('setting', {
|
||||
activeTab: 'plans',
|
||||
scrollAnchor: 'cloudPricingPlan',
|
||||
});
|
||||
|
||||
track.$.paywall.storage.viewPlans();
|
||||
setOpen(false);
|
||||
}, [setOpen, setSettingModalAtom]);
|
||||
}, [globalDialogService, setOpen]);
|
||||
|
||||
const description = useMemo(() => {
|
||||
if (userQuota && isFreePlanOwner) {
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
import { atom } from 'jotai';
|
||||
|
||||
export const settingModalScrollContainerAtom = atom<HTMLElement | null>(null);
|
||||
@@ -1,20 +0,0 @@
|
||||
export const GeneralSettingKeys = [
|
||||
'shortcuts',
|
||||
'appearance',
|
||||
'about',
|
||||
'plans',
|
||||
'billing',
|
||||
'experimental-features',
|
||||
'editor',
|
||||
] as const;
|
||||
|
||||
export const WorkspaceSubTabs = ['preference', 'properties'] as const;
|
||||
|
||||
export type GeneralSettingKey = (typeof GeneralSettingKeys)[number];
|
||||
|
||||
export type WorkspaceSubTab = (typeof WorkspaceSubTabs)[number];
|
||||
|
||||
export type ActiveTab =
|
||||
| GeneralSettingKey
|
||||
| 'account'
|
||||
| `workspace:${WorkspaceSubTab}`;
|
||||
@@ -1,22 +0,0 @@
|
||||
import type { WorkspaceMetadata } from '@toeverything/infra';
|
||||
|
||||
import type { WorkspaceSubTab } from '../types';
|
||||
import { WorkspaceSettingDetail } from './new-workspace-setting-detail';
|
||||
import { WorkspaceSettingProperties } from './properties';
|
||||
|
||||
export const WorkspaceSetting = ({
|
||||
workspaceMetadata,
|
||||
subTab,
|
||||
}: {
|
||||
workspaceMetadata: WorkspaceMetadata;
|
||||
subTab: WorkspaceSubTab;
|
||||
}) => {
|
||||
switch (subTab) {
|
||||
case 'preference':
|
||||
return <WorkspaceSettingDetail workspaceMetadata={workspaceMetadata} />;
|
||||
case 'properties':
|
||||
return (
|
||||
<WorkspaceSettingProperties workspaceMetadata={workspaceMetadata} />
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -1,13 +1,13 @@
|
||||
import { notify, Skeleton } from '@affine/component';
|
||||
import { Button } from '@affine/component/ui/button';
|
||||
import { Menu, MenuItem, MenuTrigger } from '@affine/component/ui/menu';
|
||||
import { openSettingModalAtom } from '@affine/core/components/atoms';
|
||||
import {
|
||||
getSelectedNodes,
|
||||
useSharingUrl,
|
||||
} from '@affine/core/components/hooks/affine/use-share-url';
|
||||
import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks';
|
||||
import { ServerConfigService } from '@affine/core/modules/cloud';
|
||||
import { GlobalDialogService } from '@affine/core/modules/dialogs';
|
||||
import { EditorService } from '@affine/core/modules/editor';
|
||||
import { WorkspacePermissionService } from '@affine/core/modules/permissions';
|
||||
import { ShareInfoService } from '@affine/core/modules/share-doc';
|
||||
@@ -28,7 +28,6 @@ import {
|
||||
} from '@blocksuite/icons/rc';
|
||||
import { useLiveData, useService } from '@toeverything/infra';
|
||||
import { cssVar } from '@toeverything/theme';
|
||||
import { useSetAtom } from 'jotai';
|
||||
import { Suspense, useCallback, useEffect, useMemo } from 'react';
|
||||
import { ErrorBoundary } from 'react-error-boundary';
|
||||
|
||||
@@ -83,15 +82,14 @@ export const AFFiNESharePage = (props: ShareMenuProps) => {
|
||||
|
||||
const permissionService = useService(WorkspacePermissionService);
|
||||
const isOwner = useLiveData(permissionService.permission.isOwner$);
|
||||
const setSettingModalAtom = useSetAtom(openSettingModalAtom);
|
||||
const globalDialogService = useService(GlobalDialogService);
|
||||
|
||||
const onOpenWorkspaceSettings = useCallback(() => {
|
||||
setSettingModalAtom({
|
||||
open: true,
|
||||
globalDialogService.open('setting', {
|
||||
activeTab: 'workspace:preference',
|
||||
workspaceMetadata: props.workspaceMetadata,
|
||||
});
|
||||
}, [props.workspaceMetadata, setSettingModalAtom]);
|
||||
}, [globalDialogService, props.workspaceMetadata]);
|
||||
|
||||
const onClickAnyoneReadOnlyShare = useAsyncCallback(async () => {
|
||||
if (isSharedPage) {
|
||||
|
||||
@@ -49,47 +49,6 @@ const SubscriptionChangedNotifyFooter = ({
|
||||
);
|
||||
};
|
||||
|
||||
export const useUpgradeNotify = () => {
|
||||
const t = useI18n();
|
||||
const prevNotifyIdRef = useRef<string | number | null>(null);
|
||||
|
||||
return useCallback(
|
||||
(link: string) => {
|
||||
prevNotifyIdRef.current && notify.dismiss(prevNotifyIdRef.current);
|
||||
const id = notify(
|
||||
{
|
||||
title: (
|
||||
<span className={notifyHeader}>
|
||||
{t['com.affine.payment.upgrade-success-notify.title']()}
|
||||
</span>
|
||||
),
|
||||
message: t['com.affine.payment.upgrade-success-notify.content'](),
|
||||
alignMessage: 'title',
|
||||
icon: null,
|
||||
footer: (
|
||||
<SubscriptionChangedNotifyFooter
|
||||
to={link}
|
||||
okText={
|
||||
BUILD_CONFIG.isElectron
|
||||
? t['com.affine.payment.upgrade-success-notify.ok-client']()
|
||||
: t['com.affine.payment.upgrade-success-notify.ok-web']()
|
||||
}
|
||||
cancelText={t[
|
||||
'com.affine.payment.upgrade-success-notify.later'
|
||||
]()}
|
||||
onCancel={() => notify.dismiss(id)}
|
||||
onConfirm={() => notify.dismiss(id)}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{ duration: 24 * 60 * 60 * 1000 }
|
||||
);
|
||||
prevNotifyIdRef.current = id;
|
||||
},
|
||||
[t]
|
||||
);
|
||||
};
|
||||
|
||||
export const useDowngradeNotify = () => {
|
||||
const t = useI18n();
|
||||
const prevNotifyIdRef = useRef<string | number | null>(null);
|
||||
|
||||
@@ -1,40 +1,13 @@
|
||||
import { atom } from 'jotai';
|
||||
|
||||
import type { SettingProps } from '../affine/setting-modal';
|
||||
import type { ActiveTab } from '../affine/setting-modal/types';
|
||||
// modal atoms
|
||||
export const openWorkspacesModalAtom = atom(false);
|
||||
/**
|
||||
* @deprecated use `useSignOut` hook instated
|
||||
*/
|
||||
export const openSignOutModalAtom = atom(false);
|
||||
export const openQuotaModalAtom = atom(false);
|
||||
export const openStarAFFiNEModalAtom = atom(false);
|
||||
export const openIssueFeedbackModalAtom = atom(false);
|
||||
export const openHistoryTipsModalAtom = atom(false);
|
||||
|
||||
export const rightSidebarWidthAtom = atom(320);
|
||||
|
||||
export type PlansScrollAnchor =
|
||||
| 'aiPricingPlan'
|
||||
| 'cloudPricingPlan'
|
||||
| 'lifetimePricingPlan';
|
||||
export type SettingAtom = {
|
||||
open: boolean;
|
||||
workspaceMetadata?: SettingProps['workspaceMetadata'];
|
||||
} & (
|
||||
| {
|
||||
activeTab: 'plans';
|
||||
scrollAnchor?: PlansScrollAnchor;
|
||||
}
|
||||
| { activeTab: Exclude<ActiveTab, 'plans'> }
|
||||
);
|
||||
|
||||
export const openSettingModalAtom = atom<SettingAtom>({
|
||||
activeTab: 'appearance',
|
||||
open: false,
|
||||
});
|
||||
|
||||
export const openImportModalAtom = atom(false);
|
||||
|
||||
export type AuthAtomData =
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import { AIProvider } from '@affine/core/blocksuite/presets/ai';
|
||||
import { toggleGeneralAIOnboarding } from '@affine/core/components/affine/ai-onboarding/apis';
|
||||
import { authAtom, openSettingModalAtom } from '@affine/core/components/atoms';
|
||||
import { authAtom } from '@affine/core/components/atoms';
|
||||
import {
|
||||
getBaseUrl,
|
||||
type getCopilotHistoriesQuery,
|
||||
type RequestOptions,
|
||||
} from '@affine/graphql';
|
||||
import { track } from '@affine/track';
|
||||
import { UnauthorizedError } from '@blocksuite/affine/blocks';
|
||||
import { assertExists } from '@blocksuite/affine/global/utils';
|
||||
import { getCurrentStore } from '@toeverything/infra';
|
||||
@@ -463,14 +462,6 @@ Could you make a new website based on these notes and send back just the html fi
|
||||
return forkCopilotSession(options);
|
||||
});
|
||||
|
||||
AIProvider.slots.requestUpgradePlan.on(() => {
|
||||
getCurrentStore().set(openSettingModalAtom, {
|
||||
activeTab: 'billing',
|
||||
open: true,
|
||||
});
|
||||
track.$.paywall.aiAction.viewPlans();
|
||||
});
|
||||
|
||||
AIProvider.slots.requestLogin.on(() => {
|
||||
getCurrentStore().set(authAtom, s => ({
|
||||
...s,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { IconButton } from '@affine/component';
|
||||
import { DocInfoService } from '@affine/core/modules/doc-info';
|
||||
import { WorkspaceDialogService } from '@affine/core/modules/dialogs';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import { track } from '@affine/track';
|
||||
import { InformationIcon } from '@blocksuite/icons/rc';
|
||||
@@ -7,13 +7,13 @@ import { useService } from '@toeverything/infra';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
export const InfoButton = ({ docId }: { docId: string }) => {
|
||||
const modal = useService(DocInfoService).modal;
|
||||
const workspaceDialogService = useService(WorkspaceDialogService);
|
||||
const t = useI18n();
|
||||
|
||||
const onOpenInfoModal = useCallback(() => {
|
||||
track.$.header.actions.openDocInfo();
|
||||
modal.open(docId);
|
||||
}, [docId, modal]);
|
||||
workspaceDialogService.open('doc-info', { docId });
|
||||
}, [docId, workspaceDialogService]);
|
||||
|
||||
return (
|
||||
<IconButton
|
||||
|
||||
@@ -1,17 +1,20 @@
|
||||
import { OverlayModal } from '@affine/component';
|
||||
import { openHistoryTipsModalAtom } from '@affine/core/components/atoms';
|
||||
import { useEnableCloud } from '@affine/core/components/hooks/affine/use-enable-cloud';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import { useService, WorkspaceService } from '@toeverything/infra';
|
||||
import { useAtom } from 'jotai';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import TopSvg from './top-svg';
|
||||
|
||||
export const HistoryTipsModal = () => {
|
||||
export const HistoryTipsModal = ({
|
||||
open,
|
||||
setOpen,
|
||||
}: {
|
||||
open: boolean;
|
||||
setOpen: (open: boolean) => void;
|
||||
}) => {
|
||||
const t = useI18n();
|
||||
const currentWorkspace = useService(WorkspaceService).workspace;
|
||||
const [open, setOpen] = useAtom(openHistoryTipsModalAtom);
|
||||
const confirmEnableCloud = useEnableCloud();
|
||||
|
||||
const handleConfirm = useCallback(() => {
|
||||
@@ -1,4 +1,4 @@
|
||||
import { notify } from '@affine/component';
|
||||
import { notify, useConfirmModal } from '@affine/component';
|
||||
import {
|
||||
Menu,
|
||||
MenuItem,
|
||||
@@ -7,14 +7,9 @@ import {
|
||||
} from '@affine/component/ui/menu';
|
||||
import { PageHistoryModal } from '@affine/core/components/affine/page-history-modal';
|
||||
import { ShareMenuContent } from '@affine/core/components/affine/share-page-modal/share-menu';
|
||||
import {
|
||||
openHistoryTipsModalAtom,
|
||||
openImportModalAtom,
|
||||
} from '@affine/core/components/atoms';
|
||||
import { useBlockSuiteMetaHelper } from '@affine/core/components/hooks/affine/use-block-suite-meta-helper';
|
||||
import { useEnableCloud } from '@affine/core/components/hooks/affine/use-enable-cloud';
|
||||
import { useExportPage } from '@affine/core/components/hooks/affine/use-export-page';
|
||||
import { useTrashModalHelper } from '@affine/core/components/hooks/affine/use-trash-modal-helper';
|
||||
import { useDocMetaHelper } from '@affine/core/components/hooks/use-block-suite-page-meta';
|
||||
import {
|
||||
Export,
|
||||
@@ -23,7 +18,10 @@ import {
|
||||
} from '@affine/core/components/page-list';
|
||||
import { IsFavoriteIcon } from '@affine/core/components/pure/icons';
|
||||
import { useDetailPageHeaderResponsive } from '@affine/core/desktop/pages/workspace/detail-page/use-header-responsive';
|
||||
import { DocInfoService } from '@affine/core/modules/doc-info';
|
||||
import {
|
||||
GlobalDialogService,
|
||||
WorkspaceDialogService,
|
||||
} from '@affine/core/modules/dialogs';
|
||||
import { EditorService } from '@affine/core/modules/editor';
|
||||
import { OpenInAppService } from '@affine/core/modules/open-in-app/services';
|
||||
import { WorkbenchService } from '@affine/core/modules/workbench';
|
||||
@@ -55,11 +53,11 @@ import {
|
||||
useServiceOptional,
|
||||
WorkspaceService,
|
||||
} from '@toeverything/infra';
|
||||
import { useSetAtom } from 'jotai';
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
import { HeaderDropDownButton } from '../../../pure/header-drop-down-button';
|
||||
import { useFavorite } from '../favorite';
|
||||
import { HistoryTipsModal } from './history-tips-modal';
|
||||
|
||||
type PageMenuProps = {
|
||||
rename?: () => void;
|
||||
@@ -81,6 +79,7 @@ export const PageHeaderMenuButton = ({
|
||||
|
||||
const workspace = useService(WorkspaceService).workspace;
|
||||
|
||||
const globalDialogService = useService(GlobalDialogService);
|
||||
const editorService = useService(EditorService);
|
||||
const isInTrash = useLiveData(
|
||||
editorService.editor.doc.meta$.map(meta => meta.trash)
|
||||
@@ -98,7 +97,6 @@ export const PageHeaderMenuButton = ({
|
||||
const { favorite, toggleFavorite } = useFavorite(pageId);
|
||||
|
||||
const { duplicate } = useBlockSuiteMetaHelper();
|
||||
const { setTrashModal } = useTrashModalHelper();
|
||||
|
||||
const [isEditing, setEditing] = useState(!page.readonly);
|
||||
const { setDocReadonly } = useDocMetaHelper();
|
||||
@@ -122,8 +120,7 @@ export const PageHeaderMenuButton = ({
|
||||
}, [openSidePanel]);
|
||||
|
||||
const [historyModalOpen, setHistoryModalOpen] = useState(false);
|
||||
const setOpenHistoryTipsModal = useSetAtom(openHistoryTipsModalAtom);
|
||||
const setOpenImportModalAtom = useSetAtom(openImportModalAtom);
|
||||
const [openHistoryTipsModal, setOpenHistoryTipsModal] = useState(false);
|
||||
|
||||
const openHistoryModal = useCallback(() => {
|
||||
track.$.header.history.open();
|
||||
@@ -133,11 +130,11 @@ export const PageHeaderMenuButton = ({
|
||||
return setOpenHistoryTipsModal(true);
|
||||
}, [setOpenHistoryTipsModal, workspace.flavour]);
|
||||
|
||||
const docInfoModal = useService(DocInfoService).modal;
|
||||
const workspaceDialogService = useService(WorkspaceDialogService);
|
||||
const openInfoModal = useCallback(() => {
|
||||
track.$.header.pageInfo.open();
|
||||
docInfoModal.open(pageId);
|
||||
}, [docInfoModal, pageId]);
|
||||
workspaceDialogService.open('doc-info', { docId: pageId });
|
||||
}, [workspaceDialogService, pageId]);
|
||||
|
||||
const handleOpenInNewTab = useCallback(() => {
|
||||
workbench.openDoc(pageId, {
|
||||
@@ -151,14 +148,22 @@ export const PageHeaderMenuButton = ({
|
||||
});
|
||||
}, [pageId, workbench]);
|
||||
|
||||
const { openConfirmModal } = useConfirmModal();
|
||||
|
||||
const handleOpenTrashModal = useCallback(() => {
|
||||
track.$.header.docOptions.deleteDoc();
|
||||
setTrashModal({
|
||||
open: true,
|
||||
pageIds: [pageId],
|
||||
pageTitles: [editorService.editor.doc.meta$.value.title ?? ''],
|
||||
openConfirmModal({
|
||||
title: t['com.affine.moveToTrash.confirmModal.title'](),
|
||||
description: t['com.affine.moveToTrash.confirmModal.description']({
|
||||
title: editorService.editor.doc.title$.value || t['Untitled'](),
|
||||
}),
|
||||
cancelText: t['com.affine.confirmModal.button.cancel'](),
|
||||
confirmText: t.Delete(),
|
||||
onConfirm: () => {
|
||||
editorService.editor.doc.moveToTrash();
|
||||
},
|
||||
});
|
||||
}, [editorService, pageId, setTrashModal]);
|
||||
}, [editorService.editor.doc, openConfirmModal, t]);
|
||||
|
||||
const handleRename = useCallback(() => {
|
||||
rename?.();
|
||||
@@ -201,8 +206,8 @@ export const PageHeaderMenuButton = ({
|
||||
|
||||
const handleOpenImportModal = useCallback(() => {
|
||||
track.$.header.importModal.open();
|
||||
setOpenImportModalAtom(true);
|
||||
}, [setOpenImportModalAtom]);
|
||||
globalDialogService.open('import', undefined);
|
||||
}, [globalDialogService]);
|
||||
|
||||
const handleShareMenuOpenChange = useCallback((open: boolean) => {
|
||||
if (open) {
|
||||
@@ -411,6 +416,10 @@ export const PageHeaderMenuButton = ({
|
||||
onOpenChange={setHistoryModalOpen}
|
||||
/>
|
||||
) : null}
|
||||
<HistoryTipsModal
|
||||
open={openHistoryTipsModal}
|
||||
setOpen={setOpenHistoryTipsModal}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ConfirmModalProvider } from '@affine/component';
|
||||
import { ConfirmModalProvider, PromptModalProvider } from '@affine/component';
|
||||
import { ProviderComposer } from '@affine/component/provider-composer';
|
||||
import { ThemeProvider } from '@affine/core/components/theme-provider';
|
||||
import type { createStore } from 'jotai';
|
||||
@@ -19,6 +19,7 @@ export function AffineContext(props: AffineContextProps) {
|
||||
<Provider key="JotaiProvider" store={props.store} />,
|
||||
<ThemeProvider key="ThemeProvider" />,
|
||||
<ConfirmModalProvider key="ConfirmModalProvider" />,
|
||||
<PromptModalProvider key="PromptModalProvider" />,
|
||||
].filter(Boolean),
|
||||
[props.store]
|
||||
)}
|
||||
|
||||
@@ -1,2 +1 @@
|
||||
export * from './info-modal/info-modal';
|
||||
export * from './table';
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { toast } from '@affine/component';
|
||||
import { toast, useConfirmModal } from '@affine/component';
|
||||
import {
|
||||
PreconditionStrategy,
|
||||
registerAffineCommand,
|
||||
} from '@affine/core/commands';
|
||||
import { DocInfoService } from '@affine/core/modules/doc-info';
|
||||
import { WorkspaceDialogService } from '@affine/core/modules/dialogs';
|
||||
import type { Editor } from '@affine/core/modules/editor';
|
||||
import { CompatibleFavoriteItemsAdapter } from '@affine/core/modules/favorite';
|
||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
@@ -22,7 +22,6 @@ import { useCallback, useEffect } from 'react';
|
||||
import { pageHistoryModalAtom } from '../../../components/atoms/page-history';
|
||||
import { useBlockSuiteMetaHelper } from './use-block-suite-meta-helper';
|
||||
import { useExportPage } from './use-export-page';
|
||||
import { useTrashModalHelper } from './use-trash-modal-helper';
|
||||
|
||||
export function useRegisterBlocksuiteEditorCommands(editor: Editor) {
|
||||
const doc = useService(DocService).doc;
|
||||
@@ -36,7 +35,7 @@ export function useRegisterBlocksuiteEditorCommands(editor: Editor) {
|
||||
const trash = useLiveData(doc.trash$);
|
||||
|
||||
const setPageHistoryModalState = useSetAtom(pageHistoryModalAtom);
|
||||
const docInfoModal = useService(DocInfoService).modal;
|
||||
const workspaceDialogService = useService(WorkspaceDialogService);
|
||||
|
||||
const openHistoryModal = useCallback(() => {
|
||||
setPageHistoryModalState(() => ({
|
||||
@@ -46,22 +45,25 @@ export function useRegisterBlocksuiteEditorCommands(editor: Editor) {
|
||||
}, [docId, setPageHistoryModalState]);
|
||||
|
||||
const openInfoModal = useCallback(() => {
|
||||
docInfoModal.open(docId);
|
||||
}, [docId, docInfoModal]);
|
||||
workspaceDialogService.open('doc-info', { docId });
|
||||
}, [docId, workspaceDialogService]);
|
||||
|
||||
const { duplicate } = useBlockSuiteMetaHelper();
|
||||
const exportHandler = useExportPage();
|
||||
const { setTrashModal } = useTrashModalHelper();
|
||||
const onClickDelete = useCallback(
|
||||
(title: string) => {
|
||||
setTrashModal({
|
||||
open: true,
|
||||
pageIds: [docId],
|
||||
pageTitles: [title],
|
||||
});
|
||||
},
|
||||
[docId, setTrashModal]
|
||||
);
|
||||
const { openConfirmModal } = useConfirmModal();
|
||||
const onClickDelete = useCallback(() => {
|
||||
openConfirmModal({
|
||||
title: t['com.affine.moveToTrash.confirmModal.title'](),
|
||||
description: t['com.affine.moveToTrash.confirmModal.description']({
|
||||
title: doc.title$.value || t['Untitled'](),
|
||||
}),
|
||||
cancelText: t['com.affine.confirmModal.button.cancel'](),
|
||||
confirmText: t.Delete(),
|
||||
onConfirm: () => {
|
||||
doc.moveToTrash();
|
||||
},
|
||||
});
|
||||
}, [doc, openConfirmModal, t]);
|
||||
|
||||
const isCloudWorkspace = workspace.flavour === WorkspaceFlavour.AFFINE_CLOUD;
|
||||
|
||||
@@ -174,23 +176,6 @@ export function useRegisterBlocksuiteEditorCommands(editor: Editor) {
|
||||
})
|
||||
);
|
||||
|
||||
unsubs.push(
|
||||
registerAffineCommand({
|
||||
id: `editor:${mode}-export-to-pdf`,
|
||||
preconditionStrategy: () => mode === 'page' && !trash,
|
||||
category: `editor:${mode}`,
|
||||
icon: mode === 'page' ? <PageIcon /> : <EdgelessIcon />,
|
||||
label: t['Export to PDF'](),
|
||||
async run() {
|
||||
track.$.cmdk.editor.export({
|
||||
type: 'pdf',
|
||||
});
|
||||
|
||||
exportHandler('pdf');
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
unsubs.push(
|
||||
registerAffineCommand({
|
||||
id: `editor:${mode}-export-to-html`,
|
||||
@@ -252,7 +237,7 @@ export function useRegisterBlocksuiteEditorCommands(editor: Editor) {
|
||||
run() {
|
||||
track.$.cmdk.editor.deleteDoc();
|
||||
|
||||
onClickDelete(doc.title$.value);
|
||||
onClickDelete();
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
@@ -1,16 +1,10 @@
|
||||
import { useUpgradeNotify } from '@affine/core/components/affine/subscription-landing/notify';
|
||||
import { SubscriptionPlan, SubscriptionRecurring } from '@affine/graphql';
|
||||
import { track } from '@affine/track';
|
||||
import { nanoid } from 'nanoid';
|
||||
import { useCallback, useEffect } from 'react';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
|
||||
import { type AuthAccountInfo } from '../../../modules/cloud';
|
||||
|
||||
const separator = '::';
|
||||
const recoverSeparator = nanoid();
|
||||
const localStorageKey = 'subscription-succeed-info';
|
||||
|
||||
const typeFormUrl = 'https://6dxre9ihosp.typeform.com/to';
|
||||
const typeFormUpgradeId = 'mUMGGQS8';
|
||||
const typeFormDowngradeId = 'RvD9AoRg';
|
||||
@@ -69,80 +63,3 @@ export const generateSubscriptionCallbackLink = (
|
||||
|
||||
return `${baseUrl}?info=${encodeURIComponent(query)}`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Parse subscription callback query.info
|
||||
* @returns
|
||||
*/
|
||||
export const parseSubscriptionCallbackLink = (query: string) => {
|
||||
const [plan, recurring, id, email, rawName] =
|
||||
decodeURIComponent(query).split(separator);
|
||||
const name = rawName.replaceAll(recoverSeparator, separator);
|
||||
|
||||
return {
|
||||
plan: plan as SubscriptionPlan,
|
||||
recurring: recurring as SubscriptionRecurring,
|
||||
account: {
|
||||
id,
|
||||
email,
|
||||
info: {
|
||||
name,
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook to parse subscription callback link, and save to local storage and delete the query
|
||||
*/
|
||||
export const useSubscriptionNotifyWriter = () => {
|
||||
const [searchParams] = useSearchParams();
|
||||
|
||||
useEffect(() => {
|
||||
const query = searchParams.get('info');
|
||||
if (query) {
|
||||
localStorage.setItem(localStorageKey, query);
|
||||
searchParams.delete('info');
|
||||
}
|
||||
}, [searchParams]);
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook to read and parse subscription info from localStorage
|
||||
*/
|
||||
export const useSubscriptionNotifyReader = () => {
|
||||
const upgradeNotify = useUpgradeNotify();
|
||||
|
||||
const readAndNotify = useCallback(() => {
|
||||
const query = localStorage.getItem(localStorageKey);
|
||||
if (!query) return;
|
||||
|
||||
try {
|
||||
const { plan, recurring, account } = parseSubscriptionCallbackLink(query);
|
||||
const link = getUpgradeQuestionnaireLink({
|
||||
id: account.id,
|
||||
email: account.email,
|
||||
name: account.info?.name ?? '',
|
||||
plan,
|
||||
recurring,
|
||||
});
|
||||
upgradeNotify(link);
|
||||
localStorage.removeItem(localStorageKey);
|
||||
|
||||
track.$.settingsPanel.plans.subscribe({
|
||||
plan,
|
||||
recurring,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('Failed to parse subscription callback link', err);
|
||||
}
|
||||
}, [upgradeNotify]);
|
||||
|
||||
useEffect(() => {
|
||||
readAndNotify();
|
||||
window.addEventListener('focus', readAndNotify);
|
||||
return () => {
|
||||
window.removeEventListener('focus', readAndNotify);
|
||||
};
|
||||
}, [readAndNotify]);
|
||||
};
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
import { toast } from '@affine/component';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import { useAtom } from 'jotai';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { trashModalAtom } from '../../../components/atoms/trash-modal';
|
||||
import { useBlockSuiteMetaHelper } from './use-block-suite-meta-helper';
|
||||
|
||||
export function useTrashModalHelper() {
|
||||
const t = useI18n();
|
||||
const [trashModal, setTrashModal] = useAtom(trashModalAtom);
|
||||
const { pageIds } = trashModal;
|
||||
const { removeToTrash } = useBlockSuiteMetaHelper();
|
||||
const handleOnConfirm = useCallback(() => {
|
||||
pageIds.forEach(pageId => {
|
||||
removeToTrash(pageId);
|
||||
});
|
||||
toast(t['com.affine.toastMessage.movedTrash']());
|
||||
setTrashModal({ ...trashModal, open: false });
|
||||
}, [pageIds, removeToTrash, setTrashModal, t, trashModal]);
|
||||
|
||||
return {
|
||||
trashModal,
|
||||
setTrashModal,
|
||||
handleOnConfirm,
|
||||
};
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import { AppSidebarService } from '@affine/core/modules/app-sidebar';
|
||||
import { DesktopApiService } from '@affine/core/modules/desktop-api/service';
|
||||
import { DesktopApiService } from '@affine/core/modules/desktop-api';
|
||||
import { GlobalDialogService } from '@affine/core/modules/dialogs';
|
||||
import { I18nService } from '@affine/core/modules/i18n';
|
||||
import { UrlService } from '@affine/core/modules/url';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
@@ -25,7 +26,6 @@ import {
|
||||
registerAffineUpdatesCommands,
|
||||
} from '../../commands';
|
||||
import { usePageHelper } from '../../components/blocksuite/block-suite-page-list/utils';
|
||||
import { CreateWorkspaceDialogService } from '../../modules/create-workspace';
|
||||
import { EditorSettingService } from '../../modules/editor-setting';
|
||||
import { CMDKQuickSearchService } from '../../modules/quicksearch/services/cmdk';
|
||||
import { useActiveBlocksuiteEditor } from './use-block-suite-editor';
|
||||
@@ -78,7 +78,7 @@ export function useRegisterWorkspaceCommands() {
|
||||
const [editor] = useActiveBlocksuiteEditor();
|
||||
const cmdkQuickSearchService = useService(CMDKQuickSearchService);
|
||||
const editorSettingService = useService(EditorSettingService);
|
||||
const createWorkspaceDialogService = useService(CreateWorkspaceDialogService);
|
||||
const globalDialogService = useService(GlobalDialogService);
|
||||
const appSidebarService = useService(AppSidebarService);
|
||||
const i18n = useService(I18nService).i18n;
|
||||
|
||||
@@ -117,12 +117,19 @@ export function useRegisterWorkspaceCommands() {
|
||||
t,
|
||||
docCollection: currentWorkspace.docCollection,
|
||||
navigationHelper,
|
||||
globalDialogService,
|
||||
});
|
||||
|
||||
return () => {
|
||||
unsub();
|
||||
};
|
||||
}, [store, t, currentWorkspace.docCollection, navigationHelper]);
|
||||
}, [
|
||||
store,
|
||||
t,
|
||||
currentWorkspace.docCollection,
|
||||
navigationHelper,
|
||||
globalDialogService,
|
||||
]);
|
||||
|
||||
// register AffineSettingsCommands
|
||||
useEffect(() => {
|
||||
@@ -162,7 +169,7 @@ export function useRegisterWorkspaceCommands() {
|
||||
// register AffineCreationCommands
|
||||
useEffect(() => {
|
||||
const unsub = registerAffineCreationCommands({
|
||||
createWorkspaceDialogService,
|
||||
globalDialogService,
|
||||
pageHelper: pageHelper,
|
||||
t,
|
||||
});
|
||||
@@ -170,18 +177,18 @@ export function useRegisterWorkspaceCommands() {
|
||||
return () => {
|
||||
unsub();
|
||||
};
|
||||
}, [store, pageHelper, t, createWorkspaceDialogService]);
|
||||
}, [store, pageHelper, t, globalDialogService]);
|
||||
|
||||
// register AffineHelpCommands
|
||||
useEffect(() => {
|
||||
const unsub = registerAffineHelpCommands({
|
||||
store,
|
||||
t,
|
||||
urlService,
|
||||
globalDialogService,
|
||||
});
|
||||
|
||||
return () => {
|
||||
unsub();
|
||||
};
|
||||
}, [store, t, urlService]);
|
||||
}, [t, globalDialogService, urlService]);
|
||||
}
|
||||
|
||||
@@ -1,250 +0,0 @@
|
||||
import { toast } from '@affine/component';
|
||||
import {
|
||||
pushGlobalLoadingEventAtom,
|
||||
resolveGlobalLoadingEventAtom,
|
||||
} from '@affine/component/global-loading';
|
||||
import {
|
||||
OpenInAppCard,
|
||||
SidebarSwitch,
|
||||
} from '@affine/core/modules/app-sidebar/views';
|
||||
import { WorkspaceDesktopApiService } from '@affine/core/modules/desktop-api/service';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import { type DocMode, ZipTransformer } from '@blocksuite/affine/blocks';
|
||||
import {
|
||||
DocsService,
|
||||
effect,
|
||||
fromPromise,
|
||||
LiveData,
|
||||
onStart,
|
||||
throwIfAborted,
|
||||
useLiveData,
|
||||
useService,
|
||||
useServices,
|
||||
WorkspaceService,
|
||||
} from '@toeverything/infra';
|
||||
import { useSetAtom } from 'jotai';
|
||||
import type { PropsWithChildren } from 'react';
|
||||
import { useEffect } from 'react';
|
||||
import {
|
||||
catchError,
|
||||
EMPTY,
|
||||
finalize,
|
||||
mergeMap,
|
||||
switchMap,
|
||||
timeout,
|
||||
} from 'rxjs';
|
||||
import { Map as YMap } from 'yjs';
|
||||
|
||||
import { AIProvider } from '../../blocksuite/presets/ai';
|
||||
import { AppTabsHeader } from '../../modules/app-tabs-header';
|
||||
import { EditorSettingService } from '../../modules/editor-setting';
|
||||
import { NavigationButtons } from '../../modules/navigation';
|
||||
import { useRegisterNavigationCommands } from '../../modules/navigation/view/use-register-navigation-commands';
|
||||
import { QuickSearchContainer } from '../../modules/quicksearch';
|
||||
import { WorkbenchService } from '../../modules/workbench';
|
||||
import { WorkspaceAIOnboarding } from '../affine/ai-onboarding';
|
||||
import { AppContainer } from '../affine/app-container';
|
||||
import { SyncAwareness } from '../affine/awareness';
|
||||
import { useRegisterFindInPageCommands } from '../hooks/affine/use-register-find-in-page-commands';
|
||||
import { useSubscriptionNotifyReader } from '../hooks/affine/use-subscription-notify';
|
||||
import { useRegisterWorkspaceCommands } from '../hooks/use-register-workspace-commands';
|
||||
import { OverCapacityNotification } from '../over-capacity';
|
||||
import { CurrentWorkspaceModals } from '../providers/modal-provider';
|
||||
import { SWRConfigProvider } from '../providers/swr-config-provider';
|
||||
import { AIIsland } from '../pure/ai-island';
|
||||
import { RootAppSidebar } from '../root-app-sidebar';
|
||||
import { MainContainer } from '../workspace';
|
||||
import { WorkspaceUpgrade } from '../workspace-upgrade';
|
||||
import * as styles from './styles.css';
|
||||
|
||||
export const WorkspaceLayout = function WorkspaceLayout({
|
||||
children,
|
||||
}: PropsWithChildren) {
|
||||
return (
|
||||
<SWRConfigProvider>
|
||||
{/* load all workspaces is costly, do not block the whole UI */}
|
||||
<CurrentWorkspaceModals />
|
||||
<WorkspaceLayoutInner>{children}</WorkspaceLayoutInner>
|
||||
{/* should show after workspace loaded */}
|
||||
<WorkspaceAIOnboarding />
|
||||
<AIIsland />
|
||||
</SWRConfigProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export const WorkspaceLayoutProviders = ({ children }: PropsWithChildren) => {
|
||||
const t = useI18n();
|
||||
const pushGlobalLoadingEvent = useSetAtom(pushGlobalLoadingEventAtom);
|
||||
const resolveGlobalLoadingEvent = useSetAtom(resolveGlobalLoadingEventAtom);
|
||||
const { workspaceService, docsService } = useServices({
|
||||
WorkspaceService,
|
||||
DocsService,
|
||||
EditorSettingService,
|
||||
});
|
||||
const currentWorkspace = workspaceService.workspace;
|
||||
const docsList = docsService.list;
|
||||
|
||||
const workbench = useService(WorkbenchService).workbench;
|
||||
useEffect(() => {
|
||||
const insertTemplate = effect(
|
||||
switchMap(({ template, mode }: { template: string; mode: string }) => {
|
||||
return fromPromise(async abort => {
|
||||
const templateZip = await fetch(template, { signal: abort });
|
||||
const templateBlob = await templateZip.blob();
|
||||
throwIfAborted(abort);
|
||||
const [doc] = await ZipTransformer.importDocs(
|
||||
currentWorkspace.docCollection,
|
||||
templateBlob
|
||||
);
|
||||
if (doc) {
|
||||
doc.resetHistory();
|
||||
}
|
||||
|
||||
return { doc, mode };
|
||||
}).pipe(
|
||||
timeout(10000 /* 10s */),
|
||||
mergeMap(({ mode, doc }) => {
|
||||
if (doc) {
|
||||
docsList.setPrimaryMode(doc.id, mode as DocMode);
|
||||
workbench.openDoc(doc.id);
|
||||
}
|
||||
return EMPTY;
|
||||
}),
|
||||
onStart(() => {
|
||||
pushGlobalLoadingEvent({
|
||||
key: 'insert-template',
|
||||
});
|
||||
}),
|
||||
catchError(err => {
|
||||
console.error(err);
|
||||
toast(t['com.affine.ai.template-insert.failed']());
|
||||
return EMPTY;
|
||||
}),
|
||||
finalize(() => {
|
||||
resolveGlobalLoadingEvent('insert-template');
|
||||
})
|
||||
);
|
||||
})
|
||||
);
|
||||
|
||||
const disposable = AIProvider.slots.requestInsertTemplate.on(
|
||||
({ template, mode }) => {
|
||||
insertTemplate({ template, mode });
|
||||
}
|
||||
);
|
||||
return () => {
|
||||
disposable.dispose();
|
||||
insertTemplate.unsubscribe();
|
||||
};
|
||||
}, [
|
||||
currentWorkspace.docCollection,
|
||||
docsList,
|
||||
pushGlobalLoadingEvent,
|
||||
resolveGlobalLoadingEvent,
|
||||
t,
|
||||
workbench,
|
||||
]);
|
||||
|
||||
useSubscriptionNotifyReader();
|
||||
useRegisterWorkspaceCommands();
|
||||
useRegisterNavigationCommands();
|
||||
useRegisterFindInPageCommands();
|
||||
|
||||
useEffect(() => {
|
||||
// hotfix for blockVersions
|
||||
// this is a mistake in the
|
||||
// 0.8.0 ~ 0.8.1
|
||||
// 0.8.0-beta.0 ~ 0.8.0-beta.3
|
||||
// 0.8.0-canary.17 ~ 0.9.0-canary.3
|
||||
const meta = currentWorkspace.docCollection.doc.getMap('meta');
|
||||
const blockVersions = meta.get('blockVersions');
|
||||
if (
|
||||
!(blockVersions instanceof YMap) &&
|
||||
blockVersions !== null &&
|
||||
blockVersions !== undefined &&
|
||||
typeof blockVersions === 'object'
|
||||
) {
|
||||
meta.set(
|
||||
'blockVersions',
|
||||
new YMap(Object.entries(blockVersions as Record<string, number>))
|
||||
);
|
||||
}
|
||||
}, [currentWorkspace.docCollection.doc]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* This DndContext is used for drag page from all-pages list into a folder in sidebar */}
|
||||
{children}
|
||||
<QuickSearchContainer />
|
||||
<SyncAwareness />
|
||||
<OverCapacityNotification />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const DesktopLayout = ({ children }: PropsWithChildren) => {
|
||||
// is there a better way to make sure service is always available even if it's not explicitly used?
|
||||
useService(WorkspaceDesktopApiService);
|
||||
return (
|
||||
<div className={styles.desktopAppViewContainer}>
|
||||
<div className={styles.desktopTabsHeader}>
|
||||
<AppTabsHeader
|
||||
left={
|
||||
<>
|
||||
<SidebarSwitch show />
|
||||
<NavigationButtons />
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.desktopAppViewMain}>
|
||||
<RootAppSidebar />
|
||||
<MainContainer>{children}</MainContainer>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const BrowserLayout = ({ children }: PropsWithChildren) => {
|
||||
return (
|
||||
<div className={styles.browserAppViewContainer}>
|
||||
<OpenInAppCard />
|
||||
<RootAppSidebar />
|
||||
<MainContainer>{children}</MainContainer>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const LayoutComponent = BUILD_CONFIG.isElectron ? DesktopLayout : BrowserLayout;
|
||||
|
||||
/**
|
||||
* Wraps the workspace layout main router view
|
||||
*/
|
||||
const WorkspaceLayoutUIContainer = ({ children }: PropsWithChildren) => {
|
||||
const workbench = useService(WorkbenchService).workbench;
|
||||
const currentPath = useLiveData(
|
||||
LiveData.computed(get => {
|
||||
return get(workbench.basename$) + get(workbench.location$).pathname;
|
||||
})
|
||||
);
|
||||
|
||||
return (
|
||||
<AppContainer data-current-path={currentPath}>
|
||||
<LayoutComponent>{children}</LayoutComponent>
|
||||
</AppContainer>
|
||||
);
|
||||
};
|
||||
export const WorkspaceLayoutInner = ({ children }: PropsWithChildren) => {
|
||||
const workspace = useService(WorkspaceService).workspace;
|
||||
|
||||
const upgrading = useLiveData(workspace.upgrade.upgrading$);
|
||||
const needUpgrade = useLiveData(workspace.upgrade.needUpgrade$);
|
||||
|
||||
return (
|
||||
<WorkspaceLayoutProviders>
|
||||
<WorkspaceLayoutUIContainer>
|
||||
{needUpgrade || upgrading ? <WorkspaceUpgrade /> : children}
|
||||
</WorkspaceLayoutUIContainer>
|
||||
</WorkspaceLayoutProviders>
|
||||
);
|
||||
};
|
||||
@@ -1,9 +1,8 @@
|
||||
import { notify } from '@affine/component';
|
||||
import { openSettingModalAtom } from '@affine/core/components/atoms';
|
||||
import { GlobalDialogService } from '@affine/core/modules/dialogs';
|
||||
import { WorkspacePermissionService } from '@affine/core/modules/permissions';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import { useLiveData, useService, WorkspaceService } from '@toeverything/infra';
|
||||
import { useSetAtom } from 'jotai';
|
||||
import { debounce } from 'lodash-es';
|
||||
import { useCallback, useEffect } from 'react';
|
||||
|
||||
@@ -20,14 +19,13 @@ export const OverCapacityNotification = () => {
|
||||
permissionService.permission.revalidate();
|
||||
}, [permissionService]);
|
||||
|
||||
const setSettingModalAtom = useSetAtom(openSettingModalAtom);
|
||||
const globalDialogService = useService(GlobalDialogService);
|
||||
const jumpToPricePlan = useCallback(() => {
|
||||
setSettingModalAtom({
|
||||
open: true,
|
||||
globalDialogService.open('setting', {
|
||||
activeTab: 'plans',
|
||||
scrollAnchor: 'cloudPricingPlan',
|
||||
});
|
||||
}, [setSettingModalAtom]);
|
||||
}, [globalDialogService]);
|
||||
|
||||
// debounce sync engine status
|
||||
useEffect(() => {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
export * from './collection-list-header';
|
||||
export * from './collection-list-item';
|
||||
export * from './select-collection';
|
||||
export * from './virtualized-collection-list';
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
} from '@affine/component';
|
||||
import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks';
|
||||
import { useNavigateHelper } from '@affine/core/components/hooks/use-navigate-helper';
|
||||
import { WorkspaceDialogService } from '@affine/core/modules/dialogs';
|
||||
import type { Tag } from '@affine/core/modules/tag';
|
||||
import { TagService } from '@affine/core/modules/tag';
|
||||
import { isNewTabTrigger } from '@affine/core/utils';
|
||||
@@ -28,18 +29,13 @@ import {
|
||||
WorkspaceService,
|
||||
} from '@toeverything/infra';
|
||||
import clsx from 'clsx';
|
||||
import { nanoid } from 'nanoid';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import { CollectionService } from '../../../modules/collection';
|
||||
import { usePageHelper } from '../../blocksuite/block-suite-page-list/utils';
|
||||
import { createTagFilter } from '../filter/utils';
|
||||
import { createEmptyCollection } from '../use-collection-manager';
|
||||
import {
|
||||
useEditCollection,
|
||||
useEditCollectionName,
|
||||
} from '../view/use-edit-collection';
|
||||
import { SaveAsCollectionButton } from '../view';
|
||||
import * as styles from './page-list-header.css';
|
||||
import { PageListNewPageButton } from './page-list-new-page-button';
|
||||
|
||||
@@ -102,21 +98,22 @@ export const CollectionPageListHeader = ({
|
||||
}) => {
|
||||
const t = useI18n();
|
||||
const { jumpToCollections } = useNavigateHelper();
|
||||
const { collectionService, workspaceService } = useServices({
|
||||
CollectionService,
|
||||
WorkspaceService,
|
||||
});
|
||||
const { collectionService, workspaceService, workspaceDialogService } =
|
||||
useServices({
|
||||
CollectionService,
|
||||
WorkspaceService,
|
||||
WorkspaceDialogService,
|
||||
});
|
||||
|
||||
const handleJumpToCollections = useCallback(() => {
|
||||
jumpToCollections(workspaceId);
|
||||
}, [jumpToCollections, workspaceId]);
|
||||
|
||||
const { open } = useEditCollection();
|
||||
|
||||
const handleEdit = useAsyncCallback(async () => {
|
||||
const ret = await open({ ...collection }, 'page');
|
||||
collectionService.updateCollection(collection.id, () => ret);
|
||||
}, [collection, collectionService, open]);
|
||||
const handleEdit = useCallback(() => {
|
||||
workspaceDialogService.open('collection-editor', {
|
||||
collectionId: collection.id,
|
||||
});
|
||||
}, [collection, workspaceDialogService]);
|
||||
|
||||
const workspace = workspaceService.workspace;
|
||||
const { createEdgeless, createPage } = usePageHelper(workspace.docCollection);
|
||||
@@ -203,10 +200,6 @@ export const TagPageListHeader = ({
|
||||
const { jumpToTags, jumpToCollection } = useNavigateHelper();
|
||||
const collectionService = useService(CollectionService);
|
||||
const [openMenu, setOpenMenu] = useState(false);
|
||||
const { open } = useEditCollectionName({
|
||||
title: t['com.affine.editCollection.saveCollection'](),
|
||||
showTips: true,
|
||||
});
|
||||
|
||||
const handleJumpToTags = useCallback(() => {
|
||||
jumpToTags(workspaceId);
|
||||
@@ -222,15 +215,6 @@ export const TagPageListHeader = ({
|
||||
},
|
||||
[collectionService, tag.id, jumpToCollection, workspaceId]
|
||||
);
|
||||
const handleClick = useCallback(() => {
|
||||
open('')
|
||||
.then(name => {
|
||||
return saveToCollection(createEmptyCollection(nanoid(), { name }));
|
||||
})
|
||||
.catch(err => {
|
||||
console.error(err);
|
||||
});
|
||||
}, [open, saveToCollection]);
|
||||
|
||||
return (
|
||||
<div className={styles.docListHeader}>
|
||||
@@ -267,9 +251,7 @@ export const TagPageListHeader = ({
|
||||
</div>
|
||||
</Menu>
|
||||
</div>
|
||||
<Button onClick={handleClick}>
|
||||
{t['com.affine.editCollection.saveCollection']()}
|
||||
</Button>
|
||||
<SaveAsCollectionButton onConfirm={saveToCollection} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import { style } from '@vanilla-extract/css';
|
||||
|
||||
export const pagesTab = style({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
overflow: 'hidden',
|
||||
});
|
||||
export const pagesTabContent = style({
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
gap: 8,
|
||||
alignItems: 'center',
|
||||
padding: '16px 16px 8px 16px',
|
||||
});
|
||||
|
||||
export const pageList = style({
|
||||
width: '100%',
|
||||
});
|
||||
|
||||
export const ellipsis = style({
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
});
|
||||
@@ -13,18 +13,16 @@ import {
|
||||
} from '@toeverything/infra';
|
||||
import { type ReactNode, useCallback, useEffect, useState } from 'react';
|
||||
|
||||
import { FavoriteTag } from '../../components/favorite-tag';
|
||||
import { FilterList } from '../../filter';
|
||||
import { VariableSelect } from '../../filter/vars';
|
||||
import { usePageHeaderColsDef } from '../../header-col-def';
|
||||
import { PageListItemRenderer } from '../../page-group';
|
||||
import { ListTableHeader } from '../../page-header';
|
||||
import type { BaseSelectorDialogProps } from '../../selector';
|
||||
import { SelectorLayout } from '../../selector/selector-layout';
|
||||
import type { ListItem } from '../../types';
|
||||
import { VirtualizedList } from '../../virtualized-list';
|
||||
import { AffineShapeIcon } from '../affine-shape';
|
||||
import * as styles from './edit-collection.css';
|
||||
import { AffineShapeIcon, FavoriteTag } from '..';
|
||||
import { FilterList } from '../filter';
|
||||
import { VariableSelect } from '../filter/vars';
|
||||
import { usePageHeaderColsDef } from '../header-col-def';
|
||||
import { PageListItemRenderer } from '../page-group';
|
||||
import { ListTableHeader } from '../page-header';
|
||||
import { SelectorLayout } from '../selector/selector-layout';
|
||||
import type { ListItem } from '../types';
|
||||
import { VirtualizedList } from '../virtualized-list';
|
||||
import * as styles from './select-page.css';
|
||||
import { useFilter } from './use-filter';
|
||||
import { useSearch } from './use-search';
|
||||
|
||||
@@ -40,7 +38,10 @@ export const SelectPage = ({
|
||||
confirmText?: ReactNode;
|
||||
header?: ReactNode;
|
||||
buttons?: ReactNode;
|
||||
} & BaseSelectorDialogProps<string[]>) => {
|
||||
init?: string[];
|
||||
onConfirm?: (data: string[]) => void;
|
||||
onCancel?: () => void;
|
||||
}) => {
|
||||
const t = useI18n();
|
||||
const [value, setValue] = useState(init);
|
||||
const onChange = useCallback(
|
||||
@@ -2,8 +2,10 @@ import type { Filter } from '@affine/env/filter';
|
||||
import type { MouseEvent } from 'react';
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
import type { PageDataForFilter } from '../../use-collection-manager';
|
||||
import { filterPageByRules } from '../../use-collection-manager';
|
||||
import {
|
||||
filterPageByRules,
|
||||
type PageDataForFilter,
|
||||
} from '../use-collection-manager';
|
||||
|
||||
export const useFilter = (list: PageDataForFilter[]) => {
|
||||
const [filters, changeFilters] = useState<Filter[]>([]);
|
||||
@@ -1,12 +1,11 @@
|
||||
import { toast } from '@affine/component';
|
||||
import { useTrashModalHelper } from '@affine/core/components/hooks/affine/use-trash-modal-helper';
|
||||
import { toast, useConfirmModal } from '@affine/component';
|
||||
import { useBlockSuiteDocMeta } from '@affine/core/components/hooks/use-block-suite-page-meta';
|
||||
import { CollectionService } from '@affine/core/modules/collection';
|
||||
import type { Tag } from '@affine/core/modules/tag';
|
||||
import type { Collection, Filter } from '@affine/env/filter';
|
||||
import { Trans, useI18n } from '@affine/i18n';
|
||||
import type { DocMeta } from '@blocksuite/affine/store';
|
||||
import { useService, WorkspaceService } from '@toeverything/infra';
|
||||
import { DocsService, useService, WorkspaceService } from '@toeverything/infra';
|
||||
import { useCallback, useMemo, useRef, useState } from 'react';
|
||||
|
||||
import { ListFloatingToolbar } from '../components/list-floating-toolbar';
|
||||
@@ -62,10 +61,12 @@ export const VirtualizedPageList = ({
|
||||
listItem?: DocMeta[];
|
||||
setHideHeaderCreateNewPage?: (hide: boolean) => void;
|
||||
}) => {
|
||||
const t = useI18n();
|
||||
const listRef = useRef<ItemListHandle>(null);
|
||||
const [showFloatingToolbar, setShowFloatingToolbar] = useState(false);
|
||||
const [selectedPageIds, setSelectedPageIds] = useState<string[]>([]);
|
||||
const currentWorkspace = useService(WorkspaceService).workspace;
|
||||
const docsService = useService(DocsService);
|
||||
const pageMetas = useBlockSuiteDocMeta(currentWorkspace.docCollection);
|
||||
const pageOperations = usePageOperationsRenderer();
|
||||
const pageHeaderColsDef = usePageHeaderColsDef();
|
||||
@@ -122,26 +123,39 @@ export const VirtualizedPageList = ({
|
||||
return <PageListHeader />;
|
||||
}, [collection, currentWorkspace.id, tag]);
|
||||
|
||||
const { setTrashModal } = useTrashModalHelper();
|
||||
const { openConfirmModal } = useConfirmModal();
|
||||
|
||||
const handleMultiDelete = useCallback(() => {
|
||||
if (filteredSelectedPageIds.length === 0) {
|
||||
return;
|
||||
}
|
||||
const pageNameMapping = Object.fromEntries(
|
||||
pageMetas.map(meta => [meta.id, meta.title])
|
||||
);
|
||||
|
||||
const pageNames = filteredSelectedPageIds.map(
|
||||
id => pageNameMapping[id] ?? ''
|
||||
);
|
||||
setTrashModal({
|
||||
open: true,
|
||||
pageIds: filteredSelectedPageIds,
|
||||
pageTitles: pageNames,
|
||||
openConfirmModal({
|
||||
title: t['com.affine.moveToTrash.confirmModal.title.multiple']({
|
||||
number: filteredSelectedPageIds.length.toString(),
|
||||
}),
|
||||
description: t[
|
||||
'com.affine.moveToTrash.confirmModal.description.multiple'
|
||||
]({
|
||||
number: filteredSelectedPageIds.length.toString(),
|
||||
}),
|
||||
cancelText: t['com.affine.confirmModal.button.cancel'](),
|
||||
confirmText: t.Delete(),
|
||||
onConfirm: () => {
|
||||
for (const docId of filteredSelectedPageIds) {
|
||||
const doc = docsService.list.doc$(docId).value;
|
||||
doc?.moveToTrash();
|
||||
}
|
||||
},
|
||||
});
|
||||
hideFloatingToolbar();
|
||||
}, [filteredSelectedPageIds, hideFloatingToolbar, pageMetas, setTrashModal]);
|
||||
}, [
|
||||
docsService.list,
|
||||
filteredSelectedPageIds,
|
||||
hideFloatingToolbar,
|
||||
openConfirmModal,
|
||||
t,
|
||||
]);
|
||||
|
||||
const group = usePageItemGroupDefinitions();
|
||||
|
||||
|
||||
@@ -4,11 +4,11 @@ import {
|
||||
MenuItem,
|
||||
toast,
|
||||
useConfirmModal,
|
||||
usePromptModal,
|
||||
} from '@affine/component';
|
||||
import { useBlockSuiteMetaHelper } from '@affine/core/components/hooks/affine/use-block-suite-meta-helper';
|
||||
import { useTrashModalHelper } from '@affine/core/components/hooks/affine/use-trash-modal-helper';
|
||||
import { useCatchEventCallback } from '@affine/core/components/hooks/use-catch-event-hook';
|
||||
import { DocInfoService } from '@affine/core/modules/doc-info';
|
||||
import { WorkspaceDialogService } from '@affine/core/modules/dialogs';
|
||||
import {
|
||||
CompatibleFavoriteItemsAdapter,
|
||||
FavoriteService,
|
||||
@@ -33,6 +33,7 @@ import {
|
||||
SplitViewIcon,
|
||||
} from '@blocksuite/icons/rc';
|
||||
import {
|
||||
DocsService,
|
||||
FeatureFlagService,
|
||||
useLiveData,
|
||||
useService,
|
||||
@@ -51,7 +52,6 @@ import { DisablePublicSharing, MoveToTrash } from './operation-menu-items';
|
||||
import { CreateOrEditTag } from './tags/create-tag';
|
||||
import type { TagMeta } from './types';
|
||||
import { ColWrapper } from './utils';
|
||||
import { useEditCollection, useEditCollectionName } from './view';
|
||||
|
||||
const tooltipSideTop = { side: 'top' as const };
|
||||
const tooltipSideTopAlignEnd = { side: 'top' as const, align: 'end' as const };
|
||||
@@ -83,17 +83,19 @@ export const PageOperationCell = ({
|
||||
featureFlagService.flags.enable_multi_view.$
|
||||
);
|
||||
const currentWorkspace = workspaceService.workspace;
|
||||
const { setTrashModal } = useTrashModalHelper();
|
||||
const favourite = useLiveData(favAdapter.isFavorite$(page.id, 'doc'));
|
||||
const workbench = workbenchService.workbench;
|
||||
const { duplicate } = useBlockSuiteMetaHelper();
|
||||
const docRecord = useLiveData(useService(DocsService).list.doc$(page.id));
|
||||
const blocksuiteDoc = currentWorkspace.docCollection.getDoc(page.id);
|
||||
|
||||
const docInfoModal = useService(DocInfoService).modal;
|
||||
const workspaceDialogService = useService(WorkspaceDialogService);
|
||||
const onOpenInfoModal = useCallback(() => {
|
||||
track.$.docInfoPanel.$.open();
|
||||
docInfoModal.open(blocksuiteDoc?.id);
|
||||
}, [blocksuiteDoc?.id, docInfoModal]);
|
||||
if (blocksuiteDoc?.id) {
|
||||
track.$.docInfoPanel.$.open();
|
||||
workspaceDialogService.open('doc-info', { docId: blocksuiteDoc.id });
|
||||
}
|
||||
}, [blocksuiteDoc?.id, workspaceDialogService]);
|
||||
|
||||
const onDisablePublicSharing = useCallback(() => {
|
||||
// TODO(@EYHN): implement disable public sharing
|
||||
@@ -102,15 +104,26 @@ export const PageOperationCell = ({
|
||||
});
|
||||
}, []);
|
||||
|
||||
const { openConfirmModal } = useConfirmModal();
|
||||
|
||||
const onRemoveToTrash = useCallback(() => {
|
||||
if (!docRecord) {
|
||||
return;
|
||||
}
|
||||
track.allDocs.list.docMenu.deleteDoc();
|
||||
|
||||
setTrashModal({
|
||||
open: true,
|
||||
pageIds: [page.id],
|
||||
pageTitles: [page.title],
|
||||
openConfirmModal({
|
||||
title: t['com.affine.moveToTrash.confirmModal.title'](),
|
||||
description: t['com.affine.moveToTrash.confirmModal.description']({
|
||||
title: docRecord.title$.value || t['Untitled'](),
|
||||
}),
|
||||
cancelText: t['com.affine.confirmModal.button.cancel'](),
|
||||
confirmText: t.Delete(),
|
||||
onConfirm: () => {
|
||||
docRecord.moveToTrash();
|
||||
},
|
||||
});
|
||||
}, [page.id, page.title, setTrashModal]);
|
||||
}, [docRecord, openConfirmModal, t]);
|
||||
|
||||
const onOpenInSplitView = useCallback(() => {
|
||||
track.allDocs.list.docMenu.openInSplitView();
|
||||
@@ -297,11 +310,15 @@ export const CollectionOperationCell = ({
|
||||
info,
|
||||
}: CollectionOperationCellProps) => {
|
||||
const t = useI18n();
|
||||
const { compatibleFavoriteItemsAdapter: favAdapter, workspaceService } =
|
||||
useServices({
|
||||
CompatibleFavoriteItemsAdapter,
|
||||
WorkspaceService,
|
||||
});
|
||||
const {
|
||||
compatibleFavoriteItemsAdapter: favAdapter,
|
||||
workspaceService,
|
||||
workspaceDialogService,
|
||||
} = useServices({
|
||||
CompatibleFavoriteItemsAdapter,
|
||||
WorkspaceService,
|
||||
WorkspaceDialogService,
|
||||
});
|
||||
const docCollection = workspaceService.workspace.docCollection;
|
||||
const { createPage } = usePageHelper(docCollection);
|
||||
const { openConfirmModal } = useConfirmModal();
|
||||
@@ -309,11 +326,7 @@ export const CollectionOperationCell = ({
|
||||
favAdapter.isFavorite$(collection.id, 'collection')
|
||||
);
|
||||
|
||||
const { open: openEditCollectionModal } = useEditCollection();
|
||||
|
||||
const { open: openEditCollectionNameModal } = useEditCollectionName({
|
||||
title: t['com.affine.editCollection.renameCollection'](),
|
||||
});
|
||||
const { openPromptModal } = usePromptModal();
|
||||
|
||||
const handlePropagation = useCallback((event: MouseEvent) => {
|
||||
event.preventDefault();
|
||||
@@ -323,39 +336,36 @@ export const CollectionOperationCell = ({
|
||||
const handleEditName = useCallback(
|
||||
(event: MouseEvent) => {
|
||||
handlePropagation(event);
|
||||
// use openRenameModal if it is in the sidebar collection list
|
||||
openEditCollectionNameModal(collection.name)
|
||||
.then(name => {
|
||||
return service.updateCollection(collection.id, collection => ({
|
||||
openPromptModal({
|
||||
title: t['com.affine.editCollection.renameCollection'](),
|
||||
label: t['com.affine.editCollectionName.name'](),
|
||||
inputOptions: {
|
||||
placeholder: t['com.affine.editCollectionName.name.placeholder'](),
|
||||
},
|
||||
confirmText: t['com.affine.editCollection.save'](),
|
||||
cancelText: t['com.affine.editCollection.button.cancel'](),
|
||||
confirmButtonOptions: {
|
||||
variant: 'primary',
|
||||
},
|
||||
onConfirm(name) {
|
||||
service.updateCollection(collection.id, () => ({
|
||||
...collection,
|
||||
name,
|
||||
}));
|
||||
})
|
||||
.catch(err => {
|
||||
console.error(err);
|
||||
});
|
||||
},
|
||||
});
|
||||
},
|
||||
[
|
||||
collection.id,
|
||||
collection.name,
|
||||
handlePropagation,
|
||||
openEditCollectionNameModal,
|
||||
service,
|
||||
]
|
||||
[collection, handlePropagation, openPromptModal, service, t]
|
||||
);
|
||||
|
||||
const handleEdit = useCallback(
|
||||
(event: MouseEvent) => {
|
||||
handlePropagation(event);
|
||||
openEditCollectionModal(collection)
|
||||
.then(collection => {
|
||||
return service.updateCollection(collection.id, () => collection);
|
||||
})
|
||||
.catch(err => {
|
||||
console.error(err);
|
||||
});
|
||||
workspaceDialogService.open('collection-editor', {
|
||||
collectionId: collection.id,
|
||||
});
|
||||
},
|
||||
[handlePropagation, openEditCollectionModal, collection, service]
|
||||
[handlePropagation, workspaceDialogService, collection.id]
|
||||
);
|
||||
|
||||
const handleDelete = useCallback(() => {
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
import { SelectCollection } from '../collections';
|
||||
import { SelectTag } from '../tags';
|
||||
import { SelectPage } from '../view/edit-collection/select-page';
|
||||
import { useSelectDialog } from './use-select-dialog';
|
||||
|
||||
export * from './use-select-dialog';
|
||||
|
||||
/**
|
||||
* Return a `open` function to open the select collection dialog.
|
||||
*/
|
||||
export const useSelectCollection = () => {
|
||||
return useSelectDialog(SelectCollection, 'select-collection');
|
||||
};
|
||||
|
||||
/**
|
||||
* Return a `open` function to open the select page dialog.
|
||||
*/
|
||||
export const useSelectDoc = () => {
|
||||
return useSelectDialog(SelectPage, 'select-doc-dialog');
|
||||
};
|
||||
|
||||
/**
|
||||
* Return a `open` function to open the select tag dialog.
|
||||
*/
|
||||
export const useSelectTag = () => {
|
||||
return useSelectDialog(SelectTag, 'select-tag-dialog');
|
||||
};
|
||||
@@ -1,102 +0,0 @@
|
||||
import { Modal, type ModalProps } from '@affine/component';
|
||||
import { useMount } from '@toeverything/infra';
|
||||
import { cssVar } from '@toeverything/theme';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
export interface BaseSelectorDialogProps<T> {
|
||||
init?: T;
|
||||
onConfirm?: (data: T) => void;
|
||||
onCancel?: () => void;
|
||||
}
|
||||
|
||||
const defaultModalProps: Partial<Omit<ModalProps, 'children'>> = {};
|
||||
export const useSelectDialog = function useSelectDialog<T, P>(
|
||||
Component: React.FC<BaseSelectorDialogProps<T> & P>,
|
||||
debugKey?: string,
|
||||
options?: {
|
||||
modalProps?: Partial<Omit<ModalProps, 'children'>>;
|
||||
}
|
||||
) {
|
||||
// to control whether the dialog is open, it's not equal to !!value
|
||||
// when closing the dialog, show will be `false` first, then after the animation, value turns to `undefined`
|
||||
const [show, setShow] = useState(false);
|
||||
const [value, setValue] = useState<{
|
||||
init?: T;
|
||||
onConfirm: (v: T) => void;
|
||||
}>();
|
||||
const [additionalProps, setAdditionalProps] = useState<P>();
|
||||
|
||||
const onOpenChanged = useCallback((open: boolean) => {
|
||||
if (!open) setValue(undefined);
|
||||
setShow(open);
|
||||
}, []);
|
||||
|
||||
const close = useCallback(() => setShow(false), []);
|
||||
|
||||
/**
|
||||
* Open a dialog to select items
|
||||
*/
|
||||
const open = useCallback(
|
||||
(ids?: T, additionalProps?: P) => {
|
||||
return new Promise<T>(resolve => {
|
||||
setShow(true);
|
||||
setAdditionalProps(additionalProps);
|
||||
setValue({
|
||||
init: ids,
|
||||
onConfirm: list => {
|
||||
close();
|
||||
resolve(list);
|
||||
},
|
||||
});
|
||||
});
|
||||
},
|
||||
[close]
|
||||
);
|
||||
|
||||
const { mount } = useMount(debugKey);
|
||||
|
||||
useEffect(() => {
|
||||
const { contentOptions, ...otherModalProps } =
|
||||
options?.modalProps ?? defaultModalProps;
|
||||
|
||||
return mount(
|
||||
<Modal
|
||||
open={show}
|
||||
onOpenChange={onOpenChanged}
|
||||
withoutCloseButton
|
||||
width="calc(100% - 32px)"
|
||||
height="80%"
|
||||
contentOptions={{
|
||||
style: {
|
||||
padding: 0,
|
||||
maxWidth: 976,
|
||||
background: cssVar('backgroundPrimaryColor'),
|
||||
},
|
||||
...contentOptions,
|
||||
}}
|
||||
{...otherModalProps}
|
||||
>
|
||||
{value ? (
|
||||
<Component
|
||||
init={value.init}
|
||||
onCancel={close}
|
||||
onConfirm={value.onConfirm}
|
||||
{...(additionalProps as any)}
|
||||
/>
|
||||
) : null}
|
||||
</Modal>
|
||||
);
|
||||
}, [
|
||||
Component,
|
||||
additionalProps,
|
||||
close,
|
||||
debugKey,
|
||||
mount,
|
||||
onOpenChanged,
|
||||
options?.modalProps,
|
||||
show,
|
||||
value,
|
||||
]);
|
||||
|
||||
return open;
|
||||
};
|
||||
@@ -1,4 +1,3 @@
|
||||
export * from './select-tag';
|
||||
export * from './tag-list-header';
|
||||
export * from './tag-list-item';
|
||||
export * from './virtualized-tag-list';
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { MenuItemProps } from '@affine/component';
|
||||
import { Menu, MenuItem } from '@affine/component';
|
||||
import { Menu, MenuItem, usePromptModal } from '@affine/component';
|
||||
import { useDeleteCollectionInfo } from '@affine/core/components/hooks/affine/use-delete-collection-info';
|
||||
import { WorkspaceDialogService } from '@affine/core/modules/dialogs';
|
||||
import { CompatibleFavoriteItemsAdapter } from '@affine/core/modules/favorite';
|
||||
import { WorkbenchService } from '@affine/core/modules/workbench';
|
||||
import type { Collection } from '@affine/env/filter';
|
||||
@@ -25,10 +26,6 @@ import { useCallback, useMemo } from 'react';
|
||||
import { CollectionService } from '../../../modules/collection';
|
||||
import { IsFavoriteIcon } from '../../pure/icons';
|
||||
import * as styles from './collection-operations.css';
|
||||
import {
|
||||
useEditCollection,
|
||||
useEditCollectionName,
|
||||
} from './use-edit-collection';
|
||||
|
||||
export const CollectionOperations = ({
|
||||
collection,
|
||||
@@ -44,18 +41,17 @@ export const CollectionOperations = ({
|
||||
collectionService: service,
|
||||
workbenchService,
|
||||
featureFlagService,
|
||||
workspaceDialogService,
|
||||
} = useServices({
|
||||
CollectionService,
|
||||
WorkbenchService,
|
||||
FeatureFlagService,
|
||||
WorkspaceDialogService,
|
||||
});
|
||||
const deleteInfo = useDeleteCollectionInfo();
|
||||
const workbench = workbenchService.workbench;
|
||||
const { open: openEditCollectionModal } = useEditCollection();
|
||||
const t = useI18n();
|
||||
const { open: openEditCollectionNameModal } = useEditCollectionName({
|
||||
title: t['com.affine.editCollection.renameCollection'](),
|
||||
});
|
||||
const { openPromptModal } = usePromptModal();
|
||||
const enableMultiView = useLiveData(
|
||||
featureFlagService.flags.enable_multi_view.$
|
||||
);
|
||||
@@ -65,27 +61,31 @@ export const CollectionOperations = ({
|
||||
if (openRenameModal) {
|
||||
return openRenameModal();
|
||||
}
|
||||
openEditCollectionNameModal(collection.name)
|
||||
.then(name => {
|
||||
return service.updateCollection(collection.id, () => ({
|
||||
openPromptModal({
|
||||
title: t['com.affine.editCollection.renameCollection'](),
|
||||
label: t['com.affine.editCollectionName.name'](),
|
||||
inputOptions: {
|
||||
placeholder: t['com.affine.editCollectionName.name.placeholder'](),
|
||||
},
|
||||
confirmText: t['com.affine.editCollection.save'](),
|
||||
cancelText: t['com.affine.editCollection.button.cancel'](),
|
||||
confirmButtonOptions: {
|
||||
variant: 'primary',
|
||||
},
|
||||
onConfirm(name) {
|
||||
service.updateCollection(collection.id, () => ({
|
||||
...collection,
|
||||
name,
|
||||
}));
|
||||
})
|
||||
.catch(err => {
|
||||
console.error(err);
|
||||
});
|
||||
}, [openRenameModal, openEditCollectionNameModal, collection, service]);
|
||||
},
|
||||
});
|
||||
}, [openRenameModal, openPromptModal, t, service, collection]);
|
||||
|
||||
const showEdit = useCallback(() => {
|
||||
openEditCollectionModal(collection)
|
||||
.then(collection => {
|
||||
return service.updateCollection(collection.id, () => collection);
|
||||
})
|
||||
.catch(err => {
|
||||
console.error(err);
|
||||
});
|
||||
}, [openEditCollectionModal, collection, service]);
|
||||
workspaceDialogService.open('collection-editor', {
|
||||
collectionId: collection.id,
|
||||
});
|
||||
}, [workspaceDialogService, collection.id]);
|
||||
|
||||
const openCollectionSplitView = useCallback(() => {
|
||||
workbench.openCollection(collection.id, { at: 'tail' });
|
||||
|
||||
@@ -2,6 +2,4 @@ export * from './affine-shape';
|
||||
export * from './collection-list';
|
||||
export * from './collection-operations';
|
||||
export * from './create-collection';
|
||||
export * from './edit-collection/edit-collection';
|
||||
export * from './save-as-collection-button';
|
||||
export * from './use-edit-collection';
|
||||
|
||||
@@ -8,3 +8,8 @@ export const button = style({
|
||||
fontWeight: 500,
|
||||
height: '28px',
|
||||
});
|
||||
export const createTips = style({
|
||||
color: cssVar('textSecondaryColor'),
|
||||
fontSize: 12,
|
||||
lineHeight: '20px',
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Button } from '@affine/component';
|
||||
import { Button, usePromptModal } from '@affine/component';
|
||||
import type { Collection } from '@affine/env/filter';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import { SaveIcon } from '@blocksuite/icons/rc';
|
||||
@@ -7,7 +7,6 @@ import { useCallback } from 'react';
|
||||
|
||||
import { createEmptyCollection } from '../use-collection-manager';
|
||||
import * as styles from './save-as-collection-button.css';
|
||||
import { useEditCollectionName } from './use-edit-collection';
|
||||
|
||||
interface SaveAsCollectionButtonProps {
|
||||
onConfirm: (collection: Collection) => void;
|
||||
@@ -17,19 +16,29 @@ export const SaveAsCollectionButton = ({
|
||||
onConfirm,
|
||||
}: SaveAsCollectionButtonProps) => {
|
||||
const t = useI18n();
|
||||
const { open } = useEditCollectionName({
|
||||
title: t['com.affine.editCollection.saveCollection'](),
|
||||
showTips: true,
|
||||
});
|
||||
const { openPromptModal } = usePromptModal();
|
||||
const handleClick = useCallback(() => {
|
||||
open('')
|
||||
.then(name => {
|
||||
return onConfirm(createEmptyCollection(nanoid(), { name }));
|
||||
})
|
||||
.catch(err => {
|
||||
console.error(err);
|
||||
});
|
||||
}, [open, onConfirm]);
|
||||
openPromptModal({
|
||||
title: t['com.affine.editCollection.saveCollection'](),
|
||||
label: t['com.affine.editCollectionName.name'](),
|
||||
inputOptions: {
|
||||
placeholder: t['com.affine.editCollectionName.name.placeholder'](),
|
||||
},
|
||||
children: (
|
||||
<div className={styles.createTips}>
|
||||
{t['com.affine.editCollectionName.createTips']()}
|
||||
</div>
|
||||
),
|
||||
confirmText: t['com.affine.editCollection.save'](),
|
||||
cancelText: t['com.affine.editCollection.button.cancel'](),
|
||||
confirmButtonOptions: {
|
||||
variant: 'primary',
|
||||
},
|
||||
onConfirm(name) {
|
||||
onConfirm(createEmptyCollection(nanoid(), { name }));
|
||||
},
|
||||
});
|
||||
}, [openPromptModal, t, onConfirm]);
|
||||
return (
|
||||
<Button
|
||||
onClick={handleClick}
|
||||
|
||||
@@ -1,97 +0,0 @@
|
||||
import type { Collection } from '@affine/env/filter';
|
||||
import { useMount } from '@toeverything/infra';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
import { CreateCollectionModal } from './create-collection';
|
||||
import {
|
||||
EditCollectionModal,
|
||||
type EditCollectionMode,
|
||||
} from './edit-collection/edit-collection';
|
||||
|
||||
export const useEditCollection = () => {
|
||||
const [data, setData] = useState<{
|
||||
collection: Collection;
|
||||
mode?: 'page' | 'rule';
|
||||
onConfirm: (collection: Collection) => void;
|
||||
}>();
|
||||
const close = useCallback((open: boolean) => {
|
||||
if (!open) {
|
||||
setData(undefined);
|
||||
}
|
||||
}, []);
|
||||
const { mount } = useMount('useEditCollection');
|
||||
|
||||
useEffect(() => {
|
||||
if (!data) return;
|
||||
return mount(
|
||||
<EditCollectionModal
|
||||
init={data?.collection}
|
||||
open={!!data}
|
||||
mode={data?.mode}
|
||||
onOpenChange={close}
|
||||
onConfirm={data?.onConfirm ?? (() => {})}
|
||||
/>
|
||||
);
|
||||
}, [close, data, mount]);
|
||||
|
||||
return {
|
||||
open: (
|
||||
collection: Collection,
|
||||
mode?: EditCollectionMode
|
||||
): Promise<Collection> =>
|
||||
new Promise<Collection>(res => {
|
||||
setData({
|
||||
collection,
|
||||
mode,
|
||||
onConfirm: collection => {
|
||||
res(collection);
|
||||
},
|
||||
});
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
||||
export const useEditCollectionName = ({
|
||||
title,
|
||||
showTips,
|
||||
}: {
|
||||
title: string;
|
||||
showTips?: boolean;
|
||||
}) => {
|
||||
const [data, setData] = useState<{
|
||||
name: string;
|
||||
onConfirm: (name: string) => void;
|
||||
}>();
|
||||
const close = useCallback((open: boolean) => {
|
||||
if (!open) {
|
||||
setData(undefined);
|
||||
}
|
||||
}, []);
|
||||
const { mount } = useMount('useEditCollectionName');
|
||||
|
||||
useEffect(() => {
|
||||
if (!data) return;
|
||||
return mount(
|
||||
<CreateCollectionModal
|
||||
showTips={showTips}
|
||||
title={title}
|
||||
init={data?.name ?? ''}
|
||||
open={!!data}
|
||||
onOpenChange={close}
|
||||
onConfirm={data?.onConfirm ?? (() => {})}
|
||||
/>
|
||||
);
|
||||
}, [close, data, mount, showTips, title]);
|
||||
|
||||
return {
|
||||
open: (name: string): Promise<string> =>
|
||||
new Promise<string>(res => {
|
||||
setData({
|
||||
name,
|
||||
onConfirm: collection => {
|
||||
res(collection);
|
||||
},
|
||||
});
|
||||
}),
|
||||
};
|
||||
};
|
||||
@@ -1,218 +0,0 @@
|
||||
import { NotificationCenter, notify } from '@affine/component';
|
||||
import { DesktopApiService } from '@affine/core/modules/desktop-api/service';
|
||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
import {
|
||||
GlobalContextService,
|
||||
useLiveData,
|
||||
useService,
|
||||
useServiceOptional,
|
||||
WorkspaceService,
|
||||
WorkspacesService,
|
||||
} from '@toeverything/infra';
|
||||
import { useAtom } from 'jotai';
|
||||
import type { ReactElement } from 'react';
|
||||
import { useCallback, useEffect } from 'react';
|
||||
|
||||
import { AuthService } from '../../modules/cloud/services/auth';
|
||||
import { CreateWorkspaceDialogProvider } from '../../modules/create-workspace';
|
||||
import { FindInPageModal } from '../../modules/find-in-page/view/find-in-page-modal';
|
||||
import { ImportTemplateDialogProvider } from '../../modules/import-template';
|
||||
import { PeekViewManagerModal } from '../../modules/peek-view';
|
||||
import { AuthModal } from '../affine/auth';
|
||||
import { AiLoginRequiredModal } from '../affine/auth/ai-login-required';
|
||||
import { HistoryTipsModal } from '../affine/history-tips-modal';
|
||||
import { ImportModal } from '../affine/import-modal';
|
||||
import { IssueFeedbackModal } from '../affine/issue-feedback-modal';
|
||||
import {
|
||||
CloudQuotaModal,
|
||||
LocalQuotaModal,
|
||||
} from '../affine/quota-reached-modal';
|
||||
import { SettingModal } from '../affine/setting-modal';
|
||||
import { SignOutModal } from '../affine/sign-out-modal';
|
||||
import { StarAFFiNEModal } from '../affine/star-affine-modal';
|
||||
import type { SettingAtom } from '../atoms';
|
||||
import {
|
||||
openImportModalAtom,
|
||||
openSettingModalAtom,
|
||||
openSignOutModalAtom,
|
||||
} from '../atoms';
|
||||
import { InfoModal } from '../doc-properties/info-modal/info-modal';
|
||||
import { useTrashModalHelper } from '../hooks/affine/use-trash-modal-helper';
|
||||
import { useAsyncCallback } from '../hooks/affine-async-hooks';
|
||||
import { useNavigateHelper } from '../hooks/use-navigate-helper';
|
||||
import { MoveToTrash } from '../page-list';
|
||||
|
||||
export const Setting = () => {
|
||||
const [{ open, workspaceMetadata, activeTab }, setOpenSettingModalAtom] =
|
||||
useAtom(openSettingModalAtom);
|
||||
|
||||
const onSettingClick = useCallback(
|
||||
({
|
||||
activeTab,
|
||||
workspaceMetadata,
|
||||
}: Pick<SettingAtom, 'activeTab' | 'workspaceMetadata'>) => {
|
||||
setOpenSettingModalAtom(prev => ({
|
||||
...prev,
|
||||
activeTab,
|
||||
workspaceMetadata,
|
||||
}));
|
||||
},
|
||||
[setOpenSettingModalAtom]
|
||||
);
|
||||
|
||||
const onOpenChange = useCallback(
|
||||
(open: boolean) => {
|
||||
setOpenSettingModalAtom(prev => ({ ...prev, open }));
|
||||
},
|
||||
[setOpenSettingModalAtom]
|
||||
);
|
||||
|
||||
const desktopApi = useServiceOptional(DesktopApiService);
|
||||
|
||||
useEffect(() => {
|
||||
return desktopApi?.events?.applicationMenu.openAboutPageInSettingModal(() =>
|
||||
setOpenSettingModalAtom({
|
||||
activeTab: 'about',
|
||||
open: true,
|
||||
})
|
||||
);
|
||||
}, [desktopApi?.events?.applicationMenu, setOpenSettingModalAtom]);
|
||||
|
||||
if (!open) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<SettingModal
|
||||
open={open}
|
||||
activeTab={activeTab}
|
||||
workspaceMetadata={workspaceMetadata}
|
||||
onSettingClick={onSettingClick}
|
||||
onOpenChange={onOpenChange}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export function CurrentWorkspaceModals() {
|
||||
const currentWorkspace = useService(WorkspaceService).workspace;
|
||||
|
||||
const { trashModal, setTrashModal, handleOnConfirm } = useTrashModalHelper();
|
||||
const deletePageTitles = trashModal.pageTitles;
|
||||
const trashConfirmOpen = trashModal.open;
|
||||
const onTrashConfirmOpenChange = useCallback(
|
||||
(open: boolean) => {
|
||||
setTrashModal({
|
||||
...trashModal,
|
||||
open,
|
||||
});
|
||||
},
|
||||
[trashModal, setTrashModal]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<StarAFFiNEModal />
|
||||
<IssueFeedbackModal />
|
||||
{currentWorkspace ? <Setting /> : null}
|
||||
{currentWorkspace?.flavour === WorkspaceFlavour.LOCAL && (
|
||||
<>
|
||||
<LocalQuotaModal />
|
||||
<HistoryTipsModal />
|
||||
</>
|
||||
)}
|
||||
{currentWorkspace?.flavour === WorkspaceFlavour.AFFINE_CLOUD && (
|
||||
<CloudQuotaModal />
|
||||
)}
|
||||
<AiLoginRequiredModal />
|
||||
<PeekViewManagerModal />
|
||||
{BUILD_CONFIG.isElectron && <FindInPageModal />}
|
||||
<MoveToTrash.ConfirmModal
|
||||
open={trashConfirmOpen}
|
||||
onConfirm={handleOnConfirm}
|
||||
onOpenChange={onTrashConfirmOpenChange}
|
||||
titles={deletePageTitles}
|
||||
/>
|
||||
{currentWorkspace ? <InfoModal /> : null}
|
||||
<Import />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export const SignOutConfirmModal = () => {
|
||||
const { openPage } = useNavigateHelper();
|
||||
const authService = useService(AuthService);
|
||||
const [open, setOpen] = useAtom(openSignOutModalAtom);
|
||||
const globalContextService = useService(GlobalContextService);
|
||||
const currentWorkspaceId = useLiveData(
|
||||
globalContextService.globalContext.workspaceId.$
|
||||
);
|
||||
const workspacesService = useService(WorkspacesService);
|
||||
const workspaces = useLiveData(workspacesService.list.workspaces$);
|
||||
const currentWorkspaceMetadata = useLiveData(
|
||||
currentWorkspaceId
|
||||
? workspacesService.list.workspace$(currentWorkspaceId)
|
||||
: undefined
|
||||
);
|
||||
|
||||
const onConfirm = useAsyncCallback(async () => {
|
||||
setOpen(false);
|
||||
try {
|
||||
await authService.signOut();
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
// TODO(@eyhn): i18n
|
||||
notify.error({
|
||||
title: 'Failed to sign out',
|
||||
});
|
||||
}
|
||||
|
||||
// if current workspace is affine cloud, switch to local workspace
|
||||
if (currentWorkspaceMetadata?.flavour === WorkspaceFlavour.AFFINE_CLOUD) {
|
||||
const localWorkspace = workspaces.find(
|
||||
w => w.flavour === WorkspaceFlavour.LOCAL
|
||||
);
|
||||
if (localWorkspace) {
|
||||
openPage(localWorkspace.id, 'all');
|
||||
}
|
||||
}
|
||||
}, [
|
||||
authService,
|
||||
currentWorkspaceMetadata?.flavour,
|
||||
openPage,
|
||||
setOpen,
|
||||
workspaces,
|
||||
]);
|
||||
|
||||
return (
|
||||
<SignOutModal open={open} onOpenChange={setOpen} onConfirm={onConfirm} />
|
||||
);
|
||||
};
|
||||
|
||||
export const AllWorkspaceModals = (): ReactElement => {
|
||||
return (
|
||||
<>
|
||||
<NotificationCenter />
|
||||
<ImportTemplateDialogProvider />
|
||||
<CreateWorkspaceDialogProvider />
|
||||
<AuthModal />
|
||||
<SignOutConfirmModal />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const Import = () => {
|
||||
const [open, setOpenImportModalAtom] = useAtom(openImportModalAtom);
|
||||
|
||||
const onOpenChange = useCallback(
|
||||
(open: boolean) => {
|
||||
setOpenImportModalAtom(open);
|
||||
},
|
||||
[setOpenImportModalAtom]
|
||||
);
|
||||
|
||||
if (!open) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <ImportModal open={open} onOpenChange={onOpenChange} />;
|
||||
};
|
||||
@@ -0,0 +1,164 @@
|
||||
import { toast } from '@affine/component';
|
||||
import {
|
||||
pushGlobalLoadingEventAtom,
|
||||
resolveGlobalLoadingEventAtom,
|
||||
} from '@affine/component/global-loading';
|
||||
import { AIProvider } from '@affine/core/blocksuite/presets/ai';
|
||||
import { SyncAwareness } from '@affine/core/components/affine/awareness';
|
||||
import { useRegisterFindInPageCommands } from '@affine/core/components/hooks/affine/use-register-find-in-page-commands';
|
||||
import { useRegisterWorkspaceCommands } from '@affine/core/components/hooks/use-register-workspace-commands';
|
||||
import { OverCapacityNotification } from '@affine/core/components/over-capacity';
|
||||
import { GlobalDialogService } from '@affine/core/modules/dialogs';
|
||||
import { EditorSettingService } from '@affine/core/modules/editor-setting';
|
||||
import { useRegisterNavigationCommands } from '@affine/core/modules/navigation/view/use-register-navigation-commands';
|
||||
import { QuickSearchContainer } from '@affine/core/modules/quicksearch';
|
||||
import { WorkbenchService } from '@affine/core/modules/workbench';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import track from '@affine/track';
|
||||
import { type DocMode, ZipTransformer } from '@blocksuite/affine/blocks';
|
||||
import {
|
||||
DocsService,
|
||||
effect,
|
||||
fromPromise,
|
||||
onStart,
|
||||
throwIfAborted,
|
||||
useService,
|
||||
useServices,
|
||||
WorkspaceService,
|
||||
} from '@toeverything/infra';
|
||||
import { useSetAtom } from 'jotai';
|
||||
import { useEffect } from 'react';
|
||||
import {
|
||||
catchError,
|
||||
EMPTY,
|
||||
finalize,
|
||||
mergeMap,
|
||||
switchMap,
|
||||
timeout,
|
||||
} from 'rxjs';
|
||||
import { Map as YMap } from 'yjs';
|
||||
|
||||
/**
|
||||
* @deprecated just for legacy code, will be removed in the future
|
||||
*/
|
||||
export const WorkspaceSideEffects = () => {
|
||||
const t = useI18n();
|
||||
const pushGlobalLoadingEvent = useSetAtom(pushGlobalLoadingEventAtom);
|
||||
const resolveGlobalLoadingEvent = useSetAtom(resolveGlobalLoadingEventAtom);
|
||||
const { workspaceService, docsService } = useServices({
|
||||
WorkspaceService,
|
||||
DocsService,
|
||||
EditorSettingService,
|
||||
});
|
||||
const currentWorkspace = workspaceService.workspace;
|
||||
const docsList = docsService.list;
|
||||
|
||||
const workbench = useService(WorkbenchService).workbench;
|
||||
useEffect(() => {
|
||||
const insertTemplate = effect(
|
||||
switchMap(({ template, mode }: { template: string; mode: string }) => {
|
||||
return fromPromise(async abort => {
|
||||
const templateZip = await fetch(template, { signal: abort });
|
||||
const templateBlob = await templateZip.blob();
|
||||
throwIfAborted(abort);
|
||||
const [doc] = await ZipTransformer.importDocs(
|
||||
currentWorkspace.docCollection,
|
||||
templateBlob
|
||||
);
|
||||
if (doc) {
|
||||
doc.resetHistory();
|
||||
}
|
||||
|
||||
return { doc, mode };
|
||||
}).pipe(
|
||||
timeout(10000 /* 10s */),
|
||||
mergeMap(({ mode, doc }) => {
|
||||
if (doc) {
|
||||
docsList.setPrimaryMode(doc.id, mode as DocMode);
|
||||
workbench.openDoc(doc.id);
|
||||
}
|
||||
return EMPTY;
|
||||
}),
|
||||
onStart(() => {
|
||||
pushGlobalLoadingEvent({
|
||||
key: 'insert-template',
|
||||
});
|
||||
}),
|
||||
catchError(err => {
|
||||
console.error(err);
|
||||
toast(t['com.affine.ai.template-insert.failed']());
|
||||
return EMPTY;
|
||||
}),
|
||||
finalize(() => {
|
||||
resolveGlobalLoadingEvent('insert-template');
|
||||
})
|
||||
);
|
||||
})
|
||||
);
|
||||
|
||||
const disposable = AIProvider.slots.requestInsertTemplate.on(
|
||||
({ template, mode }) => {
|
||||
insertTemplate({ template, mode });
|
||||
}
|
||||
);
|
||||
|
||||
return () => {
|
||||
disposable.dispose();
|
||||
insertTemplate.unsubscribe();
|
||||
};
|
||||
}, [
|
||||
currentWorkspace.docCollection,
|
||||
docsList,
|
||||
pushGlobalLoadingEvent,
|
||||
resolveGlobalLoadingEvent,
|
||||
t,
|
||||
workbench,
|
||||
]);
|
||||
|
||||
const globalDialogService = useService(GlobalDialogService);
|
||||
|
||||
useEffect(() => {
|
||||
const disposable = AIProvider.slots.requestUpgradePlan.on(() => {
|
||||
globalDialogService.open('setting', {
|
||||
activeTab: 'billing',
|
||||
});
|
||||
track.$.paywall.aiAction.viewPlans();
|
||||
});
|
||||
return () => {
|
||||
disposable.dispose();
|
||||
};
|
||||
}, [globalDialogService]);
|
||||
|
||||
useRegisterWorkspaceCommands();
|
||||
useRegisterNavigationCommands();
|
||||
useRegisterFindInPageCommands();
|
||||
|
||||
useEffect(() => {
|
||||
// hotfix for blockVersions
|
||||
// this is a mistake in the
|
||||
// 0.8.0 ~ 0.8.1
|
||||
// 0.8.0-beta.0 ~ 0.8.0-beta.3
|
||||
// 0.8.0-canary.17 ~ 0.9.0-canary.3
|
||||
const meta = currentWorkspace.docCollection.doc.getMap('meta');
|
||||
const blockVersions = meta.get('blockVersions');
|
||||
if (
|
||||
!(blockVersions instanceof YMap) &&
|
||||
blockVersions !== null &&
|
||||
blockVersions !== undefined &&
|
||||
typeof blockVersions === 'object'
|
||||
) {
|
||||
meta.set(
|
||||
'blockVersions',
|
||||
new YMap(Object.entries(blockVersions as Record<string, number>))
|
||||
);
|
||||
}
|
||||
}, [currentWorkspace.docCollection.doc]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<QuickSearchContainer />
|
||||
<SyncAwareness />
|
||||
<OverCapacityNotification />
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,4 +1,4 @@
|
||||
import { DesktopApiService } from '@affine/core/modules/desktop-api/service';
|
||||
import { DesktopApiService } from '@affine/core/modules/desktop-api';
|
||||
import { useService } from '@toeverything/infra';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import { Tooltip } from '@affine/component/ui/tooltip';
|
||||
import { GlobalDialogService } from '@affine/core/modules/dialogs';
|
||||
import type { SettingTab } from '@affine/core/modules/dialogs/constant';
|
||||
import { UrlService } from '@affine/core/modules/url';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import { CloseIcon, NewIcon } from '@blocksuite/icons/rc';
|
||||
import {
|
||||
GlobalContextService,
|
||||
useLiveData,
|
||||
useService,
|
||||
useServices,
|
||||
} from '@toeverything/infra';
|
||||
import { useSetAtom } from 'jotai/react';
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
import type { SettingProps } from '../../affine/setting-modal';
|
||||
import { openSettingModalAtom } from '../../atoms';
|
||||
import { ContactIcon, HelpIcon, KeyboardIcon } from './icons';
|
||||
import {
|
||||
StyledAnimateWrapper,
|
||||
@@ -40,19 +40,18 @@ export const HelpIsland = () => {
|
||||
});
|
||||
const docId = useLiveData(globalContextService.globalContext.docId.$);
|
||||
const docMode = useLiveData(globalContextService.globalContext.docMode.$);
|
||||
const setOpenSettingModalAtom = useSetAtom(openSettingModalAtom);
|
||||
const globalDialogService = useService(GlobalDialogService);
|
||||
const [spread, setShowSpread] = useState(false);
|
||||
const t = useI18n();
|
||||
const openSettingModal = useCallback(
|
||||
(tab: SettingProps['activeTab']) => {
|
||||
(tab: SettingTab) => {
|
||||
setShowSpread(false);
|
||||
|
||||
setOpenSettingModalAtom({
|
||||
open: true,
|
||||
globalDialogService.open('setting', {
|
||||
activeTab: tab,
|
||||
});
|
||||
},
|
||||
[setOpenSettingModalAtom]
|
||||
[globalDialogService]
|
||||
);
|
||||
const openAbout = useCallback(
|
||||
() => openSettingModal('about'),
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
import {
|
||||
openImportModalAtom,
|
||||
openSettingModalAtom,
|
||||
} from '@affine/core/components/atoms';
|
||||
import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks';
|
||||
import {
|
||||
AddPageButton,
|
||||
@@ -15,6 +11,7 @@ import {
|
||||
SidebarScrollableContainer,
|
||||
} from '@affine/core/modules/app-sidebar/views';
|
||||
import { ExternalMenuLinkItem } from '@affine/core/modules/app-sidebar/views/menu-item/external-menu-link-item';
|
||||
import { GlobalDialogService } from '@affine/core/modules/dialogs';
|
||||
import {
|
||||
ExplorerCollections,
|
||||
ExplorerFavorites,
|
||||
@@ -37,10 +34,10 @@ import {
|
||||
import type { Workspace } from '@toeverything/infra';
|
||||
import {
|
||||
useLiveData,
|
||||
useService,
|
||||
useServices,
|
||||
WorkspaceService,
|
||||
} from '@toeverything/infra';
|
||||
import { useSetAtom } from 'jotai';
|
||||
import type { MouseEvent, ReactElement } from 'react';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
@@ -86,6 +83,7 @@ export const RootAppSidebar = (): ReactElement => {
|
||||
});
|
||||
const currentWorkspace = workspaceService.workspace;
|
||||
const t = useI18n();
|
||||
const globalDialogService = useService(GlobalDialogService);
|
||||
const workbench = workbenchService.workbench;
|
||||
const currentPath = useLiveData(
|
||||
workbench.location$.map(location => location.pathname)
|
||||
@@ -106,21 +104,17 @@ export const RootAppSidebar = (): ReactElement => {
|
||||
[pageHelper]
|
||||
);
|
||||
|
||||
const setOpenSettingModalAtom = useSetAtom(openSettingModalAtom);
|
||||
const setOpenImportModalAtom = useSetAtom(openImportModalAtom);
|
||||
|
||||
const onOpenSettingModal = useCallback(() => {
|
||||
setOpenSettingModalAtom({
|
||||
globalDialogService.open('setting', {
|
||||
activeTab: 'appearance',
|
||||
open: true,
|
||||
});
|
||||
track.$.navigationPanel.$.openSettings();
|
||||
}, [setOpenSettingModalAtom]);
|
||||
}, [globalDialogService]);
|
||||
|
||||
const onOpenImportModal = useCallback(() => {
|
||||
track.$.navigationPanel.importModal.open();
|
||||
setOpenImportModalAtom(true);
|
||||
}, [setOpenImportModalAtom]);
|
||||
globalDialogService.open('import', undefined);
|
||||
}, [globalDialogService]);
|
||||
|
||||
return (
|
||||
<AppSidebar>
|
||||
|
||||
@@ -8,11 +8,8 @@ import {
|
||||
type MenuProps,
|
||||
Skeleton,
|
||||
} from '@affine/component';
|
||||
import {
|
||||
authAtom,
|
||||
openSettingModalAtom,
|
||||
openSignOutModalAtom,
|
||||
} from '@affine/core/components/atoms';
|
||||
import { authAtom } from '@affine/core/components/atoms';
|
||||
import { GlobalDialogService } from '@affine/core/modules/dialogs';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import { track } from '@affine/track';
|
||||
import { AccountIcon, SignOutIcon } from '@blocksuite/icons/rc';
|
||||
@@ -32,6 +29,7 @@ import {
|
||||
UserQuotaService,
|
||||
} from '../../modules/cloud';
|
||||
import { UserPlanButton } from '../affine/auth/user-plan-button';
|
||||
import { useSignOut } from '../hooks/affine/use-sign-out';
|
||||
import * as styles from './index.css';
|
||||
import { UnknownUserIcon } from './unknow-user';
|
||||
|
||||
@@ -78,21 +76,15 @@ const UnauthorizedUserInfo = () => {
|
||||
};
|
||||
|
||||
const AccountMenu = () => {
|
||||
const setSettingModalAtom = useSetAtom(openSettingModalAtom);
|
||||
const setOpenSignOutModalAtom = useSetAtom(openSignOutModalAtom);
|
||||
const globalDialogService = useService(GlobalDialogService);
|
||||
const openSignOutModal = useSignOut();
|
||||
|
||||
const onOpenAccountSetting = useCallback(() => {
|
||||
track.$.navigationPanel.profileAndBadge.openSettings({ to: 'account' });
|
||||
setSettingModalAtom(prev => ({
|
||||
...prev,
|
||||
open: true,
|
||||
globalDialogService.open('setting', {
|
||||
activeTab: 'account',
|
||||
}));
|
||||
}, [setSettingModalAtom]);
|
||||
|
||||
const onOpenSignOutModal = useCallback(() => {
|
||||
setOpenSignOutModalAtom(true);
|
||||
}, [setOpenSignOutModalAtom]);
|
||||
});
|
||||
}, [globalDialogService]);
|
||||
|
||||
const t = useI18n();
|
||||
|
||||
@@ -108,7 +100,7 @@ const AccountMenu = () => {
|
||||
<MenuItem
|
||||
prefixIcon={<SignOutIcon />}
|
||||
data-testid="workspace-modal-sign-out-option"
|
||||
onClick={onOpenSignOutModal}
|
||||
onClick={openSignOutModal}
|
||||
>
|
||||
{t['com.affine.workspace.cloud.account.logout']()}
|
||||
</MenuItem>
|
||||
@@ -193,22 +185,20 @@ const AIUsage = () => {
|
||||
const loading = copilotActionLimit === null || copilotActionUsed === null;
|
||||
const loadError = useLiveData(copilotQuotaService.copilotQuota.error$);
|
||||
|
||||
const setSettingModalAtom = useSetAtom(openSettingModalAtom);
|
||||
const globalDialogService = useService(GlobalDialogService);
|
||||
|
||||
const goToAIPlanPage = useCallback(() => {
|
||||
setSettingModalAtom({
|
||||
open: true,
|
||||
globalDialogService.open('setting', {
|
||||
activeTab: 'plans',
|
||||
scrollAnchor: 'aiPricingPlan',
|
||||
});
|
||||
}, [setSettingModalAtom]);
|
||||
}, [globalDialogService]);
|
||||
|
||||
const goToAccountSetting = useCallback(() => {
|
||||
setSettingModalAtom({
|
||||
open: true,
|
||||
globalDialogService.open('setting', {
|
||||
activeTab: 'account',
|
||||
});
|
||||
}, [setSettingModalAtom]);
|
||||
}, [globalDialogService]);
|
||||
|
||||
if (loading) {
|
||||
if (loadError) console.error(loadError);
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { Menu, type MenuProps } from '@affine/component';
|
||||
import { useNavigateHelper } from '@affine/core/components/hooks/use-navigate-helper';
|
||||
import type { CreateWorkspaceCallbackPayload } from '@affine/core/modules/create-workspace';
|
||||
import { track } from '@affine/track';
|
||||
import {
|
||||
GlobalContextService,
|
||||
@@ -18,7 +17,10 @@ interface WorkspaceSelectorProps {
|
||||
open?: boolean;
|
||||
workspaceMetadata?: WorkspaceMetadata;
|
||||
onSelectWorkspace?: (workspaceMetadata: WorkspaceMetadata) => void;
|
||||
onCreatedWorkspace?: (payload: CreateWorkspaceCallbackPayload) => void;
|
||||
onCreatedWorkspace?: (payload: {
|
||||
metadata: WorkspaceMetadata;
|
||||
defaultDocId?: string;
|
||||
}) => void;
|
||||
showSettingsButton?: boolean;
|
||||
showEnableCloudButton?: boolean;
|
||||
showArrowDownIcon?: boolean;
|
||||
@@ -140,14 +142,14 @@ export const WorkspaceNavigator = ({
|
||||
[onSelectWorkspace, jumpToPage]
|
||||
);
|
||||
const handleCreatedWorkspace = useCallback(
|
||||
(payload: CreateWorkspaceCallbackPayload) => {
|
||||
(payload: { metadata: WorkspaceMetadata; defaultDocId?: string }) => {
|
||||
onCreatedWorkspace?.(payload);
|
||||
if (document.startViewTransition) {
|
||||
document.startViewTransition(() => {
|
||||
if (payload.defaultDocId) {
|
||||
jumpToPage(payload.meta.id, payload.defaultDocId);
|
||||
jumpToPage(payload.metadata.id, payload.defaultDocId);
|
||||
} else {
|
||||
jumpToPage(payload.meta.id, 'all');
|
||||
jumpToPage(payload.metadata.id, 'all');
|
||||
}
|
||||
return new Promise(resolve =>
|
||||
setTimeout(resolve, 150)
|
||||
@@ -155,9 +157,9 @@ export const WorkspaceNavigator = ({
|
||||
});
|
||||
} else {
|
||||
if (payload.defaultDocId) {
|
||||
jumpToPage(payload.meta.id, payload.defaultDocId);
|
||||
jumpToPage(payload.metadata.id, payload.defaultDocId);
|
||||
} else {
|
||||
jumpToPage(payload.meta.id, 'all');
|
||||
jumpToPage(payload.metadata.id, 'all');
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -2,8 +2,7 @@ import { Divider } from '@affine/component/ui/divider';
|
||||
import { MenuItem } from '@affine/component/ui/menu';
|
||||
import { authAtom } from '@affine/core/components/atoms';
|
||||
import { AuthService } from '@affine/core/modules/cloud';
|
||||
import { CreateWorkspaceDialogService } from '@affine/core/modules/create-workspace';
|
||||
import type { CreateWorkspaceCallbackPayload } from '@affine/core/modules/create-workspace/types';
|
||||
import { GlobalDialogService } from '@affine/core/modules/dialogs';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import { track } from '@affine/track';
|
||||
import { Logo1Icon } from '@blocksuite/icons/rc';
|
||||
@@ -62,7 +61,10 @@ export const SignInItem = () => {
|
||||
interface UserWithWorkspaceListProps {
|
||||
onEventEnd?: () => void;
|
||||
onClickWorkspace?: (workspace: WorkspaceMetadata) => void;
|
||||
onCreatedWorkspace?: (payload: CreateWorkspaceCallbackPayload) => void;
|
||||
onCreatedWorkspace?: (payload: {
|
||||
metadata: WorkspaceMetadata;
|
||||
defaultDocId?: string;
|
||||
}) => void;
|
||||
showSettingsButton?: boolean;
|
||||
showEnableCloudButton?: boolean;
|
||||
}
|
||||
@@ -74,7 +76,7 @@ const UserWithWorkspaceListInner = ({
|
||||
showSettingsButton,
|
||||
showEnableCloudButton,
|
||||
}: UserWithWorkspaceListProps) => {
|
||||
const createWorkspaceDialogService = useService(CreateWorkspaceDialogService);
|
||||
const globalDialogService = useService(GlobalDialogService);
|
||||
const session = useLiveData(useService(AuthService).session.session$);
|
||||
const featureFlagService = useService(FeatureFlagService);
|
||||
|
||||
@@ -97,14 +99,14 @@ const UserWithWorkspaceListInner = ({
|
||||
return openSignInModal();
|
||||
}
|
||||
track.$.navigationPanel.workspaceList.createWorkspace();
|
||||
createWorkspaceDialogService.dialog.open('new', payload => {
|
||||
globalDialogService.open('create-workspace', undefined, payload => {
|
||||
if (payload) {
|
||||
onCreatedWorkspace?.(payload);
|
||||
}
|
||||
});
|
||||
onEventEnd?.();
|
||||
}, [
|
||||
createWorkspaceDialogService,
|
||||
globalDialogService,
|
||||
featureFlagService,
|
||||
isAuthenticated,
|
||||
onCreatedWorkspace,
|
||||
@@ -116,13 +118,13 @@ const UserWithWorkspaceListInner = ({
|
||||
track.$.navigationPanel.workspaceList.createWorkspace({
|
||||
control: 'import',
|
||||
});
|
||||
createWorkspaceDialogService.dialog.open('add', payload => {
|
||||
globalDialogService.open('import-workspace', undefined, payload => {
|
||||
if (payload) {
|
||||
onCreatedWorkspace?.(payload);
|
||||
onCreatedWorkspace?.({ metadata: payload.workspace });
|
||||
}
|
||||
});
|
||||
onEventEnd?.();
|
||||
}, [createWorkspaceDialogService.dialog, onCreatedWorkspace, onEventEnd]);
|
||||
}, [globalDialogService, onCreatedWorkspace, onEventEnd]);
|
||||
|
||||
const workspaceManager = useService(WorkspacesService);
|
||||
const workspaces = useLiveData(workspaceManager.list.workspaces$);
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { ScrollableContainer } from '@affine/component';
|
||||
import { Divider } from '@affine/component/ui/divider';
|
||||
import { openSettingModalAtom } from '@affine/core/components/atoms';
|
||||
import { useEnableCloud } from '@affine/core/components/hooks/affine/use-enable-cloud';
|
||||
import { AuthService } from '@affine/core/modules/cloud';
|
||||
import { GlobalDialogService } from '@affine/core/modules/dialogs';
|
||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import { CloudWorkspaceIcon, LocalWorkspaceIcon } from '@blocksuite/icons/rc';
|
||||
@@ -14,7 +14,6 @@ import {
|
||||
WorkspaceService,
|
||||
WorkspacesService,
|
||||
} from '@toeverything/infra';
|
||||
import { useSetAtom } from 'jotai';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
|
||||
import { WorkspaceCard } from '../../workspace-card';
|
||||
@@ -100,11 +99,10 @@ export const AFFiNEWorkspaceList = ({
|
||||
}) => {
|
||||
const workspacesService = useService(WorkspacesService);
|
||||
const workspaces = useLiveData(workspacesService.list.workspaces$);
|
||||
const globalDialogService = useService(GlobalDialogService);
|
||||
|
||||
const confirmEnableCloud = useEnableCloud();
|
||||
|
||||
const setOpenSettingModalAtom = useSetAtom(openSettingModalAtom);
|
||||
|
||||
const session = useService(AuthService).session;
|
||||
const status = useLiveData(session.status$);
|
||||
|
||||
@@ -128,14 +126,13 @@ export const AFFiNEWorkspaceList = ({
|
||||
|
||||
const onClickWorkspaceSetting = useCallback(
|
||||
(workspaceMetadata: WorkspaceMetadata) => {
|
||||
setOpenSettingModalAtom({
|
||||
open: true,
|
||||
globalDialogService.open('setting', {
|
||||
activeTab: 'workspace:preference',
|
||||
workspaceMetadata,
|
||||
});
|
||||
onEventEnd?.();
|
||||
},
|
||||
[onEventEnd, setOpenSettingModalAtom]
|
||||
[globalDialogService, onEventEnd]
|
||||
);
|
||||
|
||||
const onClickEnableCloud = useCallback(
|
||||
|
||||
@@ -1,93 +0,0 @@
|
||||
import { useAppSettingHelper } from '@affine/core/components/hooks/affine/use-app-setting-helper';
|
||||
import { AppSidebarService } from '@affine/core/modules/app-sidebar';
|
||||
import {
|
||||
DocsService,
|
||||
GlobalContextService,
|
||||
useLiveData,
|
||||
useService,
|
||||
} from '@toeverything/infra';
|
||||
import { clsx } from 'clsx';
|
||||
import type { HTMLAttributes, PropsWithChildren, ReactElement } from 'react';
|
||||
import { forwardRef } from 'react';
|
||||
|
||||
import { appStyle, mainContainerStyle, toolStyle } from './index.css';
|
||||
|
||||
export type WorkspaceRootProps = PropsWithChildren<{
|
||||
className?: string;
|
||||
useNoisyBackground?: boolean;
|
||||
useBlurBackground?: boolean;
|
||||
}>;
|
||||
|
||||
export const AppContainer = ({
|
||||
useNoisyBackground,
|
||||
useBlurBackground,
|
||||
children,
|
||||
className,
|
||||
...rest
|
||||
}: WorkspaceRootProps) => {
|
||||
const noisyBackground = BUILD_CONFIG.isElectron && useNoisyBackground;
|
||||
const blurBackground =
|
||||
BUILD_CONFIG.isElectron && environment.isMacOs && useBlurBackground;
|
||||
return (
|
||||
<div
|
||||
{...rest}
|
||||
className={clsx(appStyle, className, {
|
||||
'noisy-background': noisyBackground,
|
||||
'blur-background': blurBackground,
|
||||
})}
|
||||
data-noise-background={noisyBackground}
|
||||
data-blur-background={blurBackground}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export interface MainContainerProps extends HTMLAttributes<HTMLDivElement> {}
|
||||
|
||||
export const MainContainer = forwardRef<
|
||||
HTMLDivElement,
|
||||
PropsWithChildren<MainContainerProps>
|
||||
>(function MainContainer({ className, children, ...props }, ref): ReactElement {
|
||||
const { appSettings } = useAppSettingHelper();
|
||||
const appSidebarService = useService(AppSidebarService).sidebar;
|
||||
const open = useLiveData(appSidebarService.open$);
|
||||
|
||||
return (
|
||||
<div
|
||||
{...props}
|
||||
className={clsx(mainContainerStyle, className)}
|
||||
data-is-desktop={BUILD_CONFIG.isElectron}
|
||||
data-transparent={false}
|
||||
data-client-border={appSettings.clientBorder}
|
||||
data-side-bar-open={open}
|
||||
data-testid="main-container"
|
||||
ref={ref}
|
||||
>
|
||||
<div className={mainContainerStyle}>{children}</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
MainContainer.displayName = 'MainContainer';
|
||||
|
||||
export const MainContainerFallback = ({ children }: PropsWithChildren) => {
|
||||
// todo: default app fallback?
|
||||
return <MainContainer>{children}</MainContainer>;
|
||||
};
|
||||
|
||||
export const ToolContainer = (
|
||||
props: PropsWithChildren<{ className?: string }>
|
||||
): ReactElement => {
|
||||
const docId = useLiveData(
|
||||
useService(GlobalContextService).globalContext.docId.$
|
||||
);
|
||||
const docRecordList = useService(DocsService).list;
|
||||
const doc = useLiveData(docId ? docRecordList.doc$(docId) : undefined);
|
||||
const inTrash = useLiveData(doc?.meta$)?.trash;
|
||||
return (
|
||||
<div className={clsx(toolStyle, { trash: inTrash }, props.className)}>
|
||||
{props.children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
import { style } from '@vanilla-extract/css';
|
||||
|
||||
export const islandContainer = style({
|
||||
position: 'absolute',
|
||||
right: 16,
|
||||
bottom: 16,
|
||||
zIndex: 1,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
gap: '12px',
|
||||
selectors: {
|
||||
'&.trash': {
|
||||
bottom: '78px',
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,26 @@
|
||||
import {
|
||||
DocsService,
|
||||
GlobalContextService,
|
||||
useLiveData,
|
||||
useService,
|
||||
} from '@toeverything/infra';
|
||||
import clsx from 'clsx';
|
||||
import type { PropsWithChildren, ReactElement } from 'react';
|
||||
|
||||
import { islandContainer } from './container.css';
|
||||
|
||||
export const IslandContainer = (
|
||||
props: PropsWithChildren<{ className?: string }>
|
||||
): ReactElement => {
|
||||
const docId = useLiveData(
|
||||
useService(GlobalContextService).globalContext.docId.$
|
||||
);
|
||||
const docRecordList = useService(DocsService).list;
|
||||
const doc = useLiveData(docId ? docRecordList.doc$(docId) : undefined);
|
||||
const inTrash = useLiveData(doc?.meta$)?.trash;
|
||||
return (
|
||||
<div className={clsx(islandContainer, { trash: inTrash }, props.className)}>
|
||||
{props.children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -3,7 +3,7 @@ import { useLiveData, useService } from '@toeverything/infra';
|
||||
import clsx from 'clsx';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { ToolContainer } from '../../workspace';
|
||||
import { IslandContainer } from './container';
|
||||
import { AIIcon } from './icons';
|
||||
import { aiIslandBtn, aiIslandWrapper, toolStyle } from './styles.css';
|
||||
|
||||
@@ -24,7 +24,7 @@ export const AIIsland = () => {
|
||||
}, [activeTab, haveChatTab, sidebarOpen]);
|
||||
|
||||
return (
|
||||
<ToolContainer className={clsx(toolStyle, { hide })}>
|
||||
<IslandContainer className={clsx(toolStyle, { hide })}>
|
||||
<div className={aiIslandWrapper} data-hide={hide}>
|
||||
<button
|
||||
className={aiIslandBtn}
|
||||
@@ -38,6 +38,6 @@ export const AIIsland = () => {
|
||||
<AIIcon />
|
||||
</button>
|
||||
</div>
|
||||
</ToolContainer>
|
||||
</IslandContainer>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,133 @@
|
||||
import { useAppSettingHelper } from '@affine/core/components/hooks/affine/use-app-setting-helper';
|
||||
import { RootAppSidebar } from '@affine/core/components/root-app-sidebar';
|
||||
import { AppSidebarService } from '@affine/core/modules/app-sidebar';
|
||||
import {
|
||||
AppSidebarFallback,
|
||||
OpenInAppCard,
|
||||
SidebarSwitch,
|
||||
} from '@affine/core/modules/app-sidebar/views';
|
||||
import { AppTabsHeader } from '@affine/core/modules/app-tabs-header';
|
||||
import { NavigationButtons } from '@affine/core/modules/navigation';
|
||||
import {
|
||||
useLiveData,
|
||||
useService,
|
||||
useServiceOptional,
|
||||
WorkspaceService,
|
||||
} from '@toeverything/infra';
|
||||
import clsx from 'clsx';
|
||||
import {
|
||||
forwardRef,
|
||||
type HTMLAttributes,
|
||||
type PropsWithChildren,
|
||||
type ReactElement,
|
||||
} from 'react';
|
||||
|
||||
import * as styles from './styles.css';
|
||||
|
||||
export const AppContainer = ({
|
||||
children,
|
||||
className,
|
||||
fallback = false,
|
||||
...rest
|
||||
}: PropsWithChildren<{
|
||||
className?: string;
|
||||
fallback?: boolean;
|
||||
}>) => {
|
||||
const { appSettings } = useAppSettingHelper();
|
||||
|
||||
const noisyBackground =
|
||||
BUILD_CONFIG.isElectron && appSettings.enableNoisyBackground;
|
||||
const blurBackground =
|
||||
BUILD_CONFIG.isElectron &&
|
||||
environment.isMacOs &&
|
||||
appSettings.enableBlurBackground;
|
||||
return (
|
||||
<div
|
||||
{...rest}
|
||||
className={clsx(styles.appStyle, className, {
|
||||
'noisy-background': noisyBackground,
|
||||
'blur-background': blurBackground,
|
||||
})}
|
||||
data-noise-background={noisyBackground}
|
||||
data-blur-background={blurBackground}
|
||||
>
|
||||
<LayoutComponent fallback={fallback}>{children}</LayoutComponent>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const DesktopLayout = ({
|
||||
children,
|
||||
fallback = false,
|
||||
}: PropsWithChildren<{ fallback?: boolean }>) => {
|
||||
const workspaceService = useServiceOptional(WorkspaceService);
|
||||
const isInWorkspace = !!workspaceService;
|
||||
return (
|
||||
<div className={styles.desktopAppViewContainer}>
|
||||
<div className={styles.desktopTabsHeader}>
|
||||
<AppTabsHeader
|
||||
left={
|
||||
<>
|
||||
{isInWorkspace && <SidebarSwitch show />}
|
||||
{isInWorkspace && <NavigationButtons />}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.desktopAppViewMain}>
|
||||
{fallback ? (
|
||||
<AppSidebarFallback />
|
||||
) : (
|
||||
isInWorkspace && <RootAppSidebar />
|
||||
)}
|
||||
<MainContainer>{children}</MainContainer>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const BrowserLayout = ({
|
||||
children,
|
||||
fallback = false,
|
||||
}: PropsWithChildren<{ fallback?: boolean }>) => {
|
||||
const workspaceService = useServiceOptional(WorkspaceService);
|
||||
const isInWorkspace = !!workspaceService;
|
||||
|
||||
return (
|
||||
<div className={styles.browserAppViewContainer}>
|
||||
<OpenInAppCard />
|
||||
{fallback ? <AppSidebarFallback /> : isInWorkspace && <RootAppSidebar />}
|
||||
<MainContainer>{children}</MainContainer>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const LayoutComponent = BUILD_CONFIG.isElectron ? DesktopLayout : BrowserLayout;
|
||||
|
||||
const MainContainer = forwardRef<
|
||||
HTMLDivElement,
|
||||
PropsWithChildren<HTMLAttributes<HTMLDivElement>>
|
||||
>(function MainContainer({ className, children, ...props }, ref): ReactElement {
|
||||
const workspaceService = useServiceOptional(WorkspaceService);
|
||||
const isInWorkspace = !!workspaceService;
|
||||
const { appSettings } = useAppSettingHelper();
|
||||
const appSidebarService = useService(AppSidebarService).sidebar;
|
||||
const open = useLiveData(appSidebarService.open$);
|
||||
|
||||
return (
|
||||
<div
|
||||
{...props}
|
||||
className={clsx(styles.mainContainerStyle, className)}
|
||||
data-is-desktop={BUILD_CONFIG.isElectron}
|
||||
data-transparent={false}
|
||||
data-client-border={appSettings.clientBorder}
|
||||
data-side-bar-open={open && isInWorkspace}
|
||||
data-testid="main-container"
|
||||
ref={ref}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
MainContainer.displayName = 'MainContainer';
|
||||
@@ -1,7 +1,5 @@
|
||||
import { cssVar, lightCssVariables } from '@toeverything/theme';
|
||||
import { createVar, globalStyle, style } from '@vanilla-extract/css';
|
||||
|
||||
export const panelWidthVar = createVar('panel-width');
|
||||
import { globalStyle, style } from '@vanilla-extract/css';
|
||||
|
||||
export const appStyle = style({
|
||||
width: '100%',
|
||||
@@ -42,6 +40,38 @@ globalStyle(`html[data-theme="dark"] ${appStyle}`, {
|
||||
},
|
||||
});
|
||||
|
||||
export const browserAppViewContainer = style({
|
||||
display: 'flex',
|
||||
flexFlow: 'row',
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
position: 'relative',
|
||||
});
|
||||
|
||||
export const desktopAppViewContainer = style({
|
||||
display: 'flex',
|
||||
flexFlow: 'column',
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
});
|
||||
|
||||
export const desktopAppViewMain = style({
|
||||
display: 'flex',
|
||||
flexFlow: 'row',
|
||||
width: '100%',
|
||||
height: 'calc(100% - 52px)',
|
||||
position: 'relative',
|
||||
});
|
||||
|
||||
export const desktopTabsHeader = style({
|
||||
display: 'flex',
|
||||
flexFlow: 'row',
|
||||
height: '52px',
|
||||
zIndex: 1,
|
||||
width: '100%',
|
||||
overflow: 'hidden',
|
||||
});
|
||||
|
||||
export const mainContainerStyle = style({
|
||||
position: 'relative',
|
||||
zIndex: 0,
|
||||
@@ -83,26 +113,3 @@ export const mainContainerStyle = style({
|
||||
},
|
||||
},
|
||||
});
|
||||
export const toolStyle = style({
|
||||
position: 'absolute',
|
||||
right: 16,
|
||||
bottom: 16,
|
||||
zIndex: 1,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
gap: '12px',
|
||||
selectors: {
|
||||
'&.trash': {
|
||||
bottom: '78px',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const fallbackRootStyle = style({
|
||||
paddingTop: 52,
|
||||
display: 'flex',
|
||||
flex: 1,
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
});
|
||||
@@ -5,25 +5,6 @@ export const ellipsis = style({
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
});
|
||||
export const pagesTabContent = style({
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
gap: 8,
|
||||
alignItems: 'center',
|
||||
padding: '16px 16px 8px 16px',
|
||||
});
|
||||
export const pagesTab = style({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
overflow: 'hidden',
|
||||
});
|
||||
export const pagesList = style({
|
||||
display: 'flex',
|
||||
flex: 1,
|
||||
overflow: 'hidden',
|
||||
});
|
||||
export const bottomLeft = style({
|
||||
display: 'flex',
|
||||
gap: 8,
|
||||
@@ -130,9 +111,6 @@ export const confirmButton = style({
|
||||
export const resultPages = style({
|
||||
width: '100%',
|
||||
});
|
||||
export const pageList = style({
|
||||
width: '100%',
|
||||
});
|
||||
export const previewCountTipsHighlight = style({
|
||||
color: cssVar('primaryColor'),
|
||||
});
|
||||
@@ -1,80 +1,16 @@
|
||||
import { Button, Modal, RadioGroup } from '@affine/component';
|
||||
import { Button, RadioGroup } from '@affine/component';
|
||||
import { useAllPageListConfig } from '@affine/core/components/hooks/affine/use-all-page-list-config';
|
||||
import { SelectPage } from '@affine/core/components/page-list/docs/select-page';
|
||||
import type { Collection } from '@affine/env/filter';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import type { DialogContentProps } from '@radix-ui/react-dialog';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
|
||||
import * as styles from './edit-collection.css';
|
||||
import { RulesMode } from './rules-mode';
|
||||
import { SelectPage } from './select-page';
|
||||
|
||||
export type EditCollectionMode = 'page' | 'rule';
|
||||
|
||||
export interface EditCollectionModalProps {
|
||||
init?: Collection;
|
||||
title?: string;
|
||||
open: boolean;
|
||||
mode?: EditCollectionMode;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
onConfirm: (view: Collection) => void;
|
||||
}
|
||||
|
||||
const contentOptions: DialogContentProps = {
|
||||
style: {
|
||||
padding: 0,
|
||||
maxWidth: 944,
|
||||
backgroundColor: 'var(--affine-background-primary-color)',
|
||||
},
|
||||
};
|
||||
export const EditCollectionModal = ({
|
||||
init,
|
||||
onConfirm,
|
||||
open,
|
||||
onOpenChange,
|
||||
title,
|
||||
mode,
|
||||
}: EditCollectionModalProps) => {
|
||||
const t = useI18n();
|
||||
const onConfirmOnCollection = useCallback(
|
||||
(view: Collection) => {
|
||||
onConfirm(view);
|
||||
onOpenChange(false);
|
||||
},
|
||||
[onConfirm, onOpenChange]
|
||||
);
|
||||
const onCancel = useCallback(() => {
|
||||
onOpenChange(false);
|
||||
}, [onOpenChange]);
|
||||
|
||||
if (!(open && init)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
open={open}
|
||||
onOpenChange={onOpenChange}
|
||||
withoutCloseButton
|
||||
width="calc(100% - 64px)"
|
||||
height="80%"
|
||||
contentOptions={contentOptions}
|
||||
persistent
|
||||
>
|
||||
<EditCollection
|
||||
title={title}
|
||||
onConfirmText={t['com.affine.editCollection.save']()}
|
||||
init={init}
|
||||
mode={mode}
|
||||
onCancel={onCancel}
|
||||
onConfirm={onConfirmOnCollection}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export interface EditCollectionProps {
|
||||
title?: string;
|
||||
onConfirmText?: string;
|
||||
init: Collection;
|
||||
mode?: EditCollectionMode;
|
||||
@@ -0,0 +1,60 @@
|
||||
import { Modal } from '@affine/component';
|
||||
import { CollectionService } from '@affine/core/modules/collection';
|
||||
import type { DialogComponentProps } from '@affine/core/modules/dialogs';
|
||||
import type { WORKSPACE_DIALOG_SCHEMA } from '@affine/core/modules/dialogs/constant';
|
||||
import type { Collection } from '@affine/env/filter';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import { useLiveData, useService } from '@toeverything/infra';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { EditCollection } from './edit-collection';
|
||||
|
||||
export const CollectionEditorDialog = ({
|
||||
close,
|
||||
collectionId,
|
||||
mode,
|
||||
}: DialogComponentProps<WORKSPACE_DIALOG_SCHEMA['collection-editor']>) => {
|
||||
const t = useI18n();
|
||||
const collectionService = useService(CollectionService);
|
||||
const collection = useLiveData(collectionService.collection$(collectionId));
|
||||
const onConfirmOnCollection = useCallback(
|
||||
(collection: Collection) => {
|
||||
collectionService.updateCollection(collection.id, () => collection);
|
||||
close();
|
||||
},
|
||||
[close, collectionService]
|
||||
);
|
||||
const onCancel = useCallback(() => {
|
||||
close();
|
||||
}, [close]);
|
||||
|
||||
if (!collection) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
open
|
||||
onOpenChange={onCancel}
|
||||
withoutCloseButton
|
||||
width="calc(100% - 64px)"
|
||||
height="80%"
|
||||
contentOptions={{
|
||||
style: {
|
||||
padding: 0,
|
||||
maxWidth: 944,
|
||||
backgroundColor: 'var(--affine-background-primary-color)',
|
||||
},
|
||||
}}
|
||||
persistent
|
||||
>
|
||||
<EditCollection
|
||||
onConfirmText={t['com.affine.editCollection.save']()}
|
||||
init={collection}
|
||||
mode={mode}
|
||||
onCancel={onCancel}
|
||||
onConfirm={onConfirmOnCollection}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
@@ -1,4 +1,13 @@
|
||||
import { Button, IconButton, Tooltip } from '@affine/component';
|
||||
import type { AllPageListConfig } from '@affine/core/components/hooks/affine/use-all-page-list-config';
|
||||
import {
|
||||
AffineShapeIcon,
|
||||
FilterList,
|
||||
filterPageByRules,
|
||||
List,
|
||||
type ListItem,
|
||||
ListScrollContainer,
|
||||
} from '@affine/core/components/page-list';
|
||||
import { CompatibleFavoriteItemsAdapter } from '@affine/core/modules/favorite';
|
||||
import type { Collection } from '@affine/env/filter';
|
||||
import { Trans, useI18n } from '@affine/i18n';
|
||||
@@ -15,12 +24,6 @@ import clsx from 'clsx';
|
||||
import type { ReactNode } from 'react';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
|
||||
import type { AllPageListConfig } from '../../../hooks/affine/use-all-page-list-config';
|
||||
import { FilterList } from '../../filter';
|
||||
import { List, ListScrollContainer } from '../../list';
|
||||
import type { ListItem } from '../../types';
|
||||
import { filterPageByRules } from '../../use-collection-manager';
|
||||
import { AffineShapeIcon } from '../affine-shape';
|
||||
import * as styles from './edit-collection.css';
|
||||
|
||||
export const RulesMode = ({
|
||||
@@ -1,9 +1,13 @@
|
||||
import { Avatar, ConfirmModal, Input, Switch, toast } from '@affine/component';
|
||||
import { Avatar, ConfirmModal, Input, Switch } from '@affine/component';
|
||||
import type { ConfirmModalProps } from '@affine/component/ui/modal';
|
||||
import { CloudSvg } from '@affine/core/components/affine/share-page-modal/cloud-svg';
|
||||
import { authAtom } from '@affine/core/components/atoms';
|
||||
import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks';
|
||||
import { DebugLogger } from '@affine/debug';
|
||||
import { AuthService } from '@affine/core/modules/cloud';
|
||||
import {
|
||||
type DialogComponentProps,
|
||||
type GLOBAL_DIALOG_SCHEMA,
|
||||
} from '@affine/core/modules/dialogs';
|
||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import { track } from '@affine/track';
|
||||
@@ -11,21 +15,14 @@ import {
|
||||
FeatureFlagService,
|
||||
useLiveData,
|
||||
useService,
|
||||
useServiceOptional,
|
||||
WorkspacesService,
|
||||
} from '@toeverything/infra';
|
||||
import { useSetAtom } from 'jotai';
|
||||
import { useCallback, useLayoutEffect, useState } from 'react';
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
import { AuthService } from '../../../modules/cloud';
|
||||
import { _addLocalWorkspace } from '../../../modules/workspace-engine';
|
||||
import { buildShowcaseWorkspace } from '../../../utils/first-app-data';
|
||||
import { DesktopApiService } from '../../desktop-api';
|
||||
import { CreateWorkspaceDialogService } from '../services/dialog';
|
||||
import * as styles from './dialog.css';
|
||||
|
||||
const logger = new DebugLogger('CreateWorkspaceModal');
|
||||
|
||||
interface NameWorkspaceContentProps extends ConfirmModalProps {
|
||||
loading: boolean;
|
||||
onConfirmName: (
|
||||
@@ -157,53 +154,11 @@ const NameWorkspaceContent = ({
|
||||
);
|
||||
};
|
||||
|
||||
const CreateWorkspaceDialog = () => {
|
||||
const createWorkspaceDialogService = useService(CreateWorkspaceDialogService);
|
||||
const mode = useLiveData(createWorkspaceDialogService.dialog.mode$);
|
||||
const t = useI18n();
|
||||
export const CreateWorkspaceDialog = ({
|
||||
close,
|
||||
}: DialogComponentProps<GLOBAL_DIALOG_SCHEMA['create-workspace']>) => {
|
||||
const workspacesService = useService(WorkspacesService);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const electronApi = useServiceOptional(DesktopApiService);
|
||||
|
||||
// TODO(@Peng): maybe refactor using xstate?
|
||||
useLayoutEffect(() => {
|
||||
let canceled = false;
|
||||
// if mode changed, reset step
|
||||
if (mode === 'add') {
|
||||
// a hack for now
|
||||
// when adding a workspace, we will immediately let user select a db file
|
||||
// after it is done, it will effectively add a new workspace to app-data folder
|
||||
// so after that, we will be able to load it via importLocalWorkspace
|
||||
(async () => {
|
||||
if (!electronApi) {
|
||||
return;
|
||||
}
|
||||
logger.info('load db file');
|
||||
const result = await electronApi.handler.dialog.loadDBFile();
|
||||
if (result.workspaceId && !canceled) {
|
||||
_addLocalWorkspace(result.workspaceId);
|
||||
workspacesService.list.revalidate();
|
||||
createWorkspaceDialogService.dialog.callback({
|
||||
meta: {
|
||||
flavour: WorkspaceFlavour.LOCAL,
|
||||
id: result.workspaceId,
|
||||
},
|
||||
});
|
||||
} else if (result.error || result.canceled) {
|
||||
if (result.error) {
|
||||
toast(t[result.error]());
|
||||
}
|
||||
createWorkspaceDialogService.dialog.callback(undefined);
|
||||
createWorkspaceDialogService.dialog.close();
|
||||
}
|
||||
})().catch(err => {
|
||||
console.error(err);
|
||||
});
|
||||
}
|
||||
return () => {
|
||||
canceled = true;
|
||||
};
|
||||
}, [createWorkspaceDialogService, electronApi, mode, t, workspacesService]);
|
||||
|
||||
const onConfirmName = useAsyncCallback(
|
||||
async (name: string, workspaceFlavour: WorkspaceFlavour) => {
|
||||
@@ -218,40 +173,28 @@ const CreateWorkspaceDialog = () => {
|
||||
workspaceFlavour,
|
||||
name
|
||||
);
|
||||
createWorkspaceDialogService.dialog.callback({ meta, defaultDocId });
|
||||
|
||||
createWorkspaceDialogService.dialog.close();
|
||||
close({ metadata: meta, defaultDocId });
|
||||
setLoading(false);
|
||||
},
|
||||
[createWorkspaceDialogService.dialog, loading, workspacesService]
|
||||
[loading, workspacesService, close]
|
||||
);
|
||||
|
||||
const onOpenChange = useCallback(
|
||||
(open: boolean) => {
|
||||
if (!open) {
|
||||
createWorkspaceDialogService.dialog.close();
|
||||
close();
|
||||
}
|
||||
},
|
||||
[createWorkspaceDialogService]
|
||||
[close]
|
||||
);
|
||||
|
||||
if (mode === 'new') {
|
||||
return (
|
||||
<NameWorkspaceContent
|
||||
loading={loading}
|
||||
open
|
||||
onOpenChange={onOpenChange}
|
||||
onConfirmName={onConfirmName}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export const CreateWorkspaceDialogProvider = () => {
|
||||
const createWorkspaceDialogService = useService(CreateWorkspaceDialogService);
|
||||
const isOpen = useLiveData(createWorkspaceDialogService.dialog.isOpen$);
|
||||
|
||||
return isOpen ? <CreateWorkspaceDialog /> : null;
|
||||
return (
|
||||
<NameWorkspaceContent
|
||||
loading={loading}
|
||||
open
|
||||
onOpenChange={onOpenChange}
|
||||
onConfirmName={onConfirmName}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,62 @@
|
||||
import { Modal, Scrollable } from '@affine/component';
|
||||
import { BlocksuiteHeaderTitle } from '@affine/core/components/blocksuite/block-suite-header/title';
|
||||
import type { DialogComponentProps } from '@affine/core/modules/dialogs';
|
||||
import type { WORKSPACE_DIALOG_SCHEMA } from '@affine/core/modules/dialogs/constant';
|
||||
import type { Doc } from '@toeverything/infra';
|
||||
import { DocsService, FrameworkScope, useService } from '@toeverything/infra';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { InfoTable } from './info-modal';
|
||||
import * as styles from './styles.css';
|
||||
|
||||
export const DocInfoDialog = ({
|
||||
close,
|
||||
docId,
|
||||
}: DialogComponentProps<WORKSPACE_DIALOG_SCHEMA['doc-info']>) => {
|
||||
const docsService = useService(DocsService);
|
||||
|
||||
const [doc, setDoc] = useState<Doc | null>(null);
|
||||
useEffect(() => {
|
||||
if (!docId) return;
|
||||
const docRef = docsService.open(docId);
|
||||
setDoc(docRef.doc);
|
||||
return () => {
|
||||
docRef.release();
|
||||
setDoc(null);
|
||||
};
|
||||
}, [docId, docsService]);
|
||||
|
||||
if (!doc || !docId) return null;
|
||||
|
||||
return (
|
||||
<FrameworkScope scope={doc.scope}>
|
||||
<Modal
|
||||
contentOptions={{
|
||||
className: styles.container,
|
||||
}}
|
||||
open
|
||||
onOpenChange={() => close()}
|
||||
withoutCloseButton
|
||||
>
|
||||
<Scrollable.Root>
|
||||
<Scrollable.Viewport
|
||||
className={styles.viewport}
|
||||
data-testid="info-modal"
|
||||
>
|
||||
<div
|
||||
className={styles.titleContainer}
|
||||
data-testid="info-modal-title"
|
||||
>
|
||||
<BlocksuiteHeaderTitle
|
||||
docId={docId}
|
||||
className={styles.titleStyle}
|
||||
/>
|
||||
</div>
|
||||
<InfoTable docId={docId} onClose={() => close()} />
|
||||
</Scrollable.Viewport>
|
||||
<Scrollable.Scrollbar className={styles.scrollBar} />
|
||||
</Scrollable.Root>
|
||||
</Modal>
|
||||
</FrameworkScope>
|
||||
);
|
||||
};
|
||||
@@ -1,99 +1,27 @@
|
||||
import {
|
||||
Button,
|
||||
Divider,
|
||||
type InlineEditHandle,
|
||||
Menu,
|
||||
Modal,
|
||||
PropertyCollapsibleContent,
|
||||
PropertyCollapsibleSection,
|
||||
Scrollable,
|
||||
} from '@affine/component';
|
||||
import {
|
||||
DocDatabaseBacklinkInfo,
|
||||
DocInfoService,
|
||||
} from '@affine/core/modules/doc-info';
|
||||
import { CreatePropertyMenuItems } from '@affine/core/components/doc-properties/menu/create-doc-property';
|
||||
import { DocPropertyRow } from '@affine/core/components/doc-properties/table';
|
||||
import { DocDatabaseBacklinkInfo } from '@affine/core/modules/doc-info';
|
||||
import { DocsSearchService } from '@affine/core/modules/docs-search';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import { PlusIcon } from '@blocksuite/icons/rc';
|
||||
import type { Doc } from '@toeverything/infra';
|
||||
import {
|
||||
DocsService,
|
||||
FrameworkScope,
|
||||
LiveData,
|
||||
useLiveData,
|
||||
useService,
|
||||
useServices,
|
||||
} from '@toeverything/infra';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useMemo, useState } from 'react';
|
||||
|
||||
import { BlocksuiteHeaderTitle } from '../../blocksuite/block-suite-header/title';
|
||||
import { CreatePropertyMenuItems } from '../menu/create-doc-property';
|
||||
import { DocPropertyRow } from '../table';
|
||||
import * as styles from './info-modal.css';
|
||||
import { LinksRow } from './links-row';
|
||||
|
||||
export const InfoModal = () => {
|
||||
const modal = useService(DocInfoService).modal;
|
||||
const docId = useLiveData(modal.docId$);
|
||||
const docsService = useService(DocsService);
|
||||
|
||||
const [doc, setDoc] = useState<Doc | null>(null);
|
||||
useEffect(() => {
|
||||
if (!docId) return;
|
||||
const docRef = docsService.open(docId);
|
||||
setDoc(docRef.doc);
|
||||
return () => {
|
||||
docRef.release();
|
||||
setDoc(null);
|
||||
};
|
||||
}, [docId, docsService]);
|
||||
|
||||
if (!doc || !docId) return null;
|
||||
|
||||
return (
|
||||
<FrameworkScope scope={doc.scope}>
|
||||
<InfoModalOpened docId={docId} />
|
||||
</FrameworkScope>
|
||||
);
|
||||
};
|
||||
|
||||
const InfoModalOpened = ({ docId }: { docId: string }) => {
|
||||
const modal = useService(DocInfoService).modal;
|
||||
|
||||
const titleInputHandleRef = useRef<InlineEditHandle>(null);
|
||||
const handleClose = useCallback(() => {
|
||||
modal.close();
|
||||
}, [modal]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
contentOptions={{
|
||||
className: styles.container,
|
||||
}}
|
||||
open
|
||||
onOpenChange={v => modal.onOpenChange(v)}
|
||||
withoutCloseButton
|
||||
>
|
||||
<Scrollable.Root>
|
||||
<Scrollable.Viewport
|
||||
className={styles.viewport}
|
||||
data-testid="info-modal"
|
||||
>
|
||||
<div className={styles.titleContainer} data-testid="info-modal-title">
|
||||
<BlocksuiteHeaderTitle
|
||||
docId={docId}
|
||||
className={styles.titleStyle}
|
||||
inputHandleRef={titleInputHandleRef}
|
||||
/>
|
||||
</div>
|
||||
<InfoTable docId={docId} onClose={handleClose} />
|
||||
</Scrollable.Viewport>
|
||||
<Scrollable.Scrollbar className={styles.scrollBar} />
|
||||
</Scrollable.Root>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export const InfoTable = ({
|
||||
onClose,
|
||||
docId,
|
||||
@@ -1,8 +1,8 @@
|
||||
import { PropertyCollapsibleSection } from '@affine/component';
|
||||
import { AffinePageReference } from '@affine/core/components/affine/reference-link';
|
||||
import type { Backlink, Link } from '@affine/core/modules/doc-link';
|
||||
import type { MouseEvent } from 'react';
|
||||
|
||||
import { AffinePageReference } from '../../affine/reference-link';
|
||||
import * as styles from './links-row.css';
|
||||
|
||||
export const LinksRow = ({
|
||||
@@ -0,0 +1,84 @@
|
||||
import { cssVar } from '@toeverything/theme';
|
||||
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||
import { globalStyle, style } from '@vanilla-extract/css';
|
||||
|
||||
export const container = style({
|
||||
maxWidth: 480,
|
||||
minWidth: 360,
|
||||
padding: '20px 0',
|
||||
alignSelf: 'start',
|
||||
marginTop: '120px',
|
||||
});
|
||||
|
||||
export const titleContainer = style({
|
||||
display: 'flex',
|
||||
width: '100%',
|
||||
flexDirection: 'column',
|
||||
});
|
||||
|
||||
export const titleStyle = style({
|
||||
fontSize: cssVar('fontH6'),
|
||||
fontWeight: '600',
|
||||
});
|
||||
|
||||
export const rowNameContainer = style({
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
gap: 6,
|
||||
padding: 6,
|
||||
width: '160px',
|
||||
});
|
||||
|
||||
export const viewport = style({
|
||||
maxHeight: 'calc(100vh - 220px)',
|
||||
padding: '0 24px',
|
||||
});
|
||||
|
||||
export const scrollBar = style({
|
||||
width: 6,
|
||||
transform: 'translateX(-4px)',
|
||||
});
|
||||
|
||||
export const hiddenInput = style({
|
||||
width: '0',
|
||||
height: '0',
|
||||
position: 'absolute',
|
||||
});
|
||||
|
||||
export const timeRow = style({
|
||||
marginTop: 20,
|
||||
borderBottom: 4,
|
||||
});
|
||||
|
||||
export const tableBodyRoot = style({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
position: 'relative',
|
||||
});
|
||||
|
||||
export const addPropertyButton = style({
|
||||
alignSelf: 'flex-start',
|
||||
fontSize: cssVar('fontSm'),
|
||||
color: `${cssVarV2('text/secondary')}`,
|
||||
padding: '0 4px',
|
||||
height: 36,
|
||||
fontWeight: 400,
|
||||
gap: 6,
|
||||
'@media': {
|
||||
print: {
|
||||
display: 'none',
|
||||
},
|
||||
},
|
||||
selectors: {
|
||||
[`[data-property-collapsed="true"] &`]: {
|
||||
display: 'none',
|
||||
},
|
||||
},
|
||||
});
|
||||
globalStyle(`${addPropertyButton} svg`, {
|
||||
fontSize: 16,
|
||||
color: cssVarV2('icon/secondary'),
|
||||
});
|
||||
globalStyle(`${addPropertyButton}:hover svg`, {
|
||||
color: cssVarV2('icon/primary'),
|
||||
});
|
||||
@@ -3,6 +3,15 @@ import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hoo
|
||||
import { useNavigateHelper } from '@affine/core/components/hooks/use-navigate-helper';
|
||||
import { useWorkspaceName } from '@affine/core/components/hooks/use-workspace-info';
|
||||
import { WorkspaceSelector } from '@affine/core/components/workspace-selector';
|
||||
import { AuthService } from '@affine/core/modules/cloud';
|
||||
import {
|
||||
type DialogComponentProps,
|
||||
type GLOBAL_DIALOG_SCHEMA,
|
||||
} from '@affine/core/modules/dialogs';
|
||||
import {
|
||||
ImportTemplateService,
|
||||
TemplateDownloaderService,
|
||||
} from '@affine/core/modules/import-template';
|
||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import type { DocMode } from '@blocksuite/affine/blocks';
|
||||
@@ -16,11 +25,6 @@ import {
|
||||
import { cssVar } from '@toeverything/theme';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
import { AuthService } from '../../cloud';
|
||||
import type { CreateWorkspaceCallbackPayload } from '../../create-workspace';
|
||||
import { ImportTemplateDialogService } from '../services/dialog';
|
||||
import { TemplateDownloaderService } from '../services/downloader';
|
||||
import { ImportTemplateService } from '../services/import';
|
||||
import * as styles from './dialog.css';
|
||||
|
||||
const Dialog = ({
|
||||
@@ -102,8 +106,8 @@ const Dialog = ({
|
||||
);
|
||||
|
||||
const handleCreatedWorkspace = useCallback(
|
||||
(payload: CreateWorkspaceCallbackPayload) => {
|
||||
return setSelectedWorkspace(payload.meta);
|
||||
(payload: { metadata: WorkspaceMetadata; defaultDocId?: string }) => {
|
||||
return setSelectedWorkspace(payload.metadata);
|
||||
},
|
||||
[]
|
||||
);
|
||||
@@ -224,30 +228,29 @@ const Dialog = ({
|
||||
);
|
||||
};
|
||||
|
||||
export const ImportTemplateDialogProvider = () => {
|
||||
const importTemplateDialogService = useService(ImportTemplateDialogService);
|
||||
const isOpen = useLiveData(importTemplateDialogService.dialog.isOpen$);
|
||||
const template = useLiveData(importTemplateDialogService.dialog.template$);
|
||||
|
||||
export const ImportTemplateDialog = ({
|
||||
close,
|
||||
snapshotUrl,
|
||||
templateName,
|
||||
templateMode,
|
||||
}: DialogComponentProps<GLOBAL_DIALOG_SCHEMA['import-template']>) => {
|
||||
return (
|
||||
<Modal
|
||||
open={isOpen}
|
||||
open
|
||||
modal={true}
|
||||
persistent
|
||||
withoutCloseButton
|
||||
contentOptions={{
|
||||
className: styles.modal,
|
||||
}}
|
||||
onOpenChange={() => importTemplateDialogService.dialog.close()}
|
||||
onOpenChange={() => close()}
|
||||
>
|
||||
{template && (
|
||||
<Dialog
|
||||
templateName={template.templateName}
|
||||
templateMode={template.templateMode}
|
||||
snapshotUrl={template.snapshotUrl}
|
||||
onClose={() => importTemplateDialogService.dialog.close()}
|
||||
/>
|
||||
)}
|
||||
<Dialog
|
||||
templateName={templateName}
|
||||
templateMode={templateMode}
|
||||
snapshotUrl={snapshotUrl}
|
||||
onClose={() => close()}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,77 @@
|
||||
import { cssVar } from '@toeverything/theme';
|
||||
import { style } from '@vanilla-extract/css';
|
||||
|
||||
export const header = style({
|
||||
position: 'relative',
|
||||
marginTop: '44px',
|
||||
});
|
||||
|
||||
export const subTitle = style({
|
||||
fontSize: cssVar('fontSm'),
|
||||
color: cssVar('textPrimaryColor'),
|
||||
fontWeight: 600,
|
||||
});
|
||||
|
||||
export const avatarWrapper = style({
|
||||
display: 'flex',
|
||||
margin: '10px 0',
|
||||
});
|
||||
|
||||
export const workspaceNameWrapper = style({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '8px',
|
||||
padding: '12px 0',
|
||||
});
|
||||
export const affineCloudWrapper = style({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '6px',
|
||||
paddingTop: '10px',
|
||||
});
|
||||
|
||||
export const card = style({
|
||||
padding: '12px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
borderRadius: '8px',
|
||||
backgroundColor: cssVar('backgroundSecondaryColor'),
|
||||
minHeight: '114px',
|
||||
position: 'relative',
|
||||
});
|
||||
|
||||
export const cardText = style({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center',
|
||||
width: '100%',
|
||||
gap: '12px',
|
||||
});
|
||||
|
||||
export const cardTitle = style({
|
||||
fontSize: cssVar('fontBase'),
|
||||
color: cssVar('textPrimaryColor'),
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
});
|
||||
export const cardDescription = style({
|
||||
fontSize: cssVar('fontXs'),
|
||||
color: cssVar('textSecondaryColor'),
|
||||
maxWidth: '288px',
|
||||
});
|
||||
|
||||
export const cloudTips = style({
|
||||
fontSize: cssVar('fontXs'),
|
||||
color: cssVar('textSecondaryColor'),
|
||||
});
|
||||
|
||||
export const cloudSvgContainer = style({
|
||||
width: '146px',
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-end',
|
||||
alignItems: 'center',
|
||||
position: 'absolute',
|
||||
bottom: '0',
|
||||
right: '0',
|
||||
pointerEvents: 'none',
|
||||
});
|
||||
@@ -0,0 +1,59 @@
|
||||
import { toast } from '@affine/component';
|
||||
import {
|
||||
type DialogComponentProps,
|
||||
type GLOBAL_DIALOG_SCHEMA,
|
||||
} from '@affine/core/modules/dialogs';
|
||||
import { _addLocalWorkspace } from '@affine/core/modules/workspace-engine';
|
||||
import { DebugLogger } from '@affine/debug';
|
||||
import { apis } from '@affine/electron-api';
|
||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import { useService, WorkspacesService } from '@toeverything/infra';
|
||||
import { useLayoutEffect } from 'react';
|
||||
|
||||
const logger = new DebugLogger('ImportWorkspaceDialog');
|
||||
|
||||
export const ImportWorkspaceDialog = ({
|
||||
close,
|
||||
}: DialogComponentProps<GLOBAL_DIALOG_SCHEMA['import-workspace']>) => {
|
||||
const t = useI18n();
|
||||
const workspacesService = useService(WorkspacesService);
|
||||
|
||||
// TODO(@Peng): maybe refactor using xstate?
|
||||
useLayoutEffect(() => {
|
||||
let canceled = false;
|
||||
// a hack for now
|
||||
// when adding a workspace, we will immediately let user select a db file
|
||||
// after it is done, it will effectively add a new workspace to app-data folder
|
||||
// so after that, we will be able to load it via importLocalWorkspace
|
||||
(async () => {
|
||||
if (!apis) {
|
||||
return;
|
||||
}
|
||||
logger.info('load db file');
|
||||
const result = await apis.dialog.loadDBFile();
|
||||
if (result.workspaceId && !canceled) {
|
||||
_addLocalWorkspace(result.workspaceId);
|
||||
workspacesService.list.revalidate();
|
||||
close({
|
||||
workspace: {
|
||||
flavour: WorkspaceFlavour.LOCAL,
|
||||
id: result.workspaceId,
|
||||
},
|
||||
});
|
||||
} else if (result.error || result.canceled) {
|
||||
if (result.error) {
|
||||
toast(t[result.error]());
|
||||
}
|
||||
close();
|
||||
}
|
||||
})().catch(err => {
|
||||
console.error(err);
|
||||
});
|
||||
return () => {
|
||||
canceled = true;
|
||||
};
|
||||
}, [close, t, workspacesService]);
|
||||
|
||||
return null;
|
||||
};
|
||||
365
packages/frontend/core/src/desktop/dialogs/import/index.tsx
Normal file
365
packages/frontend/core/src/desktop/dialogs/import/index.tsx
Normal file
@@ -0,0 +1,365 @@
|
||||
import { Button, IconButton, Modal } from '@affine/component';
|
||||
import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks';
|
||||
import type {
|
||||
DialogComponentProps,
|
||||
GLOBAL_DIALOG_SCHEMA,
|
||||
} from '@affine/core/modules/dialogs';
|
||||
import { UrlService } from '@affine/core/modules/url';
|
||||
import { WorkbenchService } from '@affine/core/modules/workbench';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import {
|
||||
MarkdownTransformer,
|
||||
NotionHtmlTransformer,
|
||||
openFileOrFiles,
|
||||
} from '@blocksuite/affine/blocks';
|
||||
import type { DocCollection } from '@blocksuite/affine/store';
|
||||
import {
|
||||
ExportToMarkdownIcon,
|
||||
HelpIcon,
|
||||
NotionIcon,
|
||||
} from '@blocksuite/icons/rc';
|
||||
import { useService, WorkspaceService } from '@toeverything/infra';
|
||||
import { cssVar } from '@toeverything/theme';
|
||||
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||
import { type ReactElement, useCallback, useState } from 'react';
|
||||
|
||||
import * as style from './styles.css';
|
||||
|
||||
type ImportType = 'markdown' | 'markdownZip' | 'notion';
|
||||
type AcceptType = 'Markdown' | 'Zip';
|
||||
type Status = 'idle' | 'importing' | 'success' | 'error';
|
||||
|
||||
type ImportConfig = {
|
||||
fileOptions: { acceptType: AcceptType; multiple: boolean };
|
||||
importFunction: (
|
||||
docCollection: DocCollection,
|
||||
file: File | File[]
|
||||
) => Promise<string[]>;
|
||||
};
|
||||
|
||||
const DISCORD_URL = 'https://discord.gg/whd5mjYqVw';
|
||||
|
||||
const importOptions = [
|
||||
{
|
||||
label: 'com.affine.import.markdown-files',
|
||||
prefixIcon: (
|
||||
<ExportToMarkdownIcon
|
||||
color={cssVarV2('icon/primary')}
|
||||
width={20}
|
||||
height={20}
|
||||
/>
|
||||
),
|
||||
testId: 'editor-option-menu-import-markdown-files',
|
||||
type: 'markdown' as ImportType,
|
||||
},
|
||||
{
|
||||
label: 'com.affine.import.markdown-with-media-files',
|
||||
prefixIcon: (
|
||||
<ExportToMarkdownIcon
|
||||
color={cssVarV2('icon/primary')}
|
||||
width={20}
|
||||
height={20}
|
||||
/>
|
||||
),
|
||||
testId: 'editor-option-menu-import-markdown-with-media',
|
||||
type: 'markdownZip' as ImportType,
|
||||
},
|
||||
{
|
||||
label: 'com.affine.import.notion',
|
||||
prefixIcon: <NotionIcon color={cssVar('black')} width={20} height={20} />,
|
||||
suffixIcon: (
|
||||
<HelpIcon color={cssVarV2('icon/primary')} width={20} height={20} />
|
||||
),
|
||||
suffixTooltip: 'com.affine.import.notion.tooltip',
|
||||
testId: 'editor-option-menu-import-notion',
|
||||
type: 'notion' as ImportType,
|
||||
},
|
||||
];
|
||||
|
||||
const importConfigs: Record<ImportType, ImportConfig> = {
|
||||
markdown: {
|
||||
fileOptions: { acceptType: 'Markdown', multiple: true },
|
||||
importFunction: async (docCollection, files) => {
|
||||
if (!Array.isArray(files)) {
|
||||
throw new Error('Expected an array of files for markdown files import');
|
||||
}
|
||||
const pageIds: string[] = [];
|
||||
for (const file of files) {
|
||||
const text = await file.text();
|
||||
const fileName = file.name.split('.').slice(0, -1).join('.');
|
||||
const pageId = await MarkdownTransformer.importMarkdownToDoc({
|
||||
collection: docCollection,
|
||||
markdown: text,
|
||||
fileName,
|
||||
});
|
||||
if (pageId) pageIds.push(pageId);
|
||||
}
|
||||
return pageIds;
|
||||
},
|
||||
},
|
||||
markdownZip: {
|
||||
fileOptions: { acceptType: 'Zip', multiple: false },
|
||||
importFunction: async (docCollection, file) => {
|
||||
if (Array.isArray(file)) {
|
||||
throw new Error('Expected a single zip file for markdownZip import');
|
||||
}
|
||||
return MarkdownTransformer.importMarkdownZip({
|
||||
collection: docCollection,
|
||||
imported: file,
|
||||
});
|
||||
},
|
||||
},
|
||||
notion: {
|
||||
fileOptions: { acceptType: 'Zip', multiple: false },
|
||||
importFunction: async (docCollection, file) => {
|
||||
if (Array.isArray(file)) {
|
||||
throw new Error('Expected a single zip file for notion import');
|
||||
}
|
||||
const { pageIds } = await NotionHtmlTransformer.importNotionZip({
|
||||
collection: docCollection,
|
||||
imported: file,
|
||||
});
|
||||
return pageIds;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const ImportOptionItem = ({
|
||||
label,
|
||||
prefixIcon,
|
||||
suffixIcon,
|
||||
suffixTooltip,
|
||||
type,
|
||||
onImport,
|
||||
}: {
|
||||
label: string;
|
||||
prefixIcon: ReactElement;
|
||||
suffixIcon?: ReactElement;
|
||||
suffixTooltip?: string;
|
||||
type: ImportType;
|
||||
onImport: (type: ImportType) => void;
|
||||
}) => {
|
||||
const t = useI18n();
|
||||
return (
|
||||
<div className={style.importItem} onClick={() => onImport(type)}>
|
||||
{prefixIcon}
|
||||
<div className={style.importItemLabel}>{t[label]()}</div>
|
||||
{suffixIcon && (
|
||||
<IconButton
|
||||
className={style.importItemSuffix}
|
||||
icon={suffixIcon}
|
||||
tooltip={suffixTooltip ? t[suffixTooltip]() : undefined}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const ImportOptions = ({
|
||||
onImport,
|
||||
}: {
|
||||
onImport: (type: ImportType) => void;
|
||||
}) => {
|
||||
const t = useI18n();
|
||||
return (
|
||||
<>
|
||||
<div className={style.importModalTitle}>{t['Import']()}</div>
|
||||
<div className={style.importModalContent}>
|
||||
{importOptions.map(
|
||||
({ label, prefixIcon, suffixIcon, suffixTooltip, testId, type }) => (
|
||||
<ImportOptionItem
|
||||
key={testId}
|
||||
prefixIcon={prefixIcon}
|
||||
suffixIcon={suffixIcon}
|
||||
suffixTooltip={suffixTooltip}
|
||||
label={label}
|
||||
data-testid={testId}
|
||||
type={type}
|
||||
onImport={onImport}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
<div className={style.importModalTip}>
|
||||
{t['com.affine.import.modal.tip']()}{' '}
|
||||
<a
|
||||
className={style.link}
|
||||
href="https://discord.gg/whd5mjYqVw"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
Discord
|
||||
</a>{' '}
|
||||
.
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const ImportingStatus = () => {
|
||||
const t = useI18n();
|
||||
return (
|
||||
<>
|
||||
<div className={style.importModalTitle}>
|
||||
{t['com.affine.import.status.importing.title']()}
|
||||
</div>
|
||||
<p className={style.importStatusContent}>
|
||||
{t['com.affine.import.status.importing.message']()}
|
||||
</p>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const SuccessStatus = ({ onComplete }: { onComplete: () => void }) => {
|
||||
const t = useI18n();
|
||||
return (
|
||||
<>
|
||||
<div className={style.importModalTitle}>
|
||||
{t['com.affine.import.status.success.title']()}
|
||||
</div>
|
||||
<p className={style.importStatusContent}>
|
||||
{t['com.affine.import.status.success.message']()}{' '}
|
||||
<a
|
||||
className={style.link}
|
||||
href={DISCORD_URL}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
Discord
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
<div className={style.importModalButtonContainer}>
|
||||
<Button onClick={onComplete} variant="primary">
|
||||
{t['Complete']()}
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const ErrorStatus = ({
|
||||
error,
|
||||
onRetry,
|
||||
}: {
|
||||
error: string | null;
|
||||
onRetry: () => void;
|
||||
}) => {
|
||||
const t = useI18n();
|
||||
const urlService = useService(UrlService);
|
||||
return (
|
||||
<>
|
||||
<div className={style.importModalTitle}>
|
||||
{t['com.affine.import.status.failed.title']()}
|
||||
</div>
|
||||
<p className={style.importStatusContent}>
|
||||
{error || 'Unknown error occurred'}
|
||||
</p>
|
||||
<div className={style.importModalButtonContainer}>
|
||||
<Button
|
||||
onClick={() => {
|
||||
urlService.openPopupWindow(DISCORD_URL);
|
||||
}}
|
||||
variant="secondary"
|
||||
>
|
||||
{t['Feedback']()}
|
||||
</Button>
|
||||
<Button onClick={onRetry} variant="primary">
|
||||
{t['Retry']()}
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const ImportDialog = ({
|
||||
close,
|
||||
}: DialogComponentProps<GLOBAL_DIALOG_SCHEMA['import']>) => {
|
||||
const t = useI18n();
|
||||
const [status, setStatus] = useState<Status>('idle');
|
||||
const [importError, setImportError] = useState<string | null>(null);
|
||||
const [pageIds, setPageIds] = useState<string[]>([]);
|
||||
const workspace = useService(WorkspaceService).workspace;
|
||||
const workbench = useService(WorkbenchService).workbench;
|
||||
const docCollection = workspace.docCollection;
|
||||
|
||||
const handleImport = useAsyncCallback(
|
||||
async (type: ImportType) => {
|
||||
setImportError(null);
|
||||
try {
|
||||
const importConfig = importConfigs[type];
|
||||
const file = await openFileOrFiles(importConfig.fileOptions);
|
||||
|
||||
if (!file || (Array.isArray(file) && file.length === 0)) {
|
||||
throw new Error(
|
||||
t['com.affine.import.status.failed.message.no-file-selected']()
|
||||
);
|
||||
}
|
||||
|
||||
setStatus('importing');
|
||||
|
||||
const pageIds = await importConfig.importFunction(docCollection, file);
|
||||
|
||||
setPageIds(pageIds);
|
||||
setStatus('success');
|
||||
} catch (error) {
|
||||
setImportError(
|
||||
error instanceof Error ? error.message : 'Unknown error occurred'
|
||||
);
|
||||
setStatus('error');
|
||||
}
|
||||
},
|
||||
[docCollection, t]
|
||||
);
|
||||
|
||||
const handleComplete = useCallback(() => {
|
||||
if (pageIds.length > 1) {
|
||||
workbench.openAll();
|
||||
} else if (pageIds.length === 1) {
|
||||
workbench.openDoc(pageIds[0]);
|
||||
}
|
||||
close();
|
||||
}, [pageIds, close, workbench]);
|
||||
|
||||
const handleRetry = () => {
|
||||
setStatus('idle');
|
||||
};
|
||||
|
||||
const statusComponents = {
|
||||
idle: <ImportOptions onImport={handleImport} />,
|
||||
importing: <ImportingStatus />,
|
||||
success: <SuccessStatus onComplete={handleComplete} />,
|
||||
error: <ErrorStatus error={importError} onRetry={handleRetry} />,
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
open
|
||||
onOpenChange={() => {
|
||||
close();
|
||||
}}
|
||||
width={480}
|
||||
contentOptions={{
|
||||
['data-testid' as string]: 'import-modal',
|
||||
style: {
|
||||
maxHeight: '85vh',
|
||||
maxWidth: '70vw',
|
||||
minHeight: '126px',
|
||||
padding: 0,
|
||||
overflow: 'hidden',
|
||||
display: 'flex',
|
||||
background: cssVarV2('layer/background/primary'),
|
||||
},
|
||||
}}
|
||||
closeButtonOptions={{
|
||||
className: style.closeButton,
|
||||
}}
|
||||
withoutCloseButton={status === 'importing'}
|
||||
persistent={status === 'importing'}
|
||||
>
|
||||
<div className={style.importModalContainer}>
|
||||
{statusComponents[status]}
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
110
packages/frontend/core/src/desktop/dialogs/import/styles.css.ts
Normal file
110
packages/frontend/core/src/desktop/dialogs/import/styles.css.ts
Normal file
@@ -0,0 +1,110 @@
|
||||
import { cssVar } from '@toeverything/theme';
|
||||
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||
import { style } from '@vanilla-extract/css';
|
||||
|
||||
export const importModalContainer = style({
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
boxSizing: 'border-box',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
padding: '20px 24px',
|
||||
gap: '12px',
|
||||
});
|
||||
|
||||
export const importModalTitle = style({
|
||||
width: '100%',
|
||||
height: 'auto',
|
||||
fontSize: cssVar('fontH6'),
|
||||
fontWeight: '600',
|
||||
lineHeight: cssVar('lineHeight'),
|
||||
});
|
||||
|
||||
export const importModalContent = style({
|
||||
width: '100%',
|
||||
flex: 1,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '12px',
|
||||
});
|
||||
|
||||
export const closeButton = style({
|
||||
top: '24px',
|
||||
right: '24px',
|
||||
});
|
||||
|
||||
export const importModalTip = style({
|
||||
width: '100%',
|
||||
height: 'auto',
|
||||
fontSize: cssVar('fontSm'),
|
||||
lineHeight: cssVar('lineHeight'),
|
||||
fontWeight: '400',
|
||||
color: cssVar('textSecondaryColor'),
|
||||
});
|
||||
|
||||
export const link = style({
|
||||
color: cssVar('linkColor'),
|
||||
cursor: 'pointer',
|
||||
});
|
||||
|
||||
export const importStatusContent = style({
|
||||
width: '100%',
|
||||
fontSize: cssVar('fontBase'),
|
||||
lineHeight: cssVar('lineHeight'),
|
||||
fontWeight: '400',
|
||||
color: cssVar('textPrimaryColor'),
|
||||
});
|
||||
|
||||
export const importModalButtonContainer = style({
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
gap: '20px',
|
||||
justifyContent: 'end',
|
||||
marginTop: '20px',
|
||||
});
|
||||
|
||||
export const importItem = style({
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
width: '100%',
|
||||
height: 'auto',
|
||||
gap: '4px',
|
||||
padding: '8px 12px',
|
||||
borderRadius: '8px',
|
||||
border: `1px solid ${cssVarV2('layer/insideBorder/border')}`,
|
||||
background: cssVarV2('button/secondary'),
|
||||
selectors: {
|
||||
'&:hover': {
|
||||
background: cssVarV2('layer/background/hoverOverlay'),
|
||||
cursor: 'pointer',
|
||||
transition: 'background .30s',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const importItemLabel = style({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
padding: '0 4px',
|
||||
textAlign: 'left',
|
||||
flex: 1,
|
||||
color: cssVar('textPrimaryColor'),
|
||||
fontSize: cssVar('fontBase'),
|
||||
lineHeight: cssVar('lineHeight'),
|
||||
fontWeight: '500',
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
});
|
||||
|
||||
export const importItemPrefix = style({
|
||||
marginRight: 'auto',
|
||||
});
|
||||
|
||||
export const importItemSuffix = style({
|
||||
marginLeft: 'auto',
|
||||
});
|
||||
96
packages/frontend/core/src/desktop/dialogs/index.tsx
Normal file
96
packages/frontend/core/src/desktop/dialogs/index.tsx
Normal file
@@ -0,0 +1,96 @@
|
||||
import { AuthModal } from '@affine/core/components/affine/auth';
|
||||
import {
|
||||
type DialogComponentProps,
|
||||
type GLOBAL_DIALOG_SCHEMA,
|
||||
GlobalDialogService,
|
||||
WorkspaceDialogService,
|
||||
} from '@affine/core/modules/dialogs';
|
||||
import type { WORKSPACE_DIALOG_SCHEMA } from '@affine/core/modules/dialogs/constant';
|
||||
import { useLiveData, useService } from '@toeverything/infra';
|
||||
|
||||
import { CollectionEditorDialog } from './collection-editor';
|
||||
import { CreateWorkspaceDialog } from './create-workspace';
|
||||
import { DocInfoDialog } from './doc-info';
|
||||
import { ImportDialog } from './import';
|
||||
import { ImportTemplateDialog } from './import-template';
|
||||
import { ImportWorkspaceDialog } from './import-workspace';
|
||||
import { CollectionSelectorDialog } from './selectors/collection';
|
||||
import { DocSelectorDialog } from './selectors/doc';
|
||||
import { TagSelectorDialog } from './selectors/tag';
|
||||
import { SettingDialog } from './setting';
|
||||
|
||||
const GLOBAL_DIALOGS = {
|
||||
'create-workspace': CreateWorkspaceDialog,
|
||||
'import-workspace': ImportWorkspaceDialog,
|
||||
'import-template': ImportTemplateDialog,
|
||||
setting: SettingDialog,
|
||||
import: ImportDialog,
|
||||
} satisfies {
|
||||
[key in keyof GLOBAL_DIALOG_SCHEMA]?: React.FC<
|
||||
DialogComponentProps<GLOBAL_DIALOG_SCHEMA[key]>
|
||||
>;
|
||||
};
|
||||
|
||||
const WORKSPACE_DIALOGS = {
|
||||
'doc-info': DocInfoDialog,
|
||||
'collection-editor': CollectionEditorDialog,
|
||||
'tag-selector': TagSelectorDialog,
|
||||
'doc-selector': DocSelectorDialog,
|
||||
'collection-selector': CollectionSelectorDialog,
|
||||
} satisfies {
|
||||
[key in keyof WORKSPACE_DIALOG_SCHEMA]?: React.FC<
|
||||
DialogComponentProps<WORKSPACE_DIALOG_SCHEMA[key]>
|
||||
>;
|
||||
};
|
||||
|
||||
export const GlobalDialogs = () => {
|
||||
const globalDialogService = useService(GlobalDialogService);
|
||||
const dialogs = useLiveData(globalDialogService.dialogs$);
|
||||
return (
|
||||
<>
|
||||
{dialogs.map(dialog => {
|
||||
const DialogComponent =
|
||||
GLOBAL_DIALOGS[dialog.type as keyof typeof GLOBAL_DIALOGS];
|
||||
if (!DialogComponent) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<DialogComponent
|
||||
key={dialog.id}
|
||||
{...(dialog.props as any)}
|
||||
close={(result?: unknown) => {
|
||||
globalDialogService.close(dialog.id, result);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
<AuthModal />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const WorkspaceDialogs = () => {
|
||||
const workspaceDialogService = useService(WorkspaceDialogService);
|
||||
const dialogs = useLiveData(workspaceDialogService.dialogs$);
|
||||
return (
|
||||
<>
|
||||
{dialogs.map(dialog => {
|
||||
const DialogComponent =
|
||||
WORKSPACE_DIALOGS[dialog.type as keyof typeof WORKSPACE_DIALOGS];
|
||||
if (!DialogComponent) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<DialogComponent
|
||||
key={dialog.id}
|
||||
{...(dialog.props as any)}
|
||||
close={(result?: unknown) => {
|
||||
workspaceDialogService.close(dialog.id, result);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,19 +1,23 @@
|
||||
import { toast } from '@affine/component';
|
||||
import { Modal, toast } from '@affine/component';
|
||||
import {
|
||||
collectionHeaderColsDef,
|
||||
CollectionListItemRenderer,
|
||||
type CollectionMeta,
|
||||
FavoriteTag,
|
||||
type ListItem,
|
||||
ListTableHeader,
|
||||
VirtualizedList,
|
||||
} from '@affine/core/components/page-list';
|
||||
import { SelectorLayout } from '@affine/core/components/page-list/selector/selector-layout';
|
||||
import { CollectionService } from '@affine/core/modules/collection';
|
||||
import type { DialogComponentProps } from '@affine/core/modules/dialogs';
|
||||
import type { WORKSPACE_DIALOG_SCHEMA } from '@affine/core/modules/dialogs/constant';
|
||||
import { CompatibleFavoriteItemsAdapter } from '@affine/core/modules/favorite';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import { useLiveData, useService, WorkspaceService } from '@toeverything/infra';
|
||||
import { cssVar } from '@toeverything/theme';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
|
||||
import { FavoriteTag } from '../components/favorite-tag';
|
||||
import { collectionHeaderColsDef } from '../header-col-def';
|
||||
import { CollectionListItemRenderer } from '../page-group';
|
||||
import { ListTableHeader } from '../page-header';
|
||||
import type { BaseSelectorDialogProps } from '../selector';
|
||||
import { SelectorLayout } from '../selector/selector-layout';
|
||||
import type { CollectionMeta, ListItem } from '../types';
|
||||
import { VirtualizedList } from '../virtualized-list';
|
||||
|
||||
const FavoriteOperation = ({ collection }: { collection: ListItem }) => {
|
||||
const t = useI18n();
|
||||
const favAdapter = useService(CompatibleFavoriteItemsAdapter);
|
||||
@@ -39,17 +43,16 @@ const FavoriteOperation = ({ collection }: { collection: ListItem }) => {
|
||||
);
|
||||
};
|
||||
|
||||
export const SelectCollection = ({
|
||||
init = [],
|
||||
onCancel,
|
||||
onConfirm,
|
||||
}: BaseSelectorDialogProps<string[]>) => {
|
||||
export const CollectionSelectorDialog = ({
|
||||
close,
|
||||
init: selectedCollectionIds,
|
||||
}: DialogComponentProps<WORKSPACE_DIALOG_SCHEMA['collection-selector']>) => {
|
||||
const t = useI18n();
|
||||
const collectionService = useService(CollectionService);
|
||||
const workspace = useService(WorkspaceService).workspace;
|
||||
|
||||
const collections = useLiveData(collectionService.collections$);
|
||||
const [selection, setSelection] = useState(init);
|
||||
const [selection, setSelection] = useState(selectedCollectionIds);
|
||||
const [keyword, setKeyword] = useState('');
|
||||
|
||||
const collectionMetas = useMemo(() => {
|
||||
@@ -80,28 +83,43 @@ export const SelectCollection = ({
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<SelectorLayout
|
||||
searchPlaceholder={t[
|
||||
'com.affine.selector-collection.search.placeholder'
|
||||
]()}
|
||||
selectedCount={selection.length}
|
||||
onSearch={setKeyword}
|
||||
onClear={() => setSelection([])}
|
||||
onCancel={() => onCancel?.()}
|
||||
onConfirm={() => onConfirm?.(selection)}
|
||||
<Modal
|
||||
open
|
||||
onOpenChange={() => close()}
|
||||
withoutCloseButton
|
||||
width="calc(100% - 32px)"
|
||||
height="80%"
|
||||
contentOptions={{
|
||||
style: {
|
||||
padding: 0,
|
||||
maxWidth: 976,
|
||||
background: cssVar('backgroundPrimaryColor'),
|
||||
},
|
||||
}}
|
||||
>
|
||||
<VirtualizedList
|
||||
selectable={true}
|
||||
draggable={false}
|
||||
selectedIds={selection}
|
||||
onSelectedIdsChange={setSelection}
|
||||
items={collectionMetas}
|
||||
itemRenderer={collectionItemRenderer}
|
||||
rowAsLink
|
||||
docCollection={workspace.docCollection}
|
||||
operationsRenderer={collectionOperationRenderer}
|
||||
headerRenderer={collectionHeaderRenderer}
|
||||
/>
|
||||
</SelectorLayout>
|
||||
<SelectorLayout
|
||||
searchPlaceholder={t[
|
||||
'com.affine.selector-collection.search.placeholder'
|
||||
]()}
|
||||
selectedCount={selection.length}
|
||||
onSearch={setKeyword}
|
||||
onClear={() => setSelection([])}
|
||||
onCancel={() => close()}
|
||||
onConfirm={() => close(selection)}
|
||||
>
|
||||
<VirtualizedList
|
||||
selectable={true}
|
||||
draggable={false}
|
||||
selectedIds={selection}
|
||||
onSelectedIdsChange={setSelection}
|
||||
items={collectionMetas}
|
||||
itemRenderer={collectionItemRenderer}
|
||||
rowAsLink
|
||||
docCollection={workspace.docCollection}
|
||||
operationsRenderer={collectionOperationRenderer}
|
||||
headerRenderer={collectionHeaderRenderer}
|
||||
/>
|
||||
</SelectorLayout>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
35
packages/frontend/core/src/desktop/dialogs/selectors/doc.tsx
Normal file
35
packages/frontend/core/src/desktop/dialogs/selectors/doc.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
import { Modal } from '@affine/component';
|
||||
import { SelectPage } from '@affine/core/components/page-list/docs/select-page';
|
||||
import type {
|
||||
DialogComponentProps,
|
||||
WORKSPACE_DIALOG_SCHEMA,
|
||||
} from '@affine/core/modules/dialogs';
|
||||
import { cssVar } from '@toeverything/theme';
|
||||
|
||||
export const DocSelectorDialog = ({
|
||||
close,
|
||||
init: selectedDocIds,
|
||||
}: DialogComponentProps<WORKSPACE_DIALOG_SCHEMA['doc-selector']>) => {
|
||||
return (
|
||||
<Modal
|
||||
open
|
||||
onOpenChange={() => close()}
|
||||
withoutCloseButton
|
||||
width="calc(100% - 32px)"
|
||||
height="80%"
|
||||
contentOptions={{
|
||||
style: {
|
||||
padding: 0,
|
||||
maxWidth: 976,
|
||||
background: cssVar('backgroundPrimaryColor'),
|
||||
},
|
||||
}}
|
||||
>
|
||||
<SelectPage
|
||||
init={selectedDocIds}
|
||||
onCancel={() => close()}
|
||||
onConfirm={value => close(value)}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
@@ -1,19 +1,25 @@
|
||||
import { toast } from '@affine/component';
|
||||
import { Modal, toast } from '@affine/component';
|
||||
import {
|
||||
FavoriteTag,
|
||||
type ListItem,
|
||||
ListTableHeader,
|
||||
tagHeaderColsDef,
|
||||
TagListItemRenderer,
|
||||
type TagMeta,
|
||||
VirtualizedList,
|
||||
} from '@affine/core/components/page-list';
|
||||
import { SelectorLayout } from '@affine/core/components/page-list/selector/selector-layout';
|
||||
import type {
|
||||
DialogComponentProps,
|
||||
WORKSPACE_DIALOG_SCHEMA,
|
||||
} from '@affine/core/modules/dialogs';
|
||||
import { FavoriteService } from '@affine/core/modules/favorite';
|
||||
import { TagService } from '@affine/core/modules/tag';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import { useLiveData, useService, WorkspaceService } from '@toeverything/infra';
|
||||
import { cssVar } from '@toeverything/theme';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
|
||||
import { FavoriteTag } from '../components/favorite-tag';
|
||||
import { tagHeaderColsDef } from '../header-col-def';
|
||||
import { TagListItemRenderer } from '../page-group';
|
||||
import { ListTableHeader } from '../page-header';
|
||||
import type { BaseSelectorDialogProps } from '../selector';
|
||||
import { SelectorLayout } from '../selector/selector-layout';
|
||||
import type { ListItem, TagMeta } from '../types';
|
||||
import { VirtualizedList } from '../virtualized-list';
|
||||
|
||||
const FavoriteOperation = ({ tag }: { tag: ListItem }) => {
|
||||
const t = useI18n();
|
||||
const favoriteService = useService(FavoriteService);
|
||||
@@ -39,17 +45,16 @@ const FavoriteOperation = ({ tag }: { tag: ListItem }) => {
|
||||
);
|
||||
};
|
||||
|
||||
export const SelectTag = ({
|
||||
init = [],
|
||||
onConfirm,
|
||||
onCancel,
|
||||
}: BaseSelectorDialogProps<string[]>) => {
|
||||
export const TagSelectorDialog = ({
|
||||
close,
|
||||
init: selectedTagIds,
|
||||
}: DialogComponentProps<WORKSPACE_DIALOG_SCHEMA['tag-selector']>) => {
|
||||
const t = useI18n();
|
||||
|
||||
const workspace = useService(WorkspaceService).workspace;
|
||||
const tagList = useService(TagService).tagList;
|
||||
|
||||
const [selection, setSelection] = useState(init);
|
||||
const [selection, setSelection] = useState(selectedTagIds);
|
||||
const [keyword, setKeyword] = useState('');
|
||||
const tagMetas: TagMeta[] = useLiveData(tagList.tagMetas$);
|
||||
|
||||
@@ -73,25 +78,40 @@ export const SelectTag = ({
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<SelectorLayout
|
||||
searchPlaceholder={t['com.affine.selector-tag.search.placeholder']()}
|
||||
selectedCount={selection.length}
|
||||
onSearch={setKeyword}
|
||||
onConfirm={() => onConfirm?.(selection)}
|
||||
onCancel={onCancel}
|
||||
onClear={() => setSelection([])}
|
||||
<Modal
|
||||
open
|
||||
onOpenChange={() => close()}
|
||||
withoutCloseButton
|
||||
width="calc(100% - 32px)"
|
||||
height="80%"
|
||||
contentOptions={{
|
||||
style: {
|
||||
padding: 0,
|
||||
maxWidth: 976,
|
||||
background: cssVar('backgroundPrimaryColor'),
|
||||
},
|
||||
}}
|
||||
>
|
||||
<VirtualizedList
|
||||
selectable={true}
|
||||
draggable={false}
|
||||
selectedIds={selection}
|
||||
onSelectedIdsChange={setSelection}
|
||||
items={filteredTagMetas}
|
||||
docCollection={workspace.docCollection}
|
||||
itemRenderer={tagItemRenderer}
|
||||
operationsRenderer={tagOperationRenderer}
|
||||
headerRenderer={tagHeaderRenderer}
|
||||
/>
|
||||
</SelectorLayout>
|
||||
<SelectorLayout
|
||||
searchPlaceholder={t['com.affine.selector-tag.search.placeholder']()}
|
||||
selectedCount={selection.length}
|
||||
onSearch={setKeyword}
|
||||
onConfirm={() => close(selection)}
|
||||
onCancel={close}
|
||||
onClear={() => setSelection([])}
|
||||
>
|
||||
<VirtualizedList
|
||||
selectable={true}
|
||||
draggable={false}
|
||||
selectedIds={selection}
|
||||
onSelectedIdsChange={setSelection}
|
||||
items={filteredTagMetas}
|
||||
docCollection={workspace.docCollection}
|
||||
itemRenderer={tagItemRenderer}
|
||||
operationsRenderer={tagOperationRenderer}
|
||||
headerRenderer={tagHeaderRenderer}
|
||||
/>
|
||||
</SelectorLayout>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
@@ -1,6 +1,5 @@
|
||||
import { Button, ErrorMessage, Skeleton } from '@affine/component';
|
||||
import { SettingRow } from '@affine/component/setting-components';
|
||||
import { openSettingModalAtom } from '@affine/core/components/atoms';
|
||||
import {
|
||||
ServerConfigService,
|
||||
SubscriptionService,
|
||||
@@ -11,15 +10,18 @@ import { useI18n } from '@affine/i18n';
|
||||
import { track } from '@affine/track';
|
||||
import { useLiveData, useService } from '@toeverything/infra';
|
||||
import { cssVar } from '@toeverything/theme';
|
||||
import { useSetAtom } from 'jotai';
|
||||
import { useCallback, useEffect } from 'react';
|
||||
|
||||
import { AIResume, AISubscribe } from '../general-setting/plans/ai/actions';
|
||||
import type { SettingState } from '../types';
|
||||
import * as styles from './storage-progress.css';
|
||||
|
||||
export const AIUsagePanel = () => {
|
||||
export const AIUsagePanel = ({
|
||||
onChangeSettingState,
|
||||
}: {
|
||||
onChangeSettingState?: (settingState: SettingState) => void;
|
||||
}) => {
|
||||
const t = useI18n();
|
||||
const setOpenSettingModal = useSetAtom(openSettingModalAtom);
|
||||
const serverConfigService = useService(ServerConfigService);
|
||||
const hasPaymentFeature = useLiveData(
|
||||
serverConfigService.serverConfig.features$.map(f => f?.payment)
|
||||
@@ -44,12 +46,11 @@ export const AIUsagePanel = () => {
|
||||
const loadError = useLiveData(copilotQuotaService.copilotQuota.error$);
|
||||
|
||||
const openBilling = useCallback(() => {
|
||||
setOpenSettingModal({
|
||||
open: true,
|
||||
onChangeSettingState?.({
|
||||
activeTab: 'billing',
|
||||
});
|
||||
track.$.settingsPanel.accountUsage.viewPlans({ plan: SubscriptionPlan.AI });
|
||||
}, [setOpenSettingModal]);
|
||||
}, [onChangeSettingState]);
|
||||
|
||||
if (loading) {
|
||||
if (loadError) {
|
||||
@@ -5,8 +5,11 @@ import {
|
||||
} from '@affine/component/setting-components';
|
||||
import { Avatar } from '@affine/component/ui/avatar';
|
||||
import { Button } from '@affine/component/ui/button';
|
||||
import { authAtom } from '@affine/core/components/atoms';
|
||||
import { useSignOut } from '@affine/core/components/hooks/affine/use-sign-out';
|
||||
import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks';
|
||||
import { useCatchEventCallback } from '@affine/core/components/hooks/use-catch-event-hook';
|
||||
import { Upload } from '@affine/core/components/pure/file-upload';
|
||||
import { SubscriptionPlan } from '@affine/graphql';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import { track } from '@affine/track';
|
||||
@@ -18,16 +21,10 @@ import {
|
||||
useServices,
|
||||
} from '@toeverything/infra';
|
||||
import { useSetAtom } from 'jotai';
|
||||
import type { FC } from 'react';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
import { AuthService, ServerConfigService } from '../../../../modules/cloud';
|
||||
import {
|
||||
authAtom,
|
||||
openSettingModalAtom,
|
||||
openSignOutModalAtom,
|
||||
} from '../../../atoms';
|
||||
import { Upload } from '../../../pure/file-upload';
|
||||
import type { SettingState } from '../types';
|
||||
import { AIUsagePanel } from './ai-usage-panel';
|
||||
import { StorageProgress } from './storage-progress';
|
||||
import * as styles from './style.css';
|
||||
@@ -148,20 +145,22 @@ export const AvatarAndName = () => {
|
||||
);
|
||||
};
|
||||
|
||||
const StoragePanel = () => {
|
||||
const StoragePanel = ({
|
||||
onChangeSettingState,
|
||||
}: {
|
||||
onChangeSettingState?: (settingState: SettingState) => void;
|
||||
}) => {
|
||||
const t = useI18n();
|
||||
|
||||
const setSettingModalAtom = useSetAtom(openSettingModalAtom);
|
||||
const onUpgrade = useCallback(() => {
|
||||
track.$.settingsPanel.accountUsage.viewPlans({
|
||||
plan: SubscriptionPlan.Pro,
|
||||
});
|
||||
setSettingModalAtom({
|
||||
open: true,
|
||||
onChangeSettingState?.({
|
||||
activeTab: 'plans',
|
||||
scrollAnchor: 'cloudPricingPlan',
|
||||
});
|
||||
}, [setSettingModalAtom]);
|
||||
}, [onChangeSettingState]);
|
||||
|
||||
return (
|
||||
<SettingRow
|
||||
@@ -174,7 +173,11 @@ const StoragePanel = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export const AccountSetting: FC = () => {
|
||||
export const AccountSetting = ({
|
||||
onChangeSettingState,
|
||||
}: {
|
||||
onChangeSettingState?: (settingState: SettingState) => void;
|
||||
}) => {
|
||||
const { authService, serverConfigService } = useServices({
|
||||
AuthService,
|
||||
ServerConfigService,
|
||||
@@ -189,7 +192,7 @@ export const AccountSetting: FC = () => {
|
||||
}, [session]);
|
||||
const account = useEnsureLiveData(session.account$);
|
||||
const setAuthModal = useSetAtom(authAtom);
|
||||
const setSignOutModal = useSetAtom(openSignOutModalAtom);
|
||||
const openSignOutModal = useSignOut();
|
||||
|
||||
const onChangeEmail = useCallback(() => {
|
||||
setAuthModal({
|
||||
@@ -211,10 +214,6 @@ export const AccountSetting: FC = () => {
|
||||
});
|
||||
}, [account.email, account.info?.hasPassword, setAuthModal]);
|
||||
|
||||
const onOpenSignOutModal = useCallback(() => {
|
||||
setSignOutModal(true);
|
||||
}, [setSignOutModal]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<SettingHeader
|
||||
@@ -240,14 +239,16 @@ export const AccountSetting: FC = () => {
|
||||
: t['com.affine.settings.password.action.set']()}
|
||||
</Button>
|
||||
</SettingRow>
|
||||
<StoragePanel />
|
||||
{serverFeatures?.copilot && <AIUsagePanel />}
|
||||
<StoragePanel onChangeSettingState={onChangeSettingState} />
|
||||
{serverFeatures?.copilot && (
|
||||
<AIUsagePanel onChangeSettingState={onChangeSettingState} />
|
||||
)}
|
||||
<SettingRow
|
||||
name={t[`Sign out`]()}
|
||||
desc={t['com.affine.setting.sign.out.message']()}
|
||||
style={{ cursor: 'pointer' }}
|
||||
data-testid="sign-out-button"
|
||||
onClick={onOpenSignOutModal}
|
||||
onClick={openSignOutModal}
|
||||
>
|
||||
<ArrowRightSmallIcon />
|
||||
</SettingRow>
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
} from '@affine/component/setting-components';
|
||||
import { useAppUpdater } from '@affine/core/components/hooks/use-app-updater';
|
||||
import { UrlService } from '@affine/core/modules/url';
|
||||
import { appIconMap, appNames } from '@affine/core/utils';
|
||||
import { appIconMap, appNames } from '@affine/core/utils/channel';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import { mixpanel } from '@affine/track';
|
||||
import { ArrowRightSmallIcon, OpenInNewIcon } from '@blocksuite/icons/rc';
|
||||
@@ -24,18 +24,18 @@ import { UpdateCheckSection } from './update-check-section';
|
||||
|
||||
export const AboutAffine = () => {
|
||||
const t = useI18n();
|
||||
const { appSettings, updateSettings } = useAppSettingHelper();
|
||||
const { toggleAutoCheck, toggleAutoDownload } = useAppUpdater();
|
||||
const channel = BUILD_CONFIG.appBuildType;
|
||||
const appIcon = appIconMap[channel];
|
||||
const appName = appNames[channel];
|
||||
const { urlService, featureFlagService } = useServices({
|
||||
UrlService,
|
||||
FeatureFlagService,
|
||||
});
|
||||
const { appSettings, updateSettings } = useAppSettingHelper();
|
||||
const { toggleAutoCheck, toggleAutoDownload } = useAppUpdater();
|
||||
const enableSnapshotImportExport = useLiveData(
|
||||
featureFlagService.flags.enable_snapshot_import_export.$
|
||||
);
|
||||
const channel = BUILD_CONFIG.appBuildType;
|
||||
const appIcon = appIconMap[channel];
|
||||
const appName = appNames[channel];
|
||||
|
||||
const onSwitchAutoCheck = useCallback(
|
||||
(checked: boolean) => {
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
SettingRow,
|
||||
SettingWrapper,
|
||||
} from '@affine/component/setting-components';
|
||||
import { LanguageMenu } from '@affine/core/components/affine/language-menu';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import {
|
||||
FeatureFlagService,
|
||||
@@ -15,7 +16,6 @@ import { useTheme } from 'next-themes';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
|
||||
import { useAppSettingHelper } from '../../../../../components/hooks/affine/use-app-setting-helper';
|
||||
import { LanguageMenu } from '../../../language-menu';
|
||||
import { OpenInAppLinksMenu } from './links';
|
||||
import { settingWrapper } from './style.css';
|
||||
import { ThemeEditorSetting } from './theme-editor-setting';
|
||||
@@ -118,6 +118,7 @@ export const AppearanceSettings = () => {
|
||||
</SettingRow>
|
||||
</SettingWrapper>
|
||||
) : null}
|
||||
|
||||
{BUILD_CONFIG.isElectron ? (
|
||||
<SettingWrapper
|
||||
title={t['com.affine.appearanceSettings.sidebar.title']()}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user