import { Button, Input, Modal, ModalCloseButton, ModalWrapper, toast, Tooltip, } from '@affine/component'; import { DebugLogger } from '@affine/debug'; import { config } from '@affine/env'; import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { HelpIcon } from '@blocksuite/icons'; import { useSetAtom } from 'jotai'; import type { KeyboardEvent } from 'react'; import { useEffect } from 'react'; import { useLayoutEffect } from 'react'; import { useCallback, useRef, useState } from 'react'; import { openDisableCloudAlertModalAtom } from '../../../atoms'; import { useAppHelper } from '../../../hooks/use-workspaces'; import * as style from './index.css'; type CreateWorkspaceStep = | 'set-db-location' | 'name-workspace' | 'set-syncing-mode'; export type CreateWorkspaceMode = 'add' | 'new' | false; const logger = new DebugLogger('CreateWorkspaceModal'); interface ModalProps { mode: CreateWorkspaceMode; // false means not open onClose: () => void; onCreate: (id: string) => void; } interface NameWorkspaceContentProps { onClose: () => void; onConfirmName: (name: string) => void; } const NameWorkspaceContent = ({ onConfirmName, onClose, }: NameWorkspaceContentProps) => { const [workspaceName, setWorkspaceName] = useState(''); const isComposition = useRef(false); const handleCreateWorkspace = useCallback(() => { onConfirmName(workspaceName); }, [onConfirmName, workspaceName]); const handleKeyDown = useCallback( (event: KeyboardEvent) => { if (event.key === 'Enter' && workspaceName && !isComposition.current) { handleCreateWorkspace(); } }, [handleCreateWorkspace, workspaceName] ); const t = useAFFiNEI18N(); return (
{t['Name Your Workspace']()}

{t['Workspace description']()}

{ if (ref) { setTimeout(() => ref.focus(), 0); } }} data-testid="create-workspace-input" onKeyDown={handleKeyDown} placeholder={t['Set a Workspace name']()} maxLength={64} minLength={0} onChange={setWorkspaceName} onCompositionStart={() => { isComposition.current = true; }} onCompositionEnd={() => { isComposition.current = false; }} />
); }; interface SetDBLocationContentProps { onConfirmLocation: (dir?: string) => void; } const useDefaultDBLocation = () => { const [defaultDBLocation, setDefaultDBLocation] = useState(''); useEffect(() => { window.apis?.db .getDefaultStorageLocation() .then(dir => { setDefaultDBLocation(dir); }) .catch(err => { console.error(err); }); }, []); return defaultDBLocation; }; const SetDBLocationContent = ({ onConfirmLocation, }: SetDBLocationContentProps) => { const t = useAFFiNEI18N(); const defaultDBLocation = useDefaultDBLocation(); const [opening, setOpening] = useState(false); const handleSelectDBFileLocation = useCallback(() => { if (opening) { return; } setOpening(true); (async function () { const result = await window.apis?.dialog.selectDBFileLocation(); setOpening(false); if (result?.filePath) { onConfirmLocation(result.filePath); } else if (result?.error) { // @ts-expect-error: result.error is dynamic so the type is unknown toast(t[result.error]()); } })().catch(err => { logger.error(err); }); }, [onConfirmLocation, opening, t]); return (
{t['Set database location']()}

{t['Workspace database storage description']()}

); }; interface SetSyncingModeContentProps { mode: CreateWorkspaceMode; onConfirmMode: (enableCloudSyncing: boolean) => void; } const SetSyncingModeContent = ({ mode, onConfirmMode, }: SetSyncingModeContentProps) => { const t = useAFFiNEI18N(); const [enableCloudSyncing, setEnableCloudSyncing] = useState(false); return (
{t[mode === 'new' ? 'Created Successfully' : 'Added Successfully']()}
); }; export const CreateWorkspaceModal = ({ mode, onClose, onCreate, }: ModalProps) => { const { createLocalWorkspace, addLocalWorkspace } = useAppHelper(); const [step, setStep] = useState(); const [addedId, setAddedId] = useState(); const [workspaceName, setWorkspaceName] = useState(); const [dbFileLocation, setDBFileLocation] = useState(); const setOpenDisableCloudAlertModal = useSetAtom( openDisableCloudAlertModalAtom ); const t = useAFFiNEI18N(); // todo: 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 (!window.apis) { return; } logger.info('load db file'); setStep(undefined); const result = await window.apis.dialog.loadDBFile(); if (result.workspaceId && !canceled) { setAddedId(result.workspaceId); setStep('set-syncing-mode'); } else if (result.error || result.canceled) { if (result.error) { // @ts-expect-error: result.error is dynamic so the type is unknown toast(t[result.error]()); } onClose(); } })().catch(err => { console.error(err); }); } else if (mode === 'new') { setStep( environment.isDesktop && config.enableSQLiteProvider ? 'set-db-location' : 'name-workspace' ); } else { setStep(undefined); } return () => { canceled = true; }; }, [mode, onClose, t]); const onConfirmEnableCloudSyncing = useCallback( (enableCloudSyncing: boolean) => { (async function () { if (!config.enableLegacyCloud && enableCloudSyncing) { setOpenDisableCloudAlertModal(true); } else { let id = addedId; // syncing mode is also the last step if (addedId && mode === 'add') { await addLocalWorkspace(addedId); } else if (mode === 'new' && workspaceName) { id = await createLocalWorkspace(workspaceName); // if dbFileLocation is set, move db file to that location if (dbFileLocation) { await window.apis?.dialog.moveDBFile(id, dbFileLocation); } } else { logger.error('invalid state'); return; } if (id) { onCreate(id); } } })().catch(e => { logger.error(e); }); }, [ addLocalWorkspace, addedId, createLocalWorkspace, dbFileLocation, mode, onCreate, setOpenDisableCloudAlertModal, workspaceName, ] ); const onConfirmName = useCallback( (name: string) => { setWorkspaceName(name); if (environment.isDesktop && config.enableSQLiteProvider) { setStep('set-syncing-mode'); } else { // this will be the last step for web for now // fix me later createLocalWorkspace(name) .then(id => { onCreate(id); }) .catch(err => { logger.error(err); }); } }, [createLocalWorkspace, onCreate] ); const nameWorkspaceNode = step === 'name-workspace' ? ( ) : null; const setDBLocationNode = step === 'set-db-location' ? ( { setDBFileLocation(dir); setStep('name-workspace'); }} /> ) : null; const setSyncingModeNode = step === 'set-syncing-mode' ? ( ) : null; return (
{nameWorkspaceNode} {setDBLocationNode} {setSyncingModeNode}
); };