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
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
343 changed files with 3846 additions and 3508 deletions

View File

@ -4,9 +4,6 @@ import type { FrameworkProvider, Scope, Service } from '../core';
import { ComponentNotFoundError, Framework } from '../core';
import { parseIdentifier } from '../core/identifier';
import type { GeneralIdentifier, IdentifierType, Type } from '../core/types';
import { MountPoint } from './scope-root-components';
export { useMount } from './scope-root-components';
export const FrameworkStackContext = React.createContext<FrameworkProvider[]>([
Framework.EMPTY.provider(),
@ -129,7 +126,7 @@ export const FrameworkScope = ({
return (
<FrameworkStackContext.Provider value={nextStack}>
<MountPoint>{children}</MountPoint>
{children}
</FrameworkStackContext.Provider>
);
};

View File

@ -1,74 +0,0 @@
import React from 'react';
type NodesMap = Map<
number,
{
node: React.ReactNode;
debugKey?: string;
}
>;
const ScopeRootComponentsContext = React.createContext<{
nodes: NodesMap;
setNodes: React.Dispatch<React.SetStateAction<NodesMap>>;
}>({ nodes: new Map(), setNodes: () => {} });
let _id = 0;
/**
* A hook to add nodes to the nearest scope's root
*/
export const useMount = (debugKey?: string) => {
const [id] = React.useState(_id++);
const { setNodes } = React.useContext(ScopeRootComponentsContext);
const unmount = React.useCallback(() => {
setNodes(prev => {
if (!prev.has(id)) {
return prev;
}
const next = new Map(prev);
next.delete(id);
return next;
});
}, [id, setNodes]);
const mount = React.useCallback(
(node: React.ReactNode) => {
setNodes(prev => new Map(prev).set(id, { node, debugKey }));
return unmount;
},
[setNodes, id, debugKey, unmount]
);
return React.useMemo(() => {
return {
/**
* Add a node to the nearest scope root
* ```tsx
* const { mount } = useMount();
* useEffect(() => {
* const unmount = mount(<div>Node</div>);
* return unmount;
* }, [])
* ```
* @return A function to unmount the added node.
*/
mount,
};
}, [mount]);
};
export const MountPoint = ({ children }: React.PropsWithChildren) => {
const [nodes, setNodes] = React.useState<NodesMap>(new Map());
return (
<ScopeRootComponentsContext.Provider value={{ nodes, setNodes }}>
{children}
{Array.from(nodes.entries()).map(([id, { node, debugKey }]) => (
<div data-testid={debugKey} key={id} style={{ display: 'contents' }}>
{node}
</div>
))}
</ScopeRootComponentsContext.Provider>
);
};

View File

@ -1,5 +1,4 @@
import { AffineContext } from '@affine/core/components/context';
import { Telemetry } from '@affine/core/components/telemetry';
import { AppFallback } from '@affine/core/mobile/components';
import { configureMobileModules } from '@affine/core/mobile/modules';
import { router } from '@affine/core/mobile/router';
@ -47,7 +46,6 @@ export function App() {
<FrameworkRoot framework={frameworkProvider}>
<I18nProvider>
<AffineContext store={getCurrentStore()}>
<Telemetry />
<RouterProvider
fallbackElement={<AppFallback />}
router={router}

View File

@ -1,8 +1,6 @@
import { GlobalLoading } from '@affine/component/global-loading';
import { AppFallback } from '@affine/core/components/affine/app-container';
import { AffineContext } from '@affine/core/components/context';
import { WindowsAppControls } from '@affine/core/components/pure/header/windows-app-controls';
import { Telemetry } from '@affine/core/components/telemetry';
import { AppContainer } from '@affine/core/desktop/components/app-container';
import { router } from '@affine/core/desktop/router';
import { configureCommonModules } from '@affine/core/modules';
import { configureAppTabsHeaderModule } from '@affine/core/modules/app-tabs-header';
@ -11,27 +9,35 @@ import {
configureDesktopApiModule,
DesktopApiService,
} from '@affine/core/modules/desktop-api';
import { GlobalDialogService } from '@affine/core/modules/dialogs';
import { EditorSettingService } from '@affine/core/modules/editor-setting';
import { configureFindInPageModule } from '@affine/core/modules/find-in-page';
import { I18nProvider } from '@affine/core/modules/i18n';
import { configureElectronStateStorageImpls } from '@affine/core/modules/storage';
import { CustomThemeModifier } from '@affine/core/modules/theme-editor';
import {
ClientSchemeProvider,
PopupWindowProvider,
} from '@affine/core/modules/url';
import { configureSqliteUserspaceStorageProvider } from '@affine/core/modules/userspace';
import { configureDesktopWorkbenchModule } from '@affine/core/modules/workbench';
import {
configureDesktopWorkbenchModule,
WorkbenchService,
} from '@affine/core/modules/workbench';
import {
configureBrowserWorkspaceFlavours,
configureSqliteWorkspaceEngineStorageProvider,
} from '@affine/core/modules/workspace-engine';
import createEmotionCache from '@affine/core/utils/create-emotion-cache';
import { apis, events } from '@affine/electron-api';
import { CacheProvider } from '@emotion/react';
import {
DocsService,
Framework,
FrameworkRoot,
getCurrentStore,
GlobalContextService,
LifecycleService,
WorkspacesService,
} from '@toeverything/infra';
import { Suspense } from 'react';
import { RouterProvider } from 'react-router-dom';
@ -111,6 +117,49 @@ window.addEventListener('focus', () => {
});
frameworkProvider.get(LifecycleService).applicationStart();
events?.applicationMenu.openAboutPageInSettingModal(() =>
frameworkProvider.get(GlobalDialogService).open('setting', {
activeTab: 'about',
})
);
events?.applicationMenu.onNewPageAction(() => {
const currentWorkspaceId = frameworkProvider
.get(GlobalContextService)
.globalContext.workspaceId.get();
const workspacesService = frameworkProvider.get(WorkspacesService);
const workspaceMetadata = currentWorkspaceId
? workspacesService.list.workspace$(currentWorkspaceId).value
: null;
const workspaceRef =
workspaceMetadata &&
workspacesService.open({ metadata: workspaceMetadata });
if (!workspaceRef) {
return;
}
const { workspace, dispose } = workspaceRef;
const editorSettingService = frameworkProvider.get(EditorSettingService);
const docsService = workspace.scope.get(DocsService);
const editorSetting = editorSettingService.editorSetting;
const docProps = {
note: editorSetting.get('affine:note'),
};
apis?.ui
.isActiveTab()
.then(isActive => {
if (!isActive) {
return;
}
const page = docsService.createDoc({ docProps });
workspace.scope.get(WorkbenchService).workbench.openDoc(page.id);
})
.catch(err => {
console.error(err);
});
dispose();
});
export function App() {
return (
<Suspense>
@ -119,11 +168,8 @@ export function App() {
<I18nProvider>
<AffineContext store={getCurrentStore()}>
<DesktopThemeSync />
<Telemetry />
<CustomThemeModifier />
<GlobalLoading />
<RouterProvider
fallbackElement={<AppFallback />}
fallbackElement={<AppContainer fallback />}
router={router}
future={future}
/>

View File

@ -16,18 +16,17 @@ export const root = style({
},
});
export const body = style({
flex: 1,
paddingTop: 52,
});
export const appTabsHeader = style({
zIndex: 1,
position: 'absolute',
top: 0,
});
export const fallbackRoot = style({
width: '100%',
height: '100%',
paddingTop: 52,
});
export const splitViewFallback = style({
width: '100%',
height: '100%',

View File

@ -1,7 +1,7 @@
import { ShellAppFallback } from '@affine/core/components/affine/app-container';
import { useAppSettingHelper } from '@affine/core/components/hooks/affine/use-app-setting-helper';
import { ThemeProvider } from '@affine/core/components/theme-provider';
import { configureAppSidebarModule } from '@affine/core/modules/app-sidebar';
import { ShellAppSidebarFallback } from '@affine/core/modules/app-sidebar/views';
import {
AppTabsHeader,
configureAppTabsHeaderModule,
@ -10,7 +10,6 @@ import { configureDesktopApiModule } from '@affine/core/modules/desktop-api';
import { configureI18nModule, I18nProvider } from '@affine/core/modules/i18n';
import { configureElectronStateStorageImpls } from '@affine/core/modules/storage';
import { configureAppThemeModule } from '@affine/core/modules/theme';
import { SplitViewFallback } from '@affine/core/modules/workbench/view/split-view/split-view';
import {
configureGlobalStorageModule,
Framework,
@ -41,9 +40,9 @@ export function App() {
<I18nProvider>
<div className={styles.root} data-translucent={translucent}>
<AppTabsHeader mode="shell" className={styles.appTabsHeader} />
<ShellAppFallback className={styles.fallbackRoot}>
<SplitViewFallback className={styles.splitViewFallback} />
</ShellAppFallback>
<div className={styles.body}>
<ShellAppSidebarFallback />
</div>
</div>
</I18nProvider>
</ThemeProvider>

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 { useTheme } from 'next-themes';
import { useRef } from 'react';

View File

@ -9,11 +9,8 @@ import { storeWorkspaceMeta } from '../workspace';
import { getWorkspaceDBPath, getWorkspacesBasePath } from '../workspace/meta';
export type ErrorMessage =
| 'DB_FILE_ALREADY_LOADED'
| 'DB_FILE_PATH_INVALID'
| 'DB_FILE_INVALID'
| 'DB_FILE_MIGRATION_FAILED'
| 'FILE_ALREADY_EXISTS'
| 'UNKNOWN_ERROR';
export interface LoadDBFileResult {

View File

@ -1,10 +0,0 @@
import type { NamespaceHandlers } from '../type';
import { savePDFFileAs } from './pdf';
export const exportHandlers = {
savePDFFileAs: async (_, title: string) => {
return savePDFFileAs(title);
},
} satisfies NamespaceHandlers;
export * from './pdf';

View File

@ -1,90 +0,0 @@
import { BrowserWindow, dialog } from 'electron';
import fs from 'fs-extra';
import { logger } from '../logger';
import type { ErrorMessage } from './utils';
import { getFakedResult } from './utils';
export interface SavePDFFileResult {
filePath?: string;
canceled?: boolean;
error?: ErrorMessage;
}
/**
* This function is called when the user clicks the "Export to PDF" button in the electron.
*
* It will just copy the file to the given path
*/
export async function savePDFFileAs(
pageTitle: string
): Promise<SavePDFFileResult> {
try {
const ret =
getFakedResult() ??
(await dialog.showSaveDialog({
properties: ['showOverwriteConfirmation'],
title: 'Save PDF',
showsTagField: false,
buttonLabel: 'Save',
defaultPath: `${pageTitle}.pdf`,
message: 'Save Page as a PDF file',
filters: [{ name: 'PDF Files', extensions: ['pdf'] }],
}));
const filePath = ret.filePath;
if (ret.canceled || !filePath) {
return {
canceled: true,
};
}
await BrowserWindow.getFocusedWindow()
?.webContents.printToPDF({
pageSize: 'A4',
margins: {
bottom: 0.5,
},
printBackground: true,
landscape: false,
displayHeaderFooter: true,
headerTemplate: '<div></div>',
footerTemplate: getFootTemple(),
})
.then(data => {
fs.writeFile(filePath, data, error => {
if (error) throw error;
logger.log(`Wrote PDF successfully to ${filePath}`);
});
});
return { filePath };
} catch (err) {
logger.error('savePDFFileAs', err);
return {
error: 'UNKNOWN_ERROR',
};
}
}
function getFootTemple(): string {
const logo = `
<svg xmlns="http://www.w3.org/2000/svg" width="53" height="12" viewBox="0 0 53 12" fill="none">
<path d="M18.9256 0.709372C18.8749 0.504937 18.6912 0.361572 18.4807 0.361572H17.77C17.5595 0.361572 17.3758 0.504937 17.3252 0.709372L14.9153 10.4283C14.8438 10.7172 15.0621 10.9965 15.3601 10.9965H15.6052C15.8183 10.9965 16.0033 10.8497 16.0513 10.6423L16.5646 8.43721C16.6127 8.22974 16.7976 8.08291 17.0107 8.08291H19.2396C19.4527 8.08291 19.6376 8.22974 19.6857 8.43721L20.199 10.6423C20.247 10.8497 20.432 10.9965 20.6451 10.9965H20.8902C21.1878 10.9965 21.4065 10.7172 21.335 10.4283L18.9251 0.709372H18.9256ZM18.7891 7.0629H17.4616C17.1666 7.0629 16.9483 6.7883 17.0155 6.50113L17.9025 2.23181C17.9575 1.99576 18.2936 1.99576 18.3486 2.23181L19.2357 6.50113C19.3024 6.7883 19.0845 7.0629 18.7896 7.0629H18.7891Z" fill="black" fill-opacity="0.1"/>
<path d="M36.2654 5.00861H30.766C30.5131 5.00861 30.3078 4.8033 30.3078 4.55036V2.25132C30.3078 1.77055 30.6976 1.38074 31.1783 1.38074H33.8997C34.1526 1.38074 34.3579 1.17544 34.3579 0.922494V0.818977C34.3579 0.566031 34.1526 0.36073 33.8997 0.36073H30.8539C29.8924 0.36073 29.1132 1.14036 29.1132 2.10146V5.00774H24.2171C23.9642 5.00774 23.7589 4.80244 23.7589 4.54949V2.25046C23.7589 1.76969 24.1487 1.37988 24.6295 1.37988H27.3508C27.6038 1.37988 27.8091 1.17457 27.8091 0.921628V0.818111C27.8091 0.565165 27.6038 0.359863 27.3508 0.359863H24.3051C23.3435 0.359863 22.5643 1.13949 22.5643 2.1006V10.5366C22.5643 10.7895 22.7696 10.9948 23.0226 10.9948H23.3011C23.554 10.9948 23.7593 10.7895 23.7593 10.5366V6.48513C23.7593 6.23219 23.9646 6.02689 24.2176 6.02689H29.1136V10.5366C29.1136 10.7895 29.3189 10.9948 29.5719 10.9948H29.8504C30.1033 10.9948 30.3086 10.7895 30.3086 10.5366V6.48513C30.3086 6.23219 30.5139 6.02689 30.7669 6.02689H35.9713C36.4521 6.02689 36.8419 6.4167 36.8419 6.89747V10.5418C36.8419 10.7947 37.0472 11 37.3001 11H37.5492C37.8021 11 38.0074 10.7947 38.0074 10.5418V6.74804C38.0074 5.7865 37.2278 5.00731 36.2667 5.00731L36.2654 5.00861Z" fill="black" fill-opacity="0.1"/>
<path d="M45.2871 0.361517H45.0363C44.7838 0.361517 44.5789 0.565953 44.5781 0.818032L44.5504 9.53946L42.0512 0.695024C41.9954 0.497519 41.8156 0.361517 41.6103 0.361517H40.521C40.268 0.361517 40.0627 0.566819 40.0627 0.819765V10.5387C40.0627 10.7916 40.268 10.9969 40.521 10.9969H40.7718C41.0243 10.9969 41.2292 10.7925 41.23 10.5404L41.2577 1.81899L43.7569 10.6634C43.8128 10.8609 43.9925 10.9969 44.1978 10.9969H45.2871C45.5401 10.9969 45.7454 10.7916 45.7454 10.5387V0.819331C45.7454 0.566386 45.5401 0.361084 45.2871 0.361084V0.361517Z" fill="black" fill-opacity="0.1"/>
<path d="M49.2307 1.3811H51.8212C52.0741 1.3811 52.2794 1.17579 52.2794 0.922849V0.819331C52.2794 0.566386 52.0741 0.361084 51.8212 0.361084H48.9214C47.9599 0.361084 47.1807 1.14071 47.1807 2.10182V9.25489C47.1807 10.2164 47.9603 10.9956 48.9214 10.9956H51.8212C52.0741 10.9956 52.2794 10.7903 52.2794 10.5374V10.4339C52.2794 10.1809 52.0741 9.97562 51.8212 9.97562H49.2307C48.7499 9.97562 48.3601 9.5858 48.3601 9.10503V6.33996C48.3601 6.08701 48.5654 5.88171 48.8183 5.88171H51.6752C51.9282 5.88171 52.1335 5.67641 52.1335 5.42346V5.31994C52.1335 5.067 51.9282 4.8617 51.6752 4.8617H48.8183C48.5654 4.8617 48.3601 4.65639 48.3601 4.40345V2.24995C48.3601 1.76918 48.7499 1.37936 49.2307 1.37936V1.3811Z" fill="black" fill-opacity="0.1"/>
<path d="M37.3088 1.65787C37.1052 1.4543 36.7583 1.54742 36.6838 1.82549L36.3473 3.08199C36.2728 3.35962 36.527 3.61387 36.8051 3.5398L38.0616 3.20326C38.3396 3.12876 38.4323 2.7814 38.2292 2.57826L37.3097 1.65873L37.3088 1.65787Z" fill="black" fill-opacity="0.1"/>
<path d="M11.9043 9.92891C11.8624 9.85125 11.3139 8.91339 11.2674 8.82602C9.92288 6.49855 7.98407 3.13718 6.64195 0.814557C6.37775 0.29657 5.6448 0.286862 5.36882 0.795141C3.92685 3.2932 1.71414 7.12436 0.274242 9.6193C0.214607 9.72505 0.118915 9.88037 0.0617076 9.99687C-0.035025 10.2063 -0.0163025 10.4677 0.10574 10.6608C0.238877 10.8858 0.502032 11.0165 0.759985 11.0027H0.8269C2.06986 11.0009 9.86983 11.0048 11.2844 11.0027C11.8329 11.0041 12.1779 10.4022 11.904 9.92926L11.9043 9.92891ZM6.09068 1.66053C6.91793 3.09661 7.8967 4.79099 8.85535 6.45036C8.47016 6.21355 8.05792 6.02875 7.6169 5.91711C7.49763 5.89007 7.04136 5.83529 6.94289 5.83113C6.9207 5.82939 6.89851 5.82835 6.87598 5.82835H5.12474C4.97288 5.82835 4.82622 5.86753 4.69655 5.93757C4.53325 5.14845 4.68199 4.26468 4.8914 3.51683C4.90978 3.45269 4.92954 3.38889 4.9493 3.3251C5.29636 2.7239 5.62366 2.15737 5.91073 1.65984C5.95095 1.5905 6.0508 1.59084 6.09068 1.65984V1.66053ZM6.15412 8.25707C6.15412 8.25707 6.12327 8.30492 6.09692 8.34583C6.07161 8.37079 6.03728 8.38535 5.99984 8.38535C5.94956 8.38535 5.90484 8.35935 5.87988 8.31601L5.03841 6.85809C5.03841 6.85809 5.0124 6.80747 4.98987 6.76413C4.98085 6.72946 4.98571 6.69271 5.00408 6.66046C5.02905 6.61712 5.07412 6.59112 5.12404 6.59112H6.80733C6.80733 6.59112 6.86419 6.59389 6.91308 6.59632C6.9474 6.60568 6.97722 6.62822 6.99559 6.66046C7.02056 6.7038 7.02056 6.75581 6.99559 6.79915L6.15378 8.25707H6.15412ZM1.12681 9.94521C1.30502 9.63733 1.53766 9.23653 1.5914 9.14084C2.1971 8.09169 3.04585 6.62163 3.89252 5.15539C3.88004 5.60715 3.92616 6.05684 4.04993 6.49473C4.08599 6.61158 4.26697 7.03422 4.31274 7.12159C4.32591 7.14967 4.34256 7.17914 4.35851 7.20619L5.21939 8.69739C5.29532 8.8288 5.40245 8.93628 5.52796 9.01359C4.92607 9.54961 4.08634 9.86269 3.33397 10.0551C3.27191 10.0707 3.20916 10.0849 3.14675 10.0988C2.34827 10.0988 1.66837 10.0995 1.21695 10.1009C1.13651 10.1009 1.08659 10.0142 1.12681 9.94486V9.94521ZM10.7834 10.1016C9.65661 10.1026 7.37212 10.1005 5.25475 10.0995C5.65139 9.88453 6.01683 9.62034 6.33337 9.29478C6.41624 9.20498 6.69222 8.83712 6.74492 8.75391C6.7626 8.72825 6.77994 8.69913 6.79519 8.67208L7.65608 7.18088C7.73201 7.04947 7.77153 6.90281 7.77569 6.75546C8.54089 7.00856 9.23188 7.57925 9.77449 8.13468C9.82129 8.18322 9.86706 8.23245 9.91248 8.28203C10.2464 8.86035 10.5695 9.41994 10.8729 9.9459C10.9127 10.0152 10.8632 10.1019 10.7831 10.1019L10.7834 10.1016Z" fill="black" fill-opacity="0.1"/>
</svg>
`;
const footerTemp = `
<div style="font-size: 14px; width: 100%; display: flex; justify-content: flex-end; margin-right: 40px;">
<a href="https://affine.pro" style="display: flex; text-decoration: none; color: rgba(0, 0, 0, 0.1);">
<span>Created with</span>
<div style="display: flex; align-items: center;">${logo}</div>
</a>
</div>
`;
return footerTemp;
}

View File

@ -1,24 +0,0 @@
// provide a backdoor to set dialog path for testing in playwright
interface FakeDialogResult {
canceled?: boolean;
filePath?: string;
filePaths?: string[];
}
// result will be used in the next call to showOpenDialog
// if it is being read once, it will be reset to undefined
let fakeDialogResult: FakeDialogResult | undefined = undefined;
export function getFakedResult() {
const result = fakeDialogResult;
fakeDialogResult = undefined;
return result;
}
export function setFakeDialogResult(result: FakeDialogResult | undefined) {
fakeDialogResult = result;
// for convenience, we will fill filePaths with filePath if it is not set
if (result?.filePaths === undefined && result?.filePath !== undefined) {
result.filePaths = [result.filePath];
}
}
const ErrorMessages = ['FILE_ALREADY_EXISTS', 'UNKNOWN_ERROR'] as const;
export type ErrorMessage = (typeof ErrorMessages)[number];

View File

@ -3,7 +3,6 @@ import { ipcMain } from 'electron';
import { AFFINE_API_CHANNEL_NAME } from '../shared/type';
import { clipboardHandlers } from './clipboard';
import { configStorageHandlers } from './config-storage';
import { exportHandlers } from './export';
import { findInPageHandlers } from './find-in-page';
import { getLogFilePath, logger, revealLogFile } from './logger';
import { sharedStorageHandlers } from './shared-storage';
@ -24,7 +23,6 @@ export const allHandlers = {
debug: debugHandlers,
ui: uiHandlers,
clipboard: clipboardHandlers,
export: exportHandlers,
updater: updaterHandlers,
configStorage: configStorageHandlers,
findInPage: findInPageHandlers,

View File

@ -1,5 +1,4 @@
import { AffineContext } from '@affine/core/components/context';
import { Telemetry } from '@affine/core/components/telemetry';
import { AppFallback } from '@affine/core/mobile/components';
import { configureMobileModules } from '@affine/core/mobile/modules';
import { router } from '@affine/core/mobile/router';
@ -123,7 +122,6 @@ export function App() {
<FrameworkRoot framework={frameworkProvider}>
<I18nProvider>
<AffineContext store={getCurrentStore()}>
<Telemetry />
<RouterProvider
fallbackElement={<AppFallback />}
router={router}

View File

@ -1,5 +1,4 @@
import { AffineContext } from '@affine/core/components/context';
import { Telemetry } from '@affine/core/components/telemetry';
import { AppFallback } from '@affine/core/mobile/components';
import { configureMobileModules } from '@affine/core/mobile/modules';
import { router } from '@affine/core/mobile/router';
@ -67,7 +66,6 @@ export function App() {
<FrameworkRoot framework={frameworkProvider}>
<I18nProvider>
<AffineContext store={getCurrentStore()}>
<Telemetry />
<RouterProvider
fallbackElement={<AppFallback />}
router={router}

View File

@ -1,16 +1,10 @@
import { GlobalLoading } from '@affine/component/global-loading';
import { AppFallback } from '@affine/core/components/affine/app-container';
import { AffineContext } from '@affine/core/components/context';
import { Telemetry } from '@affine/core/components/telemetry';
import { AppContainer } from '@affine/core/desktop/components/app-container';
import { router } from '@affine/core/desktop/router';
import { configureCommonModules } from '@affine/core/modules';
import { I18nProvider } from '@affine/core/modules/i18n';
import {
configureOpenInApp,
OpenInAppGuard,
} from '@affine/core/modules/open-in-app';
import { OpenInAppGuard } from '@affine/core/modules/open-in-app';
import { configureLocalStorageStateStorageImpls } from '@affine/core/modules/storage';
import { CustomThemeModifier } from '@affine/core/modules/theme-editor';
import { PopupWindowProvider } from '@affine/core/modules/url';
import { configureIndexedDBUserspaceStorageProvider } from '@affine/core/modules/userspace';
import { configureBrowserWorkbenchModule } from '@affine/core/modules/workbench';
@ -42,7 +36,6 @@ configureLocalStorageStateStorageImpls(framework);
configureBrowserWorkspaceFlavours(framework);
configureIndexedDBWorkspaceEngineStorageProvider(framework);
configureIndexedDBUserspaceStorageProvider(framework);
configureOpenInApp(framework);
framework.impl(PopupWindowProvider, {
open: (target: string) => {
const targetUrl = new URL(target);
@ -77,12 +70,9 @@ export function App() {
<CacheProvider value={cache}>
<I18nProvider>
<AffineContext store={getCurrentStore()}>
<Telemetry />
<CustomThemeModifier />
<GlobalLoading />
<OpenInAppGuard>
<RouterProvider
fallbackElement={<AppFallback key="RouterFallback" />}
fallbackElement={<AppContainer fallback />}
router={router}
future={future}
/>

View File

@ -1,35 +1,5 @@
import { useAtomValue } from 'jotai';
import type { ReactNode } from 'react';
import { useEffect, useState } from 'react';
import { Loading } from '../../ui/loading';
import * as styles from './index.css';
import { globalLoadingEventsAtom } from './index.jotai';
export {
type GlobalLoadingEvent,
pushGlobalLoadingEventAtom,
resolveGlobalLoadingEventAtom,
} from './index.jotai';
export function GlobalLoading(): ReactNode {
const globalLoadingEvents = useAtomValue(globalLoadingEventsAtom);
const [loading, setLoading] = useState(false);
useEffect(() => {
if (globalLoadingEvents.length) {
setLoading(true);
} else {
setLoading(false);
}
}, [globalLoadingEvents]);
if (!globalLoadingEvents.length) {
return null;
}
return (
<div className={styles.globalLoadingWrapperStyle} data-loading={loading}>
<Loading size={20} />
</div>
);
}

View File

@ -1,3 +1,4 @@
export * from './confirm-modal';
export * from './modal';
export * from './overlay-modal';
export * from './prompt-modal';

View File

@ -229,6 +229,7 @@ export const ModalInner = forwardRef<HTMLDivElement, ModalProps>(
styles.modalContentWrapper,
contentWrapperClassName
)}
data-mobile={BUILD_CONFIG.isMobileEdition ? '' : undefined}
style={contentWrapperStyle}
>
<Dialog.Content

View File

@ -0,0 +1,109 @@
import { cssVar } from '@toeverything/theme';
import { style } from '@vanilla-extract/css';
// desktop
export const desktopStyles = {
container: style({
display: 'flex',
flexDirection: 'column',
}),
description: style({}),
header: style({}),
content: style({
height: '100%',
overflowY: 'auto',
padding: '12px 4px 20px 4px',
}),
label: style({
color: cssVar('textSecondaryColor'),
fontSize: 14,
lineHeight: '22px',
padding: '8px 0',
}),
input: style({}),
inputContainer: style({}),
footer: style({
display: 'flex',
justifyContent: 'flex-end',
alignItems: 'center',
paddingTop: '40px',
marginTop: 'auto',
gap: '20px',
selectors: {
'&.modalFooterWithChildren': {
paddingTop: '20px',
},
'&.reverse': {
flexDirection: 'row-reverse',
justifyContent: 'flex-start',
},
},
}),
action: style({}),
};
// mobile
export const mobileStyles = {
container: style({
display: 'flex',
flexDirection: 'column',
padding: '12px 0 !important',
borderRadius: 22,
}),
description: style({
padding: '11px 22px',
fontSize: 17,
fontWeight: 400,
letterSpacing: -0.43,
lineHeight: '22px',
}),
label: style({
color: cssVar('textSecondaryColor'),
fontSize: 14,
lineHeight: '22px',
padding: '8px 16px',
}),
header: style({
padding: '10px 16px',
marginBottom: '0px !important',
fontSize: 17,
fontWeight: 600,
letterSpacing: -0.43,
lineHeight: '22px',
}),
inputContainer: style({
padding: '0 16px',
}),
input: style({
height: 44,
fontSize: 17,
lineHeight: '22px',
}),
content: style({
padding: '11px 22px',
fontSize: 17,
fontWeight: 400,
letterSpacing: -0.43,
lineHeight: '22px',
}),
footer: style({
padding: '8px 16px',
display: 'flex',
flexDirection: 'column',
gap: 16,
selectors: {
'&.reverse': {
flexDirection: 'column-reverse',
},
},
}),
action: style({
width: '100%',
height: 44,
borderRadius: 8,
fontSize: 17,
fontWeight: 400,
letterSpacing: -0.43,
lineHeight: '22px',
}),
};

View File

@ -0,0 +1,235 @@
import { DialogTrigger } from '@radix-ui/react-dialog';
import clsx from 'clsx';
import type { PropsWithChildren } from 'react';
import { createContext, useCallback, useContext, useState } from 'react';
import type { ButtonProps } from '../button';
import { Button } from '../button';
import Input, { type InputProps } from '../input';
import type { ModalProps } from './modal';
import { Modal } from './modal';
import { desktopStyles, mobileStyles } from './prompt-modal.css';
const styles = BUILD_CONFIG.isMobileEdition ? mobileStyles : desktopStyles;
export interface PromptModalProps extends ModalProps {
confirmButtonOptions?: Omit<ButtonProps, 'children'>;
onConfirm?: ((text: string) => void) | ((text: string) => Promise<void>);
onCancel?: () => void;
confirmText?: React.ReactNode;
cancelText?: React.ReactNode;
label?: React.ReactNode;
defaultValue?: string;
required?: boolean;
cancelButtonOptions?: Omit<ButtonProps, 'children'>;
inputOptions?: Omit<InputProps, 'value' | 'onChange'>;
reverseFooter?: boolean;
/**
* Auto focus on confirm button when modal opened
* @default true
*/
autoFocusConfirm?: boolean;
}
export const PromptModal = ({
children,
confirmButtonOptions,
// FIXME: we need i18n
confirmText,
cancelText = 'Cancel',
cancelButtonOptions,
reverseFooter,
onConfirm,
onCancel,
label,
required = true,
inputOptions,
defaultValue,
width = 480,
autoFocusConfirm = true,
headerClassName,
descriptionClassName,
...props
}: PromptModalProps) => {
const [value, setValue] = useState(defaultValue ?? '');
const onConfirmClick = useCallback(() => {
Promise.resolve(onConfirm?.(value))
.catch(err => {
console.error(err);
})
.finally(() => {
setValue('');
});
}, [onConfirm, value]);
const onKeyDown = useCallback(
(e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Escape') {
if (value) {
e.preventDefault();
return;
} else {
e.currentTarget.blur();
}
}
},
[value]
);
return (
<Modal
contentOptions={{
className: styles.container,
onPointerDownOutside: e => {
e.stopPropagation();
onCancel?.();
},
}}
width={width}
closeButtonOptions={{
onClick: onCancel,
}}
headerClassName={clsx(styles.header, headerClassName)}
descriptionClassName={clsx(styles.description, descriptionClassName)}
{...props}
>
<div className={styles.label}>{label}</div>
<div className={styles.inputContainer}>
<Input
value={value}
onChange={setValue}
autoFocus
className={styles.input}
onKeyDown={onKeyDown}
data-testid="prompt-modal-input"
{...inputOptions}
/>
</div>
{children ? <div className={styles.content}>{children}</div> : null}
<div
className={clsx(styles.footer, {
modalFooterWithChildren: !!children,
reverse: reverseFooter,
})}
>
<DialogTrigger asChild>
<Button
className={styles.action}
onClick={onCancel}
data-testid="prompt-modal-cancel"
{...cancelButtonOptions}
>
{cancelText}
</Button>
</DialogTrigger>
<Button
className={styles.action}
onClick={onConfirmClick}
disabled={required && !value}
data-testid="prompt-modal-confirm"
autoFocus={autoFocusConfirm}
{...confirmButtonOptions}
>
{confirmText}
</Button>
</div>
</Modal>
);
};
interface OpenPromptModalOptions {
autoClose?: boolean;
onSuccess?: () => void;
}
interface PromptModalContextProps {
modalProps: PromptModalProps;
openPromptModal: (
props?: PromptModalProps,
options?: OpenPromptModalOptions
) => void;
closePromptModal: () => void;
}
const PromptModalContext = createContext<PromptModalContextProps>({
modalProps: { open: false },
openPromptModal: () => {},
closePromptModal: () => {},
});
export const PromptModalProvider = ({ children }: PropsWithChildren) => {
const [modalProps, setModalProps] = useState<PromptModalProps>({
open: false,
});
const setLoading = useCallback((value: boolean) => {
setModalProps(prev => ({
...prev,
confirmButtonOptions: {
...prev.confirmButtonOptions,
loading: value,
},
}));
}, []);
const closePromptModal = useCallback(() => {
setModalProps({ open: false });
}, []);
const openPromptModal = useCallback(
(props?: PromptModalProps, options?: OpenPromptModalOptions) => {
const { autoClose = true, onSuccess } = options ?? {};
if (!props) {
setModalProps({ open: true });
return;
}
const { onConfirm: _onConfirm, ...otherProps } = props;
const onConfirm = (text: string) => {
setLoading(true);
return Promise.resolve(_onConfirm?.(text))
.then(() => onSuccess?.())
.catch(console.error)
.finally(() => setLoading(false))
.finally(() => autoClose && closePromptModal());
};
setModalProps({ ...otherProps, onConfirm, open: true });
},
[closePromptModal, setLoading]
);
const onOpenChange = useCallback(
(open: boolean) => {
modalProps.onOpenChange?.(open);
setModalProps(props => ({ ...props, open }));
},
[modalProps]
);
return (
<PromptModalContext.Provider
value={{
openPromptModal: openPromptModal,
closePromptModal: closePromptModal,
modalProps,
}}
>
{children}
{/* TODO(@catsjuice): multi-instance support(unnecessary for now) */}
<PromptModal {...modalProps} onOpenChange={onOpenChange} />
</PromptModalContext.Provider>
);
};
export const usePromptModal = () => {
const context = useContext(PromptModalContext);
if (!context) {
throw new Error(
'useConfirmModal must be used within a ConfirmModalProvider'
);
}
return {
openPromptModal: context.openPromptModal,
closePromptModal: context.closePromptModal,
};
};

View File

@ -79,15 +79,12 @@ export const modalContentWrapper = style({
alignItems: 'center',
justifyContent: 'center',
zIndex: cssVar('zIndexModal'),
'@media': {
'screen and (width <= 640px)': {
// todo: adjust animation
selectors: {
'&[data-mobile]': {
alignItems: 'flex-end',
paddingBottom: 'env(safe-area-inset-bottom, 20px)',
},
},
selectors: {
'&[data-full-screen="true"]': {
padding: '0 !important',
},

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

Some files were not shown because too many files have changed in this diff Show More