fix: should not show open folder if it is not moved (#2299)

This commit is contained in:
Peng Xiao
2023-05-11 13:36:22 +08:00
committed by LongYinan
parent 9902892615
commit 20fb801ecd
12 changed files with 170 additions and 51 deletions

View File

@@ -2,25 +2,37 @@ import { Subject } from 'rxjs';
import type { MainEventListener } from './type'; import type { MainEventListener } from './type';
interface DBFilePathMeta {
workspaceId: string;
path: string;
realPath: string;
}
export const dbSubjects = { export const dbSubjects = {
// emit workspace ids // emit workspace ids
dbFileMissing: new Subject<string>(), dbFileMissing: new Subject<string>(),
// emit workspace ids // emit workspace ids
dbFileUpdate: new Subject<string>(), dbFileUpdate: new Subject<string>(),
dbFilePathChange: new Subject<DBFilePathMeta>(),
}; };
export const dbEvents = { export const dbEvents = {
onDbFileMissing: (fn: (workspaceId: string) => void) => { onDBFileMissing: (fn: (workspaceId: string) => void) => {
const sub = dbSubjects.dbFileMissing.subscribe(fn); const sub = dbSubjects.dbFileMissing.subscribe(fn);
return () => { return () => {
sub.unsubscribe(); sub.unsubscribe();
}; };
}, },
onDbFileUpdate: (fn: (workspaceId: string) => void) => { onDBFileUpdate: (fn: (workspaceId: string) => void) => {
const sub = dbSubjects.dbFileUpdate.subscribe(fn); const sub = dbSubjects.dbFileUpdate.subscribe(fn);
return () => { return () => {
sub.unsubscribe(); sub.unsubscribe();
}; };
}, },
onDBFilePathChange: (fn: (meta: DBFilePathMeta) => void) => {
const sub = dbSubjects.dbFilePathChange.subscribe(fn);
return () => {
sub.unsubscribe();
};
},
} satisfies Record<string, MainEventListener>; } satisfies Record<string, MainEventListener>;

View File

@@ -160,7 +160,7 @@ describe('ensureSQLiteDB', () => {
// wait for 1000ms for file watcher to detect file removal // wait for 1000ms for file watcher to detect file removal
await delay(2000); await delay(2000);
expect(sendStub).toBeCalledWith('db:onDbFileMissing', id); expect(sendStub).toBeCalledWith('db:onDBFileMissing', id);
// ensureSQLiteDB should recreate the db file // ensureSQLiteDB should recreate the db file
workspaceDB = await ensureSQLiteDB(id); workspaceDB = await ensureSQLiteDB(id);
@@ -190,10 +190,7 @@ describe('ensureSQLiteDB', () => {
// wait for 200ms for file watcher to detect file change // wait for 200ms for file watcher to detect file change
await delay(2000); await delay(2000);
expect(sendStub).toBeCalledWith('db:onDbFileUpdate', id); expect(sendStub).toBeCalledWith('db:onDBFileUpdate', id);
// should only call once for multiple writes
expect(sendStub).toBeCalledTimes(1);
}); });
}); });

View File

@@ -1,3 +1,5 @@
import fs from 'fs-extra';
import { appContext } from '../../context'; import { appContext } from '../../context';
import type { NamespaceHandlers } from '../type'; import type { NamespaceHandlers } from '../type';
import { ensureSQLiteDB } from './ensure-db'; import { ensureSQLiteDB } from './ensure-db';
@@ -30,4 +32,11 @@ export const dbHandlers = {
getDefaultStorageLocation: async () => { getDefaultStorageLocation: async () => {
return appContext.appDataPath; return appContext.appDataPath;
}, },
getDBFilePath: async (_, workspaceId: string) => {
const workspaceDB = await ensureSQLiteDB(workspaceId);
return {
path: workspaceDB.path,
realPath: await fs.realpath(workspaceDB.path),
};
},
} satisfies NamespaceHandlers; } satisfies NamespaceHandlers;

View File

@@ -6,6 +6,7 @@ import fs from 'fs-extra';
import * as Y from 'yjs'; import * as Y from 'yjs';
import type { AppContext } from '../../context'; import type { AppContext } from '../../context';
import { dbSubjects } from '../../events/db';
import { logger } from '../../logger'; import { logger } from '../../logger';
import { ts } from '../../utils'; import { ts } from '../../utils';
@@ -62,6 +63,18 @@ export class WorkspaceSQLiteDB {
this.db.close(); 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? // use cached version?
const db = (this.db = sqlite(this.path)); const db = (this.db = sqlite(this.path));
db.exec(schemas.join(';')); db.exec(schemas.join(';'));

View File

@@ -47,6 +47,7 @@ const ErrorMessages = [
'DB_FILE_ALREADY_LOADED', 'DB_FILE_ALREADY_LOADED',
'DB_FILE_PATH_INVALID', 'DB_FILE_PATH_INVALID',
'DB_FILE_INVALID', 'DB_FILE_INVALID',
'FILE_ALREADY_EXISTS',
'UNKNOWN_ERROR', 'UNKNOWN_ERROR',
] as const; ] as const;
@@ -201,7 +202,7 @@ export async function loadDBFile(): Promise<LoadDBFileResult> {
await fs.ensureDir(path.join(appContext.appDataPath, 'workspaces')); 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}`); logger.info(`loadDBFile, symlink: ${filePath} -> ${linkedFilePath}`);
return { workspaceId }; return { workspaceId };
@@ -262,6 +263,12 @@ export async function moveDBFile(
}; };
} }
if (await fs.pathExists(newFilePath)) {
return {
error: 'FILE_ALREADY_EXISTS',
};
}
if (isLink) { if (isLink) {
// remove the old link to unblock new link // remove the old link to unblock new link
await fs.unlink(db.path); await fs.unlink(db.path);
@@ -271,9 +278,12 @@ export async function moveDBFile(
overwrite: true, overwrite: true,
}); });
await fs.ensureSymlink(newFilePath, db.path); db.db.close();
await fs.ensureSymlink(newFilePath, db.path, 'file');
logger.info(`openMoveDBFileDialog symlink: ${realpath} -> ${newFilePath}`); logger.info(`openMoveDBFileDialog symlink: ${realpath} -> ${newFilePath}`);
db.reconnectDB(); db.reconnectDB();
return { return {
filePath: newFilePath, filePath: newFilePath,
}; };

View File

@@ -118,10 +118,7 @@ interface SetDBLocationContentProps {
onConfirmLocation: (dir?: string) => void; onConfirmLocation: (dir?: string) => void;
} }
const SetDBLocationContent = ({ const useDefaultDBLocation = () => {
onConfirmLocation,
}: SetDBLocationContentProps) => {
const t = useAFFiNEI18N();
const [defaultDBLocation, setDefaultDBLocation] = useState(''); const [defaultDBLocation, setDefaultDBLocation] = useState('');
useEffect(() => { 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 ( return (
<div className={style.content}> <div className={style.content}>
<div className={style.contentTitle}>{t['Set database location']()}</div> <div className={style.contentTitle}>{t['Set database location']()}</div>
<p>{t['Workspace database storage description']()}</p> <p>{t['Workspace database storage description']()}</p>
<div className={style.buttonGroup}> <div className={style.buttonGroup}>
<Button <Button
disabled={opening}
data-testid="create-workspace-customize-button" data-testid="create-workspace-customize-button"
type="light" type="light"
onClick={async () => { onClick={handleSelectDBFileLocation}
const result = await window.apis?.dialog.selectDBFileLocation();
if (result) {
onConfirmLocation(result.filePath);
}
}}
> >
{t['Customize']()} {t['Customize']()}
</Button> </Button>

View File

@@ -6,8 +6,10 @@ import { globalStyle, style, styleVariants } from '@vanilla-extract/css';
export const container = style({ export const container = style({
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
padding: '52px 52px 0 52px', marginTop: '52px',
padding: '0 52px 52px 52px',
height: 'calc(100vh - 52px)', height: 'calc(100vh - 52px)',
overflow: 'auto',
}); });
export const sidebar = style({ export const sidebar = style({
@@ -15,7 +17,6 @@ export const sidebar = style({
}); });
export const content = style({ export const content = style({
overflow: 'auto',
flex: 1, flex: 1,
marginTop: '40px', marginTop: '40px',
}); });
@@ -157,7 +158,10 @@ export const indicator = style({
export const tabButtonWrapper = style({ export const tabButtonWrapper = style({
display: 'flex', display: 'flex',
position: 'relative', position: 'sticky',
top: '0',
background: 'var(--affine-background-primary-color)',
zIndex: 1,
}); });
export const storageTypeWrapper = style({ export const storageTypeWrapper = style({
@@ -176,6 +180,10 @@ export const storageTypeWrapper = style({
'&:not(:last-child)': { '&:not(:last-child)': {
marginBottom: '12px', marginBottom: '12px',
}, },
'&[data-disabled="true"]': {
cursor: 'default',
pointerEvents: 'none',
},
}, },
}); });

View File

@@ -15,8 +15,13 @@ export const ExportPanel = () => {
disabled={!environment.isDesktop || !id} disabled={!environment.isDesktop || !id}
data-testid="export-affine-backup" data-testid="export-affine-backup"
onClick={async () => { onClick={async () => {
if (id && (await window.apis?.dialog.saveDBFileAs(id))) { if (id) {
toast(t['Export success']()); const result = await window.apis?.dialog.saveDBFileAs(id);
if (result?.error) {
toast(t[result.error]());
} else if (!result?.canceled) {
toast(t['Export success']());
}
} }
}} }}
> >

View File

@@ -12,7 +12,7 @@ import { useBlockSuiteWorkspaceAvatarUrl } from '@toeverything/hooks/use-block-s
import { useBlockSuiteWorkspaceName } from '@toeverything/hooks/use-block-suite-workspace-name'; import { useBlockSuiteWorkspaceName } from '@toeverything/hooks/use-block-suite-workspace-name';
import clsx from 'clsx'; import clsx from 'clsx';
import type React from 'react'; import type React from 'react';
import { useState } from 'react'; import { useEffect, useState } from 'react';
import { useIsWorkspaceOwner } from '../../../../../hooks/affine/use-is-workspace-owner'; import { useIsWorkspaceOwner } from '../../../../../hooks/affine/use-is-workspace-owner';
import { Upload } from '../../../../pure/file-upload'; import { Upload } from '../../../../pure/file-upload';
@@ -23,6 +23,26 @@ import { CameraIcon } from './icons';
import { WorkspaceLeave } from './leave'; import { WorkspaceLeave } from './leave';
import { StyledInput } from './style'; 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<PanelProps> = ({ export const GeneralPanel: React.FC<PanelProps> = ({
workspace, workspace,
onDeleteWorkspace, onDeleteWorkspace,
@@ -36,11 +56,36 @@ export const GeneralPanel: React.FC<PanelProps> = ({
const isOwner = useIsWorkspaceOwner(workspace); const isOwner = useIsWorkspaceOwner(workspace);
const t = useAFFiNEI18N(); const t = useAFFiNEI18N();
const dbPathMeta = useDBFilePathMeta(workspace.id);
const showOpenFolder =
environment.isDesktop && dbPathMeta?.path !== dbPathMeta?.realPath;
const handleUpdateWorkspaceName = (name: string) => { const handleUpdateWorkspaceName = (name: string) => {
setName(name); setName(name);
toast(t['Update workspace name success']()); toast(t['Update workspace name success']());
}; };
const [moveToInProgress, setMoveToInProgress] = useState<boolean>(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( const [, update] = useBlockSuiteWorkspaceAvatarUrl(
workspace.blockSuiteWorkspace workspace.blockSuiteWorkspace
); );
@@ -128,34 +173,33 @@ export const GeneralPanel: React.FC<PanelProps> = ({
</div> </div>
<div className={style.col}> <div className={style.col}>
<div {showOpenFolder && (
className={style.storageTypeWrapper} <div
onClick={() => { className={style.storageTypeWrapper}
if (environment.isDesktop) { onClick={() => {
window.apis?.dialog.revealDBFile(workspace.id); if (environment.isDesktop) {
} window.apis?.dialog.revealDBFile(workspace.id);
}} }
> }}
<FolderIcon color="var(--affine-primary-color)" /> >
<div className={style.storageTypeLabelWrapper}> <FolderIcon color="var(--affine-primary-color)" />
<div className={style.storageTypeLabel}> <div className={style.storageTypeLabelWrapper}>
{t['Open folder']()} <div className={style.storageTypeLabel}>
</div> {t['Open folder']()}
<div className={style.storageTypeLabelHint}> </div>
{t['Open folder hint']()} <div className={style.storageTypeLabelHint}>
{t['Open folder hint']()}
</div>
</div> </div>
<ArrowRightSmallIcon color="var(--affine-primary-color)" />
</div> </div>
<ArrowRightSmallIcon color="var(--affine-primary-color)" /> )}
</div>
<div <div
data-testid="move-folder" data-testid="move-folder"
data-disabled={moveToInProgress}
className={style.storageTypeWrapper} className={style.storageTypeWrapper}
onClick={async () => { onClick={handleMoveTo}
if (await window.apis?.dialog.moveDBFile(workspace.id)) {
toast(t['Move folder success']());
}
}}
> >
<MoveToIcon color="var(--affine-primary-color)" /> <MoveToIcon color="var(--affine-primary-color)" />
<div className={style.storageTypeLabelWrapper}> <div className={style.storageTypeLabelWrapper}>

View File

@@ -260,6 +260,7 @@
"Move folder hint": "Select a new storage location.", "Move folder hint": "Select a new storage location.",
"Storage Folder": "Storage Folder", "Storage Folder": "Storage Folder",
"DB_FILE_INVALID": "Invalid Database file", "DB_FILE_INVALID": "Invalid Database file",
"FILE_ALREADY_EXISTS": "File already exists",
"Name Your Workspace": "Name Your Workspace", "Name Your Workspace": "Name Your Workspace",
"DB_FILE_PATH_INVALID": "Database file path invalid", "DB_FILE_PATH_INVALID": "Database file path invalid",
"Default db location hint": "By default will be saved to {{location}}", "Default db location hint": "By default will be saved to {{location}}",

View File

@@ -36,7 +36,7 @@ vi.stubGlobal('window', {
}, },
events: { events: {
db: { db: {
onDbFileUpdate: (fn: (id: string) => void) => { onDBFileUpdate: (fn: (id: string) => void) => {
triggerDBUpdate = fn; triggerDBUpdate = fn;
return () => { return () => {
triggerDBUpdate = null; triggerDBUpdate = null;
@@ -44,7 +44,10 @@ vi.stubGlobal('window', {
}, },
// not used in this test // not used in this test
onDbFileMissing: () => { onDBFileMissing: () => {
return () => {};
},
onDBFilePathChange: () => {
return () => {}; return () => {};
}, },
}, },

View File

@@ -243,7 +243,7 @@ const createSQLiteProvider = (
blockSuiteWorkspace.doc.on('update', handleUpdate); blockSuiteWorkspace.doc.on('update', handleUpdate);
let timer = 0; let timer = 0;
unsubscribe = events.db.onDbFileUpdate(workspaceId => { unsubscribe = events.db.onDBFileUpdate(workspaceId => {
if (workspaceId === blockSuiteWorkspace.id) { if (workspaceId === blockSuiteWorkspace.id) {
// throttle // throttle
logger.debug('on db update', workspaceId); logger.debug('on db update', workspaceId);