From 06fa0cdb60f026eae99a660836d007b04c21c61a Mon Sep 17 00:00:00 2001 From: Peng Xiao Date: Thu, 11 May 2023 13:36:22 +0800 Subject: [PATCH] fix: should not show open folder if it is not moved (#2299) --- apps/electron/layers/main/src/events/db.ts | 18 +++- .../src/handlers/__tests__/handlers.spec.ts | 7 +- .../layers/main/src/handlers/db/index.ts | 9 ++ .../layers/main/src/handlers/db/sqlite.ts | 13 +++ .../layers/main/src/handlers/dialog/dialog.ts | 14 ++- .../affine/create-workspace-modal/index.tsx | 37 +++++--- .../workspace-setting-detail/index.css.ts | 14 ++- .../panel/export/index.tsx | 9 +- .../panel/general/index.tsx | 90 ++++++++++++++----- packages/i18n/src/resources/en.json | 1 + .../__tests__/sqlite-provider.spec.ts | 7 +- packages/workspace/src/providers/index.ts | 2 +- 12 files changed, 170 insertions(+), 51 deletions(-) diff --git a/apps/electron/layers/main/src/events/db.ts b/apps/electron/layers/main/src/events/db.ts index 7d2db05b1c..4830602c79 100644 --- a/apps/electron/layers/main/src/events/db.ts +++ b/apps/electron/layers/main/src/events/db.ts @@ -2,25 +2,37 @@ import { Subject } from 'rxjs'; import type { MainEventListener } from './type'; +interface DBFilePathMeta { + workspaceId: string; + path: string; + realPath: string; +} + export const dbSubjects = { // emit workspace ids dbFileMissing: new Subject(), // emit workspace ids dbFileUpdate: new Subject(), + dbFilePathChange: new Subject(), }; export const dbEvents = { - onDbFileMissing: (fn: (workspaceId: string) => void) => { + onDBFileMissing: (fn: (workspaceId: string) => void) => { const sub = dbSubjects.dbFileMissing.subscribe(fn); - return () => { sub.unsubscribe(); }; }, - onDbFileUpdate: (fn: (workspaceId: string) => void) => { + onDBFileUpdate: (fn: (workspaceId: string) => void) => { const sub = dbSubjects.dbFileUpdate.subscribe(fn); return () => { sub.unsubscribe(); }; }, + onDBFilePathChange: (fn: (meta: DBFilePathMeta) => void) => { + const sub = dbSubjects.dbFilePathChange.subscribe(fn); + return () => { + sub.unsubscribe(); + }; + }, } satisfies Record; diff --git a/apps/electron/layers/main/src/handlers/__tests__/handlers.spec.ts b/apps/electron/layers/main/src/handlers/__tests__/handlers.spec.ts index cd761b6032..2fd380f30f 100644 --- a/apps/electron/layers/main/src/handlers/__tests__/handlers.spec.ts +++ b/apps/electron/layers/main/src/handlers/__tests__/handlers.spec.ts @@ -160,7 +160,7 @@ describe('ensureSQLiteDB', () => { // wait for 1000ms for file watcher to detect file removal await delay(2000); - expect(sendStub).toBeCalledWith('db:onDbFileMissing', id); + expect(sendStub).toBeCalledWith('db:onDBFileMissing', id); // ensureSQLiteDB should recreate the db file workspaceDB = await ensureSQLiteDB(id); @@ -190,10 +190,7 @@ describe('ensureSQLiteDB', () => { // wait for 200ms for file watcher to detect file change await delay(2000); - expect(sendStub).toBeCalledWith('db:onDbFileUpdate', id); - - // should only call once for multiple writes - expect(sendStub).toBeCalledTimes(1); + expect(sendStub).toBeCalledWith('db:onDBFileUpdate', id); }); }); diff --git a/apps/electron/layers/main/src/handlers/db/index.ts b/apps/electron/layers/main/src/handlers/db/index.ts index 79f67611d3..5e951cd1af 100644 --- a/apps/electron/layers/main/src/handlers/db/index.ts +++ b/apps/electron/layers/main/src/handlers/db/index.ts @@ -1,3 +1,5 @@ +import fs from 'fs-extra'; + import { appContext } from '../../context'; import type { NamespaceHandlers } from '../type'; import { ensureSQLiteDB } from './ensure-db'; @@ -30,4 +32,11 @@ export const dbHandlers = { getDefaultStorageLocation: async () => { return appContext.appDataPath; }, + getDBFilePath: async (_, workspaceId: string) => { + const workspaceDB = await ensureSQLiteDB(workspaceId); + return { + path: workspaceDB.path, + realPath: await fs.realpath(workspaceDB.path), + }; + }, } satisfies NamespaceHandlers; diff --git a/apps/electron/layers/main/src/handlers/db/sqlite.ts b/apps/electron/layers/main/src/handlers/db/sqlite.ts index d58480a544..b35d71d4b1 100644 --- a/apps/electron/layers/main/src/handlers/db/sqlite.ts +++ b/apps/electron/layers/main/src/handlers/db/sqlite.ts @@ -6,6 +6,7 @@ import fs from 'fs-extra'; import * as Y from 'yjs'; import type { AppContext } from '../../context'; +import { dbSubjects } from '../../events/db'; import { logger } from '../../logger'; import { ts } from '../../utils'; @@ -62,6 +63,18 @@ export class WorkspaceSQLiteDB { this.db.close(); } + fs.realpath(this.path) + .then(realPath => { + dbSubjects.dbFilePathChange.next({ + workspaceId: this.workspaceId, + path: this.path, + realPath, + }); + }) + .catch(() => { + // skip error + }); + // use cached version? const db = (this.db = sqlite(this.path)); db.exec(schemas.join(';')); diff --git a/apps/electron/layers/main/src/handlers/dialog/dialog.ts b/apps/electron/layers/main/src/handlers/dialog/dialog.ts index 360d92bf9e..ec6ce4b54e 100644 --- a/apps/electron/layers/main/src/handlers/dialog/dialog.ts +++ b/apps/electron/layers/main/src/handlers/dialog/dialog.ts @@ -47,6 +47,7 @@ const ErrorMessages = [ 'DB_FILE_ALREADY_LOADED', 'DB_FILE_PATH_INVALID', 'DB_FILE_INVALID', + 'FILE_ALREADY_EXISTS', 'UNKNOWN_ERROR', ] as const; @@ -201,7 +202,7 @@ export async function loadDBFile(): Promise { await fs.ensureDir(path.join(appContext.appDataPath, 'workspaces')); - await fs.symlink(filePath, linkedFilePath); + await fs.symlink(filePath, linkedFilePath, 'file'); logger.info(`loadDBFile, symlink: ${filePath} -> ${linkedFilePath}`); return { workspaceId }; @@ -262,6 +263,12 @@ export async function moveDBFile( }; } + if (await fs.pathExists(newFilePath)) { + return { + error: 'FILE_ALREADY_EXISTS', + }; + } + if (isLink) { // remove the old link to unblock new link await fs.unlink(db.path); @@ -271,9 +278,12 @@ export async function moveDBFile( overwrite: true, }); - await fs.ensureSymlink(newFilePath, db.path); + db.db.close(); + + await fs.ensureSymlink(newFilePath, db.path, 'file'); logger.info(`openMoveDBFileDialog symlink: ${realpath} -> ${newFilePath}`); db.reconnectDB(); + return { filePath: newFilePath, }; diff --git a/apps/web/src/components/affine/create-workspace-modal/index.tsx b/apps/web/src/components/affine/create-workspace-modal/index.tsx index 134e5b2a82..cad003ef75 100644 --- a/apps/web/src/components/affine/create-workspace-modal/index.tsx +++ b/apps/web/src/components/affine/create-workspace-modal/index.tsx @@ -118,10 +118,7 @@ interface SetDBLocationContentProps { onConfirmLocation: (dir?: string) => void; } -const SetDBLocationContent = ({ - onConfirmLocation, -}: SetDBLocationContentProps) => { - const t = useAFFiNEI18N(); +const useDefaultDBLocation = () => { const [defaultDBLocation, setDefaultDBLocation] = useState(''); useEffect(() => { @@ -130,20 +127,40 @@ const SetDBLocationContent = ({ }); }, []); + return defaultDBLocation; +}; + +const SetDBLocationContent = ({ + onConfirmLocation, +}: SetDBLocationContentProps) => { + const t = useAFFiNEI18N(); + const defaultDBLocation = useDefaultDBLocation(); + const [opening, setOpening] = useState(false); + + const handleSelectDBFileLocation = async () => { + if (opening) { + return; + } + setOpening(true); + const result = await window.apis?.dialog.selectDBFileLocation(); + setOpening(false); + if (result?.filePath) { + onConfirmLocation(result.filePath); + } else if (result?.error) { + toast(t[result.error]()); + } + }; + return (
{t['Set database location']()}

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

diff --git a/apps/web/src/components/affine/workspace-setting-detail/index.css.ts b/apps/web/src/components/affine/workspace-setting-detail/index.css.ts index da1cac3494..0dbcfb2bff 100644 --- a/apps/web/src/components/affine/workspace-setting-detail/index.css.ts +++ b/apps/web/src/components/affine/workspace-setting-detail/index.css.ts @@ -6,8 +6,10 @@ import { globalStyle, style, styleVariants } from '@vanilla-extract/css'; export const container = style({ display: 'flex', flexDirection: 'column', - padding: '52px 52px 0 52px', + marginTop: '52px', + padding: '0 52px 52px 52px', height: 'calc(100vh - 52px)', + overflow: 'auto', }); export const sidebar = style({ @@ -15,7 +17,6 @@ export const sidebar = style({ }); export const content = style({ - overflow: 'auto', flex: 1, marginTop: '40px', }); @@ -157,7 +158,10 @@ export const indicator = style({ export const tabButtonWrapper = style({ display: 'flex', - position: 'relative', + position: 'sticky', + top: '0', + background: 'var(--affine-background-primary-color)', + zIndex: 1, }); export const storageTypeWrapper = style({ @@ -176,6 +180,10 @@ export const storageTypeWrapper = style({ '&:not(:last-child)': { marginBottom: '12px', }, + '&[data-disabled="true"]': { + cursor: 'default', + pointerEvents: 'none', + }, }, }); diff --git a/apps/web/src/components/affine/workspace-setting-detail/panel/export/index.tsx b/apps/web/src/components/affine/workspace-setting-detail/panel/export/index.tsx index 60b681f8d4..8572eda17c 100644 --- a/apps/web/src/components/affine/workspace-setting-detail/panel/export/index.tsx +++ b/apps/web/src/components/affine/workspace-setting-detail/panel/export/index.tsx @@ -15,8 +15,13 @@ export const ExportPanel = () => { disabled={!environment.isDesktop || !id} data-testid="export-affine-backup" onClick={async () => { - if (id && (await window.apis?.dialog.saveDBFileAs(id))) { - toast(t['Export success']()); + if (id) { + const result = await window.apis?.dialog.saveDBFileAs(id); + if (result?.error) { + toast(t[result.error]()); + } else if (!result?.canceled) { + toast(t['Export success']()); + } } }} > diff --git a/apps/web/src/components/affine/workspace-setting-detail/panel/general/index.tsx b/apps/web/src/components/affine/workspace-setting-detail/panel/general/index.tsx index 8e0450c975..0c76ca79c4 100644 --- a/apps/web/src/components/affine/workspace-setting-detail/panel/general/index.tsx +++ b/apps/web/src/components/affine/workspace-setting-detail/panel/general/index.tsx @@ -12,7 +12,7 @@ import { useBlockSuiteWorkspaceAvatarUrl } from '@toeverything/hooks/use-block-s import { useBlockSuiteWorkspaceName } from '@toeverything/hooks/use-block-suite-workspace-name'; import clsx from 'clsx'; import type React from 'react'; -import { useState } from 'react'; +import { useEffect, useState } from 'react'; import { useIsWorkspaceOwner } from '../../../../../hooks/affine/use-is-workspace-owner'; import { Upload } from '../../../../pure/file-upload'; @@ -23,6 +23,26 @@ import { CameraIcon } from './icons'; import { WorkspaceLeave } from './leave'; import { StyledInput } from './style'; +const useDBFilePathMeta = (workspaceId: string) => { + const [meta, setMeta] = useState<{ + path: string; + realPath: string; + }>(); + useEffect(() => { + if (window.apis && window.events) { + window.apis.db.getDBFilePath(workspaceId).then(meta => { + setMeta(meta); + }); + return window.events.db.onDBFilePathChange(meta => { + if (meta.workspaceId === workspaceId) { + setMeta(meta); + } + }); + } + }, [workspaceId]); + return meta; +}; + export const GeneralPanel: React.FC = ({ workspace, onDeleteWorkspace, @@ -36,11 +56,36 @@ export const GeneralPanel: React.FC = ({ const isOwner = useIsWorkspaceOwner(workspace); const t = useAFFiNEI18N(); + const dbPathMeta = useDBFilePathMeta(workspace.id); + const showOpenFolder = + environment.isDesktop && dbPathMeta?.path !== dbPathMeta?.realPath; + const handleUpdateWorkspaceName = (name: string) => { setName(name); toast(t['Update workspace name success']()); }; + const [moveToInProgress, setMoveToInProgress] = useState(false); + + const handleMoveTo = async () => { + if (moveToInProgress) { + return; + } + try { + setMoveToInProgress(true); + const result = await window.apis?.dialog.moveDBFile(workspace.id); + if (!result?.error && !result?.canceled) { + toast(t['Move folder success']()); + } else if (result?.error) { + toast(t[result.error]()); + } + } catch (err) { + toast(t['UNKNOWN_ERROR']()); + } finally { + setMoveToInProgress(false); + } + }; + const [, update] = useBlockSuiteWorkspaceAvatarUrl( workspace.blockSuiteWorkspace ); @@ -128,34 +173,33 @@ export const GeneralPanel: React.FC = ({
-
{ - if (environment.isDesktop) { - window.apis?.dialog.revealDBFile(workspace.id); - } - }} - > - -
-
- {t['Open folder']()} -
-
- {t['Open folder hint']()} + {showOpenFolder && ( +
{ + if (environment.isDesktop) { + window.apis?.dialog.revealDBFile(workspace.id); + } + }} + > + +
+
+ {t['Open folder']()} +
+
+ {t['Open folder hint']()} +
+
- -
+ )}
{ - if (await window.apis?.dialog.moveDBFile(workspace.id)) { - toast(t['Move folder success']()); - } - }} + onClick={handleMoveTo} >
diff --git a/packages/i18n/src/resources/en.json b/packages/i18n/src/resources/en.json index 61f0ff209e..50be5ae516 100644 --- a/packages/i18n/src/resources/en.json +++ b/packages/i18n/src/resources/en.json @@ -260,6 +260,7 @@ "Move folder hint": "Select a new storage location.", "Storage Folder": "Storage Folder", "DB_FILE_INVALID": "Invalid Database file", + "FILE_ALREADY_EXISTS": "File already exists", "Name Your Workspace": "Name Your Workspace", "DB_FILE_PATH_INVALID": "Database file path invalid", "Default db location hint": "By default will be saved to {{location}}", diff --git a/packages/workspace/src/providers/__tests__/sqlite-provider.spec.ts b/packages/workspace/src/providers/__tests__/sqlite-provider.spec.ts index 2fe7abffbf..1d43b35308 100644 --- a/packages/workspace/src/providers/__tests__/sqlite-provider.spec.ts +++ b/packages/workspace/src/providers/__tests__/sqlite-provider.spec.ts @@ -36,7 +36,7 @@ vi.stubGlobal('window', { }, events: { db: { - onDbFileUpdate: (fn: (id: string) => void) => { + onDBFileUpdate: (fn: (id: string) => void) => { triggerDBUpdate = fn; return () => { triggerDBUpdate = null; @@ -44,7 +44,10 @@ vi.stubGlobal('window', { }, // not used in this test - onDbFileMissing: () => { + onDBFileMissing: () => { + return () => {}; + }, + onDBFilePathChange: () => { return () => {}; }, }, diff --git a/packages/workspace/src/providers/index.ts b/packages/workspace/src/providers/index.ts index 7064febad0..1b81c334a8 100644 --- a/packages/workspace/src/providers/index.ts +++ b/packages/workspace/src/providers/index.ts @@ -243,7 +243,7 @@ const createSQLiteProvider = ( blockSuiteWorkspace.doc.on('update', handleUpdate); let timer = 0; - unsubscribe = events.db.onDbFileUpdate(workspaceId => { + unsubscribe = events.db.onDBFileUpdate(workspaceId => { if (workspaceId === blockSuiteWorkspace.id) { // throttle logger.debug('on db update', workspaceId);