feat: move folder and restart wiki in workspace setting

This commit is contained in:
lin onetwo 2025-11-20 20:39:28 +08:00
parent 0e96d94809
commit d9d08aa1e9
10 changed files with 160 additions and 4 deletions

View file

@ -17,7 +17,42 @@ Feature: TidGi Default Wiki
| div[data-testid^='workspace-']:has-text('wiki') | | div[data-testid^='workspace-']:has-text('wiki') |
And the window title should contain "" And the window title should contain ""
@wiki @browser-view @wiki
Scenario: Default wiki workspace displays TiddlyWiki content in browser view Scenario: Default wiki workspace displays TiddlyWiki content in browser view
And the browser view should be loaded and visible And the browser view should be loaded and visible
And I should see " TiddlyWiki" in the browser view content And I should see " TiddlyWiki" in the browser view content
@wiki @move-workspace
Scenario: Move workspace to a new location
When I click menu " > "
And I switch to "editWorkspace" window
And I wait for the page to load completely
When I click on a "save and sync options accordion" element with selector "[data-testid='preference-section-saveAndSyncOptions']"
Then I should see a "move workspace button" element with selector "button:has-text('')"
# Test the actual move operation - this will trigger a file dialog
When I prepare to select directory in dialog "wiki-test-moved"
And I click on a "move workspace button" element with selector "button:has-text('')"
Then I wait for "workspace moved to wiki-test-moved" log marker "[test-id-WORKSPACE_MOVED:"
Then I wait for "workspace restarted after move" log marker "[test-id-WORKSPACE_RESTARTED_AFTER_MOVE:"
# Wait for SSE and watch-fs to stabilize after restart (logs are in wiki log file, not TidGi log)
And I wait for 2 seconds
# Verify the workspace was moved to the new location
Then file "wiki/tiddlywiki.info" should exist in "wiki-test-moved"
# Verify the wiki is working by modifying a file in the new location
When I modify file "wiki-test-moved/wiki/tiddlers/Index.tid" to contain "Content after moving workspace"
Then I wait for tiddler "Index" to be updated by watch-fs
And I wait for 2 seconds
And I should see "Content after moving workspace" in the browser view content
# Move it back to original location for cleanup
When I prepare to select directory in dialog "wiki-test"
And I click on a "move workspace button" element with selector "button:has-text('')"
Then I wait for "workspace moved back to wiki-test" log marker "[test-id-WORKSPACE_MOVED:"
Then I wait for "workspace restarted after move back" log marker "[test-id-WORKSPACE_RESTARTED_AFTER_MOVE:"
# Wait for SSE and watch-fs to stabilize after restart back
And I wait for 2 seconds
Then file "wiki/tiddlywiki.info" should exist in "wiki-test"
# Verify the wiki still works after moving back
When I modify file "wiki-test/wiki/tiddlers/Index.tid" to contain "Content after moving back"
Then I wait for tiddler "Index" to be updated by watch-fs
And I wait for 2 seconds
And I should see "Content after moving back" in the browser view content

View file

@ -332,3 +332,20 @@ When('I launch the TidGi application', async function(this: ApplicationWorld) {
); );
} }
}); });
When('I prepare to select directory in dialog {string}', async function(this: ApplicationWorld, directoryName: string) {
if (!this.app) {
throw new Error('Application is not launched');
}
const targetPath = path.resolve(process.cwd(), directoryName);
// Setup dialog handler to intercept showOpenDialog calls in main process
await this.app.evaluate(({ dialog }, targetDirectory: string) => {
// Override showOpenDialog to return the test directory
dialog.showOpenDialog = async () => {
return {
canceled: false,
filePaths: [targetDirectory],
};
};
}, targetPath);
});

View file

@ -1,5 +1,6 @@
import { After, Before } from '@cucumber/cucumber'; import { After, Before } from '@cucumber/cucumber';
import fs from 'fs-extra'; import fs from 'fs-extra';
import path from 'path';
import { logsDirectory, screenshotsDirectory } from '../supports/paths'; import { logsDirectory, screenshotsDirectory } from '../supports/paths';
import { clearAISettings } from './agent'; import { clearAISettings } from './agent';
import { ApplicationWorld } from './application'; import { ApplicationWorld } from './application';
@ -71,6 +72,13 @@ After(async function(this: ApplicationWorld, { pickle }) {
if (pickle.tags.some((tag) => tag.name === '@hibernation')) { if (pickle.tags.some((tag) => tag.name === '@hibernation')) {
await clearHibernationTestData(); await clearHibernationTestData();
} }
// Clean up move workspace test data - remove wiki-test-moved folder
if (pickle.tags.some((tag) => tag.name === '@move-workspace')) {
const wikiTestMovedPath = path.resolve(process.cwd(), 'wiki-test-moved');
if (await fs.pathExists(wikiTestMovedPath)) {
await fs.remove(wikiTestMovedPath);
}
}
// Separate logs by test scenario for easier debugging // Separate logs by test scenario for easier debugging
try { try {

View file

@ -323,9 +323,10 @@ Then('I wait for {string} log marker {string}', async function(this: Application
// Determine timeout and log prefix based on operation type // Determine timeout and log prefix based on operation type
const isGitOperation = marker.includes('git-') || marker.includes('revert'); const isGitOperation = marker.includes('git-') || marker.includes('revert');
const isWikiRestart = marker.includes('MAIN_WIKI_RESTARTED'); const isWikiRestart = marker.includes('MAIN_WIKI_RESTARTED');
const isWorkspaceOperation = marker.includes('WORKSPACE_');
const isRevert = marker.includes('revert'); const isRevert = marker.includes('revert');
const timeout = isRevert ? 30000 : (isWikiRestart ? 25000 : (isGitOperation ? 25000 : 15000)); const timeout = isRevert ? 30000 : (isWikiRestart ? 25000 : (isGitOperation ? 25000 : 15000));
const logPrefix = (isGitOperation || isWikiRestart) ? 'TidGi-' : undefined; const logPrefix = (isGitOperation || isWikiRestart || isWorkspaceOperation) ? 'TidGi-' : undefined;
await waitForLogMarker(marker, `Log marker "${marker}" not found. Expected: ${description}`, timeout, logPrefix); await waitForLogMarker(marker, `Log marker "${marker}" not found. Expected: ${description}`, timeout, logPrefix);
}); });

View file

@ -171,6 +171,12 @@
"LastVisitState": "Last page visited", "LastVisitState": "Last page visited",
"MainWorkspacePath": "Main Workspace Path", "MainWorkspacePath": "Main Workspace Path",
"MiscOptions": "Misc", "MiscOptions": "Misc",
"MoveWorkspace": "Move Workspace...",
"MoveWorkspaceTooltip": "Move workspace folder to a new location. The wiki will be stopped before moving to prevent file lock issues.",
"MoveWorkspaceSuccess": "Workspace Moved Successfully",
"MoveWorkspaceSuccessMessage": "Workspace {{name}} has been moved to {{newLocation}}. The edit window will close automatically.",
"MoveWorkspaceFailed": "Failed to Move Workspace",
"MoveWorkspaceFailedMessage": "Failed to move workspace",
"Name": "Workspace Name", "Name": "Workspace Name",
"NameDescription": "The name of the workspace, which will be displayed on the sidebar, can be different from the actual folder name of the Git repository in the workspace", "NameDescription": "The name of the workspace, which will be displayed on the sidebar, can be different from the actual folder name of the Git repository in the workspace",
"NoRevert": "Caution! This operation can't be reverted.", "NoRevert": "Caution! This operation can't be reverted.",

View file

@ -171,6 +171,12 @@
"LastVisitState": "上次访问的页面", "LastVisitState": "上次访问的页面",
"MainWorkspacePath": "主工作区路径", "MainWorkspacePath": "主工作区路径",
"MiscOptions": "杂项设置", "MiscOptions": "杂项设置",
"MoveWorkspace": "移动工作区...",
"MoveWorkspaceTooltip": "将工作区文件夹移动到新位置。移动前会自动停止知识库以避免文件锁定问题。",
"MoveWorkspaceSuccess": "工作区移动成功",
"MoveWorkspaceSuccessMessage": "工作区 {{name}} 已移动到 {{newLocation}}。编辑窗口将自动关闭。",
"MoveWorkspaceFailed": "移动工作区失败",
"MoveWorkspaceFailedMessage": "移动工作区失败",
"Name": "工作区名", "Name": "工作区名",
"NameDescription": "工作区的名字将显示在侧边栏上可以与工作区Git仓库的实际文件夹名不同", "NameDescription": "工作区的名字将显示在侧边栏上可以与工作区Git仓库的实际文件夹名不同",
"NoRevert": "注意!这个操作无法撤销!", "NoRevert": "注意!这个操作无法撤销!",

View file

@ -1,5 +1,7 @@
import { app, dialog, powerMonitor } from 'electron'; import { app, dialog, powerMonitor } from 'electron';
import { copy, pathExists, remove } from 'fs-extra';
import { inject, injectable } from 'inversify'; import { inject, injectable } from 'inversify';
import path from 'path';
import type { IAuthenticationService } from '@services/auth/interface'; import type { IAuthenticationService } from '@services/auth/interface';
import { container } from '@services/container'; import { container } from '@services/container';
@ -255,4 +257,59 @@ export class WikiGitWorkspace implements IWikiGitWorkspaceService {
} }
} }
} }
public async moveWorkspaceLocation(workspaceID: string, newParentLocation: string): Promise<void> {
const workspaceService = container.get<IWorkspaceService>(serviceIdentifier.Workspace);
const workspace = await workspaceService.get(workspaceID);
if (workspace === undefined) {
throw new Error(`Need to get workspace with id ${workspaceID} but failed`);
}
if (!isWikiWorkspace(workspace)) {
throw new Error('moveWorkspaceLocation can only be called with wiki workspaces');
}
const { wikiFolderLocation, name } = workspace;
const wikiFolderName = path.basename(wikiFolderLocation);
const newWikiFolderLocation = path.join(newParentLocation, wikiFolderName);
if (!(await pathExists(wikiFolderLocation))) {
throw new Error(`Source wiki folder does not exist: ${wikiFolderLocation}`);
}
if (await pathExists(newWikiFolderLocation)) {
throw new Error(`Target location already exists: ${newWikiFolderLocation}`);
}
try {
logger.info(`Moving workspace ${name} from ${wikiFolderLocation} to ${newWikiFolderLocation}`);
const wikiService = container.get<IWikiService>(serviceIdentifier.Wiki);
await wikiService.stopWiki(workspaceID).catch((error_: unknown) => {
const error = error_ as Error;
logger.error(`Failed to stop wiki before move: ${error.message}`, { error });
});
await copy(wikiFolderLocation, newWikiFolderLocation, {
overwrite: false,
errorOnExist: true,
});
await workspaceService.update(workspaceID, {
wikiFolderLocation: newWikiFolderLocation,
});
await remove(wikiFolderLocation);
logger.info(`Successfully moved workspace to ${newWikiFolderLocation} [test-id-WORKSPACE_MOVED:${newWikiFolderLocation}]`);
// Restart the workspace view to load from new location
const workspaceViewService = container.get<IWorkspaceViewService>(serviceIdentifier.WorkspaceView);
await workspaceViewService.restartWorkspaceViewService(workspaceID);
logger.info(`Workspace view restarted after move [test-id-WORKSPACE_RESTARTED_AFTER_MOVE:${workspaceID}]`);
} catch (error_: unknown) {
const error = error_ as Error;
logger.error(`Failed to move workspace: ${error.message}`, { error });
throw error;
}
}
} }

View file

@ -18,11 +18,18 @@ export interface IWikiGitWorkspaceService {
* Automatically initialize a default wiki workspace if none exists. * Automatically initialize a default wiki workspace if none exists.
*/ */
initialize(): Promise<void>; initialize(): Promise<void>;
/**
* Move workspace to a new location. Will stop wiki worker before moving to prevent file lock issues.
* @param workspaceID The workspace to move
* @param newLocation The new parent folder path where the wiki folder will be moved
*/
moveWorkspaceLocation: (workspaceID: string, newLocation: string) => Promise<void>;
} }
export const WikiGitWorkspaceServiceIPCDescriptor = { export const WikiGitWorkspaceServiceIPCDescriptor = {
channel: WikiGitWorkspaceChannel.name, channel: WikiGitWorkspaceChannel.name,
properties: { properties: {
initWikiGitTransaction: ProxyPropertyType.Function, initWikiGitTransaction: ProxyPropertyType.Function,
removeWorkspace: ProxyPropertyType.Function, removeWorkspace: ProxyPropertyType.Function,
moveWorkspaceLocation: ProxyPropertyType.Function,
}, },
}; };

View file

@ -257,7 +257,7 @@ export default function EditWorkspace(): React.JSX.Element {
</OptionsAccordion> </OptionsAccordion>
<OptionsAccordion> <OptionsAccordion>
<Tooltip title={t('EditWorkspace.ClickToExpand')}> <Tooltip title={t('EditWorkspace.ClickToExpand')}>
<OptionsAccordionSummary expandIcon={<ExpandMoreIcon />}> <OptionsAccordionSummary expandIcon={<ExpandMoreIcon />} data-testid='preference-section-saveAndSyncOptions'>
{t('EditWorkspace.SaveAndSyncOptions')} {t('EditWorkspace.SaveAndSyncOptions')}
</OptionsAccordionSummary> </OptionsAccordionSummary>
</Tooltip> </Tooltip>
@ -274,6 +274,25 @@ export default function EditWorkspace(): React.JSX.Element {
workspaceSetter({ ...workspace, wikiFolderLocation: event.target.value }); workspaceSetter({ ...workspace, wikiFolderLocation: event.target.value });
}} }}
/> />
<Tooltip title={t('EditWorkspace.MoveWorkspaceTooltip') ?? ''} placement='top'>
<PictureButton
variant='outlined'
size='small'
onClick={async () => {
const directories = await window.service.native.pickDirectory();
if (directories.length > 0) {
const newLocation = directories[0];
try {
await window.service.wikiGitWorkspace.moveWorkspaceLocation(workspaceID, newLocation);
} catch (error) {
console.error('Failed to move workspace:', error);
}
}
}}
>
{t('EditWorkspace.MoveWorkspace')}
</PictureButton>
</Tooltip>
{isSubWiki && mainWikiToLink && ( {isSubWiki && mainWikiToLink && (
<TextField <TextField
fullWidth fullWidth

@ -1 +1 @@
Subproject commit bb58ff74794ceaeb84d69a359da9361e2e1642ef Subproject commit 046be23ef8603996872c0acf2c012362d0cb394b