refactor(core): desktop project struct (#8334)

This commit is contained in:
EYHN
2024-11-05 11:00:33 +08:00
committed by GitHub
parent 89d09fd5e9
commit 902635e60f
343 changed files with 3846 additions and 3508 deletions

View File

@@ -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);
},
})
);

View File

@@ -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,
});

View File

@@ -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,
}));
});
},
})
);

View File

@@ -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,
]);

View File

@@ -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));
}, []);

View File

@@ -1,5 +0,0 @@
import { style } from '@vanilla-extract/css';
export const electronFallback = style({
paddingTop: 52,
});

View File

@@ -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>
);
};

View File

@@ -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();

View File

@@ -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}>

View File

@@ -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

View File

@@ -1,10 +0,0 @@
import { HelpIsland } from '../../pure/help-island';
import { ToolContainer } from '../../workspace';
export const HubIsland = () => {
return (
<ToolContainer>
<HelpIsland />
</ToolContainer>
);
};

View File

@@ -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();

View File

@@ -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) {

View File

@@ -1,3 +0,0 @@
import { atom } from 'jotai';
export const settingModalScrollContainerAtom = atom<HTMLElement | null>(null);

View File

@@ -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}`;

View File

@@ -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} />
);
}
};

View File

@@ -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) {

View File

@@ -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);

View File

@@ -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 =

View File

@@ -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,

View File

@@ -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

View File

@@ -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(() => {

View File

@@ -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}
/>
</>
);
};

View File

@@ -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]
)}

View File

@@ -1,2 +1 @@
export * from './info-modal/info-modal';
export * from './table';

View File

@@ -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();
},
})
);

View File

@@ -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]);
};

View File

@@ -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,
};
}

View File

@@ -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]);
}

View File

@@ -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>
);
};

View File

@@ -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(() => {

View File

@@ -1,4 +1,3 @@
export * from './collection-list-header';
export * from './collection-list-item';
export * from './select-collection';
export * from './virtualized-collection-list';

View File

@@ -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>
);
};

View File

@@ -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',
});

View File

@@ -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(

View File

@@ -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[]>([]);

View File

@@ -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();

View File

@@ -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(() => {

View File

@@ -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');
};

View File

@@ -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;
};

View File

@@ -1,4 +1,3 @@
export * from './select-tag';
export * from './tag-list-header';
export * from './tag-list-item';
export * from './virtualized-tag-list';

View File

@@ -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' });

View File

@@ -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';

View File

@@ -8,3 +8,8 @@ export const button = style({
fontWeight: 500,
height: '28px',
});
export const createTips = style({
color: cssVar('textSecondaryColor'),
fontSize: 12,
lineHeight: '20px',
});

View File

@@ -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}

View File

@@ -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);
},
});
}),
};
};

View File

@@ -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} />;
};

View File

@@ -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 />
</>
);
};

View File

@@ -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';

View File

@@ -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'),

View File

@@ -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>

View File

@@ -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);

View File

@@ -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');
}
}
},

View File

@@ -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$);

View File

@@ -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(

View File

@@ -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>
);
};

View File

@@ -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',
},
},
});

View File

@@ -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>
);
};

View File

@@ -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>
);
};

View File

@@ -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';

View File

@@ -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%',
});

View File

@@ -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'),
});

View File

@@ -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;

View File

@@ -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>
);
};

View File

@@ -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 = ({

View File

@@ -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}
/>
);
};

View File

@@ -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>
);
};

View File

@@ -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,

View File

@@ -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 = ({

View File

@@ -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'),
});

View File

@@ -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>
);
};

View File

@@ -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',
});

View File

@@ -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;
};

View 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>
);
};

View 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',
});

View 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);
}}
/>
);
})}
</>
);
};

View File

@@ -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>
);
};

View 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>
);
};

View File

@@ -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>
);
};

View File

@@ -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) {

View File

@@ -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>

View File

@@ -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) => {

View File

@@ -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