mirror of
https://github.com/tiddly-gittly/TidGi-Desktop.git
synced 2025-12-05 18:20:39 -08:00
Fix/hibernate (#652)
* feat: allow use local tiddlywiki version closes #536 * test: hibernate * fix: Ensure wiki worker is started before setting active view for hibernated wikiu * fix: injection
This commit is contained in:
parent
ed198d375b
commit
82bb1c2d77
10 changed files with 284 additions and 10 deletions
73
features/hibernation.feature
Normal file
73
features/hibernation.feature
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
@hibernation
|
||||
Feature: Workspace Hibernation
|
||||
As a user
|
||||
I want to be able to hibernate workspaces
|
||||
So that I can save system resources when workspaces are not in use
|
||||
|
||||
Background:
|
||||
Given I cleanup test wiki so it could create a new one on start
|
||||
And I launch the TidGi application
|
||||
And I wait for the page to load completely
|
||||
Then I should see a "default wiki workspace" element with selector "div[data-testid^='workspace-']:has-text('wiki')"
|
||||
# Create a second wiki workspace programmatically for hibernation testing
|
||||
When I create a new wiki workspace with name "wiki2"
|
||||
And I wait for 1 seconds for "wiki2 workspace icon to appear"
|
||||
Then I should see a "wiki2 workspace" element with selector "div[data-testid^='workspace-']:has-text('wiki2')"
|
||||
|
||||
Scenario: Hibernate both workspaces and verify switching with wake up (issues #556 and #593)
|
||||
# Enable hibernation for both wiki workspaces
|
||||
# Enable for wiki
|
||||
When I open edit workspace window for workspace with name "wiki"
|
||||
And I switch to "editWorkspace" window
|
||||
And I wait for the page to load completely
|
||||
When I click on "misc options accordion and hibernation switch" elements with selectors:
|
||||
| [data-testid='preference-section-miscOptions'] |
|
||||
| [data-testid='hibernate-when-unused-switch'] |
|
||||
When I click on a "save button" element with selector "[data-testid='edit-workspace-save-button']"
|
||||
Then I should not see a "save button" element with selector "[data-testid='edit-workspace-save-button']"
|
||||
Then I switch to "main" window
|
||||
When I close "editWorkspace" window
|
||||
# Enable hibernation for wiki2
|
||||
When I open edit workspace window for workspace with name "wiki2"
|
||||
And I switch to "editWorkspace" window
|
||||
And I wait for the page to load completely
|
||||
When I click on "misc options accordion and hibernation switch" elements with selectors:
|
||||
| [data-testid='preference-section-miscOptions'] |
|
||||
| [data-testid='hibernate-when-unused-switch'] |
|
||||
When I click on a "save button" element with selector "[data-testid='edit-workspace-save-button']"
|
||||
Then I should not see a "save button" element with selector "[data-testid='edit-workspace-save-button']"
|
||||
Then I switch to "main" window
|
||||
When I close "editWorkspace" window
|
||||
# Start with wiki, create a test tiddler to verify workspace content
|
||||
When I click on a "wiki workspace button" element with selector "div[data-testid^='workspace-']:has-text('wiki')"
|
||||
Then the browser view should be loaded and visible
|
||||
# Create a test tiddler in wiki workspace
|
||||
And I click on "add tiddler button" element in browser view with selector "button:has(.tc-image-new-button)"
|
||||
And I click on "title input" element in browser view with selector "div[data-tiddler-title^='Draft of'] input.tc-titlebar.tc-edit-texteditor"
|
||||
And I wait for 0.2 seconds
|
||||
And I press "Control+a" in browser view
|
||||
And I wait for 0.2 seconds
|
||||
And I press "Delete" in browser view
|
||||
And I type "WikiTestTiddler" in "title input" element in browser view with selector "div[data-tiddler-title^='Draft of'] input.tc-titlebar.tc-edit-texteditor"
|
||||
# Confirm to save the tiddler
|
||||
And I click on "confirm button" element in browser view with selector "button:has(.tc-image-done-button)"
|
||||
And I wait for 0.2 seconds
|
||||
Then I should see a "WikiTestTiddler tiddler" element in browser view with selector "div[data-tiddler-title='WikiTestTiddler']"
|
||||
# Switch to wiki2 - wiki should hibernate, wiki2 should load
|
||||
When I click on a "wiki2 workspace button" element with selector "div[data-testid^='workspace-']:has-text('wiki2')"
|
||||
Then the browser view should be loaded and visible
|
||||
# Verify wiki workspace is now hibernated (icon should be grayed out)
|
||||
Then I should see a "wiki workspace hibernated icon" element with selector "div[data-testid^='workspace-']:has-text('wiki')[data-hibernated='true']"
|
||||
# Verify we're in wiki2 by checking Index tiddler (default open) - not WikiTestTiddler
|
||||
Then I should see a "Index tiddler" element in browser view with selector "div[data-tiddler-title='Index']"
|
||||
Then I should not see a "WikiTestTiddler tiddler" element in browser view with selector "div[data-tiddler-title='WikiTestTiddler']"
|
||||
# Switch back to wiki - wiki2 should hibernate, wiki should wake up (reproduces issue #556)
|
||||
# This also tests issue #593 - browser view should persist after wake up
|
||||
When I click on a "wiki workspace button" element with selector "div[data-testid^='workspace-']:has-text('wiki')"
|
||||
Then the browser view should be loaded and visible
|
||||
# Verify wiki2 workspace is now hibernated
|
||||
Then I should see a "wiki2 workspace hibernated icon" element with selector "div[data-testid^='workspace-']:has-text('wiki2')[data-hibernated='true']"
|
||||
# Verify wiki workspace is no longer hibernated
|
||||
Then I should see a "wiki workspace active icon" element with selector "div[data-testid^='workspace-']:has-text('wiki')[data-hibernated='false'][data-active='true']"
|
||||
# Verify WikiTestTiddler is still there after wake up
|
||||
Then I should see a "WikiTestTiddler tiddler" element in browser view with selector "div[data-tiddler-title='WikiTestTiddler']"
|
||||
|
|
@ -4,7 +4,7 @@ import { logsDirectory, screenshotsDirectory } from '../supports/paths';
|
|||
import { clearAISettings } from './agent';
|
||||
import { ApplicationWorld } from './application';
|
||||
import { clearTidgiMiniWindowSettings } from './tidgiMiniWindow';
|
||||
import { clearGitTestData, clearSubWikiRoutingTestData } from './wiki';
|
||||
import { clearGitTestData, clearHibernationTestData, clearSubWikiRoutingTestData } from './wiki';
|
||||
|
||||
Before(async function(this: ApplicationWorld, { pickle }) {
|
||||
// Create necessary directories under userData-test/logs to match appPaths in dev/test
|
||||
|
|
@ -67,6 +67,10 @@ After(async function(this: ApplicationWorld, { pickle }) {
|
|||
if (pickle.tags.some((tag) => tag.name === '@git')) {
|
||||
await clearGitTestData();
|
||||
}
|
||||
// Clean up hibernation test data - remove wiki2 folder created during tests
|
||||
if (pickle.tags.some((tag) => tag.name === '@hibernation')) {
|
||||
await clearHibernationTestData();
|
||||
}
|
||||
|
||||
// Separate logs by test scenario for easier debugging
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -466,4 +466,155 @@ When('I modify file {string} to add field {string}', async function(this: Applic
|
|||
await fs.writeFile(actualPath, lines.join('\n'), 'utf-8');
|
||||
});
|
||||
|
||||
export { clearGitTestData, clearSubWikiRoutingTestData };
|
||||
When('I open edit workspace window for workspace with name {string}', async function(this: ApplicationWorld, workspaceName: string) {
|
||||
if (!this.app) {
|
||||
throw new Error('Application is not available');
|
||||
}
|
||||
|
||||
// Read settings file to get workspace info
|
||||
const settings = await fs.readJson(settingsPath) as { workspaces?: Record<string, IWorkspace> };
|
||||
const workspaces: Record<string, IWorkspace> = settings.workspaces ?? {};
|
||||
|
||||
// Find workspace by name
|
||||
let targetWorkspaceId: string | undefined;
|
||||
for (const [id, workspace] of Object.entries(workspaces)) {
|
||||
if (!workspace.pageType && workspace.name === workspaceName) {
|
||||
targetWorkspaceId = id;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!targetWorkspaceId) {
|
||||
throw new Error(`No workspace found with name: ${workspaceName}`);
|
||||
}
|
||||
|
||||
// Call window service through main window's webContents to open edit workspace window
|
||||
await this.app.evaluate(async ({ BrowserWindow }, workspaceId: string) => {
|
||||
const windows = BrowserWindow.getAllWindows();
|
||||
const mainWindow = windows.find(win => !win.isDestroyed() && win.webContents && win.webContents.getURL().includes('index.html'));
|
||||
|
||||
if (!mainWindow) {
|
||||
throw new Error('Main window not found');
|
||||
}
|
||||
|
||||
// Call the window service to open edit workspace window
|
||||
// Safely pass workspaceId using JSON serialization to avoid string interpolation vulnerability
|
||||
await mainWindow.webContents.executeJavaScript(`
|
||||
(async () => {
|
||||
await window.service.window.open('editWorkspace', { workspaceID: ${JSON.stringify(workspaceId)} });
|
||||
})();
|
||||
`);
|
||||
}, targetWorkspaceId);
|
||||
|
||||
// Wait for the edit workspace window to appear
|
||||
const success = await this.waitForWindowCondition(
|
||||
'editWorkspace',
|
||||
(window) => window !== undefined && !window.isClosed(),
|
||||
);
|
||||
|
||||
if (!success) {
|
||||
throw new Error('Edit workspace window did not appear after opening');
|
||||
}
|
||||
});
|
||||
|
||||
When('I create a new wiki workspace with name {string}', async function(this: ApplicationWorld, workspaceName: string) {
|
||||
if (!this.app) {
|
||||
throw new Error('Application is not available');
|
||||
}
|
||||
|
||||
// Construct the full wiki path
|
||||
const wikiPath = path.join(wikiTestRootPath, workspaceName);
|
||||
|
||||
// Create the wiki folder using the template
|
||||
const templatePath = path.join(process.cwd(), 'template', 'wiki');
|
||||
await fs.copy(templatePath, wikiPath);
|
||||
|
||||
// Remove the copied .git directory from the template to start fresh
|
||||
const gitPath = path.join(wikiPath, '.git');
|
||||
await fs.remove(gitPath).catch(() => {
|
||||
// Ignore if .git doesn't exist
|
||||
});
|
||||
|
||||
// Initialize fresh git repository for the new wiki
|
||||
const { execSync } = await import('child_process');
|
||||
try {
|
||||
execSync('git init', { cwd: wikiPath });
|
||||
execSync('git config user.email "test@tidgi.test"', { cwd: wikiPath });
|
||||
execSync('git config user.name "TidGi Test"', { cwd: wikiPath });
|
||||
execSync('git add .', { cwd: wikiPath });
|
||||
execSync('git commit -m "Initial commit"', { cwd: wikiPath });
|
||||
} catch (error) {
|
||||
// Git initialization is not critical for the test, continue anyway
|
||||
console.log('Git initialization skipped:', (error as Error).message);
|
||||
}
|
||||
|
||||
// Now create workspace configuration
|
||||
await this.app.evaluate(async ({ BrowserWindow }, { wikiName, wikiFullPath }: { wikiName: string; wikiFullPath: string }) => {
|
||||
const windows = BrowserWindow.getAllWindows();
|
||||
const mainWindow = windows.find(win => !win.isDestroyed() && win.webContents && win.webContents.getURL().includes('index.html'));
|
||||
|
||||
if (!mainWindow) {
|
||||
throw new Error('Main window not found');
|
||||
}
|
||||
|
||||
// Call workspace service to create new workspace
|
||||
// Safely pass parameters using JSON serialization to avoid string interpolation vulnerability
|
||||
await mainWindow.webContents.executeJavaScript(`
|
||||
(async () => {
|
||||
await window.service.workspace.create({
|
||||
name: ${JSON.stringify(wikiName)},
|
||||
wikiFolderLocation: ${JSON.stringify(wikiFullPath)},
|
||||
isSubWiki: false,
|
||||
storageService: 'local',
|
||||
});
|
||||
})();
|
||||
`);
|
||||
}, { wikiName: workspaceName, wikiFullPath: wikiPath });
|
||||
|
||||
// Wait for workspace to appear in UI
|
||||
await this.app.evaluate(async () => {
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Clean up hibernation test data - remove wiki2 folder and its workspace config
|
||||
*/
|
||||
async function clearHibernationTestData() {
|
||||
const wiki2Path = path.join(wikiTestRootPath, 'wiki2');
|
||||
|
||||
// Remove wiki2 folder
|
||||
if (await fs.pathExists(wiki2Path)) {
|
||||
try {
|
||||
await fs.remove(wiki2Path);
|
||||
} catch (error) {
|
||||
console.warn('Failed to remove wiki2 folder in hibernation cleanup:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove wiki2 workspace config from settings.json
|
||||
const settingsPath = path.join(process.cwd(), 'userData-test', 'settings', 'settings.json');
|
||||
if (await fs.pathExists(settingsPath)) {
|
||||
try {
|
||||
type SettingsFile = { workspaces?: Record<string, IWorkspace> } & Record<string, unknown>;
|
||||
const settings = await fs.readJson(settingsPath) as SettingsFile;
|
||||
if (settings.workspaces) {
|
||||
// Find and remove wiki2 workspace by folder location
|
||||
const wiki2WorkspaceId = Object.keys(settings.workspaces).find(id => {
|
||||
const workspace = settings.workspaces?.[id];
|
||||
return workspace && 'wikiFolderLocation' in workspace && workspace.wikiFolderLocation === wiki2Path;
|
||||
});
|
||||
|
||||
if (wiki2WorkspaceId && settings.workspaces) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
|
||||
delete settings.workspaces[wiki2WorkspaceId];
|
||||
await fs.writeJson(settingsPath, settings, { spaces: 2 });
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Failed to remove wiki2 workspace config in hibernation cleanup:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export { clearGitTestData, clearHibernationTestData, clearSubWikiRoutingTestData };
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { existsSync } from 'fs';
|
||||
import os from 'os';
|
||||
import path from 'path';
|
||||
import { isMac } from '../helpers/system';
|
||||
|
|
@ -48,6 +49,26 @@ export const ZX_FOLDER = path.resolve(PACKAGE_PATH_BASE, 'zx', 'build', 'cli.js'
|
|||
export const TIDDLYWIKI_PACKAGE_FOLDER = path.resolve(PACKAGE_PATH_BASE, 'tiddlywiki', 'boot');
|
||||
export const SQLITE_BINARY_PATH = path.resolve(PACKAGE_PATH_BASE, 'better-sqlite3', 'build', 'Release', 'better_sqlite3.node');
|
||||
|
||||
/**
|
||||
* Check if a wiki folder has its own TiddlyWiki installation and return the appropriate boot path.
|
||||
* Prefers wiki-folder-local installation over the built-in version to support custom TW versions.
|
||||
*
|
||||
* @param wikiFolderLocation - The path to the wiki folder
|
||||
* @returns The path to TiddlyWiki boot folder (local if exists, otherwise built-in)
|
||||
*/
|
||||
export function getTiddlyWikiBootPath(wikiFolderLocation: string): string {
|
||||
const localTiddlyWikiBootPath = path.resolve(wikiFolderLocation, 'node_modules', 'tiddlywiki', 'boot');
|
||||
try {
|
||||
// Check if local TiddlyWiki exists synchronously since this is a critical path
|
||||
if (existsSync(localTiddlyWikiBootPath)) {
|
||||
return localTiddlyWikiBootPath;
|
||||
}
|
||||
} catch {
|
||||
// Fall through to use built-in version if check fails
|
||||
}
|
||||
return TIDDLYWIKI_PACKAGE_FOLDER;
|
||||
}
|
||||
|
||||
// Localization folder
|
||||
export const LOCALIZATION_FOLDER = isPackaged
|
||||
? path.resolve(process.resourcesPath, localizationFolderName) // Packaged: resources/localization
|
||||
|
|
|
|||
|
|
@ -191,6 +191,7 @@ export function WorkspaceSelectorBase({
|
|||
onClick={workspaceClickedLoading ? () => {} : onClick}
|
||||
data-testid={pageType ? `workspace-${pageType}` : `workspace-${id}`}
|
||||
data-active={active ? 'true' : 'false'}
|
||||
data-hibernated={hibernated ? 'true' : 'false'}
|
||||
>
|
||||
<Badge color='secondary' badgeContent={badgeCount} max={99}>
|
||||
{icon}
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ import WikiWorkerFactory from './wikiWorker/index?nodeWorker';
|
|||
import { container } from '@services/container';
|
||||
|
||||
import { WikiChannel } from '@/constants/channels';
|
||||
import { TIDDLERS_PATH, TIDDLYWIKI_PACKAGE_FOLDER, TIDDLYWIKI_TEMPLATE_FOLDER_PATH } from '@/constants/paths';
|
||||
import { getTiddlyWikiBootPath, TIDDLERS_PATH, TIDDLYWIKI_TEMPLATE_FOLDER_PATH } from '@/constants/paths';
|
||||
import type { IAuthenticationService } from '@services/auth/interface';
|
||||
import type { IGitService, IGitUserInfos } from '@services/git/interface';
|
||||
import { i18n } from '@services/libs/i18n';
|
||||
|
|
@ -136,7 +136,7 @@ export class Wiki implements IWikiService {
|
|||
const shouldUseDarkColors = await this.themeService.shouldUseDarkColors();
|
||||
const workerData: IStartNodeJSWikiConfigs = {
|
||||
authToken,
|
||||
constants: { TIDDLYWIKI_PACKAGE_FOLDER },
|
||||
constants: { TIDDLYWIKI_PACKAGE_FOLDER: getTiddlyWikiBootPath(wikiFolderLocation) },
|
||||
enableHTTPAPI,
|
||||
excludedPlugins,
|
||||
homePath: wikiFolderLocation,
|
||||
|
|
@ -352,7 +352,7 @@ export class Wiki implements IWikiService {
|
|||
if (await exists(saveWikiFolderPath)) {
|
||||
throw new AlreadyExistError(saveWikiFolderPath);
|
||||
}
|
||||
await worker.extractWikiHTML(htmlWikiPath, saveWikiFolderPath, { TIDDLYWIKI_PACKAGE_FOLDER });
|
||||
await worker.extractWikiHTML(htmlWikiPath, saveWikiFolderPath, { TIDDLYWIKI_PACKAGE_FOLDER: getTiddlyWikiBootPath(saveWikiFolderPath) });
|
||||
} catch (error) {
|
||||
const result = `${(error as Error).name} ${(error as Error).message}`;
|
||||
logger.error(result, { worker: 'NodeJSWiki', method: 'extractWikiHTML', htmlWikiPath, saveWikiFolderPath });
|
||||
|
|
@ -369,7 +369,7 @@ export class Wiki implements IWikiService {
|
|||
const worker = createWorkerProxy<WikiWorker>(nativeWorker);
|
||||
|
||||
try {
|
||||
await worker.packetHTMLFromWikiFolder(wikiFolderLocation, pathOfNewHTML, { TIDDLYWIKI_PACKAGE_FOLDER });
|
||||
await worker.packetHTMLFromWikiFolder(wikiFolderLocation, pathOfNewHTML, { TIDDLYWIKI_PACKAGE_FOLDER: getTiddlyWikiBootPath(wikiFolderLocation) });
|
||||
} finally {
|
||||
// this worker is only for one time use. we will spawn a new one for starting wiki later.
|
||||
await terminateWorker(nativeWorker);
|
||||
|
|
|
|||
|
|
@ -74,6 +74,14 @@ export function startNodeJSWiki({
|
|||
observer.next({ type: 'control', actions: WikiControlActions.start, argv: fullBootArgv });
|
||||
|
||||
try {
|
||||
// Log which TiddlyWiki version is being used (local vs built-in)
|
||||
const isUsingLocalTiddlyWiki = TIDDLYWIKI_PACKAGE_FOLDER.includes(path.join(homePath, 'node_modules'));
|
||||
void native.logFor(
|
||||
workspace.name,
|
||||
'info',
|
||||
`Starting TiddlyWiki from ${isUsingLocalTiddlyWiki ? 'wiki-local installation' : 'built-in installation'}: ${TIDDLYWIKI_PACKAGE_FOLDER}`,
|
||||
);
|
||||
|
||||
const wikiInstance = TiddlyWiki();
|
||||
setWikiInstance(wikiInstance);
|
||||
process.env.TIDDLYWIKI_PLUGIN_PATH = path.resolve(homePath, 'plugins');
|
||||
|
|
|
|||
|
|
@ -294,13 +294,16 @@ export class WorkspaceView implements IWorkspaceViewService {
|
|||
public async wakeUpWorkspaceView(workspaceID: string): Promise<void> {
|
||||
const workspace = await container.get<IWorkspaceService>(serviceIdentifier.Workspace).get(workspaceID);
|
||||
if (workspace !== undefined) {
|
||||
// First, update workspace state and start wiki server
|
||||
await Promise.all([
|
||||
container.get<IWorkspaceService>(serviceIdentifier.Workspace).update(workspaceID, {
|
||||
hibernated: false,
|
||||
}),
|
||||
this.authService.getUserName(workspace).then(userName => container.get<IWikiService>(serviceIdentifier.Wiki).startWiki(workspaceID, userName)),
|
||||
this.addViewForAllBrowserViews(workspace),
|
||||
]);
|
||||
|
||||
// Then add view after wiki server is ready and workspace is marked as not hibernated
|
||||
await this.addViewForAllBrowserViews(workspace);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -366,6 +369,17 @@ export class WorkspaceView implements IWorkspaceViewService {
|
|||
if (isWikiWorkspace(newWorkspace) && newWorkspace.hibernated) {
|
||||
await this.wakeUpWorkspaceView(nextWorkspaceID);
|
||||
}
|
||||
|
||||
// fix #556 and #593: Ensure wiki worker is started before setting active view. When switching to a wiki workspace that doesn't have a view yet, the view service will create one and immediately try to loadURL. If the wiki worker hasn't started, loadURL will hang forever waiting for the IPC server that never comes online. This must happen before `setActiveViewForAllBrowserViews` to ensure the worker is ready when view is created.
|
||||
if (isWikiWorkspace(newWorkspace) && !newWorkspace.hibernated) {
|
||||
const wikiService = container.get<IWikiService>(serviceIdentifier.Wiki);
|
||||
const worker = wikiService.getWorker(nextWorkspaceID);
|
||||
if (worker === undefined) {
|
||||
const userName = await this.authService.getUserName(newWorkspace);
|
||||
await wikiService.startWiki(nextWorkspaceID, userName);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await container.get<IViewService>(serviceIdentifier.View).setActiveViewForAllBrowserViews(nextWorkspaceID);
|
||||
await this.realignActiveWorkspace(nextWorkspaceID);
|
||||
|
|
@ -377,6 +391,7 @@ export class WorkspaceView implements IWorkspaceViewService {
|
|||
throw error;
|
||||
}
|
||||
// if we are switching to a new workspace, we hide and/or hibernate old view, and activate new view
|
||||
// This must happen after view setup succeeds to avoid issues with workspace that hasn't started yet
|
||||
if (oldActiveWorkspace !== undefined && oldActiveWorkspace.id !== nextWorkspaceID) {
|
||||
await this.hideWorkspaceView(oldActiveWorkspace.id);
|
||||
if (isWikiWorkspace(oldActiveWorkspace) && oldActiveWorkspace.hibernateWhenUnused) {
|
||||
|
|
|
|||
|
|
@ -69,7 +69,7 @@ export function ExistedWikiForm({
|
|||
// Update local state immediately for responsive UI
|
||||
const newValue = event.target.value;
|
||||
setFullPath(newValue);
|
||||
|
||||
|
||||
// Parse path into parent and folder for validation
|
||||
const lastSlashIndex = Math.max(newValue.lastIndexOf('/'), newValue.lastIndexOf('\\'));
|
||||
if (lastSlashIndex >= 0) {
|
||||
|
|
|
|||
|
|
@ -395,7 +395,7 @@ export default function EditWorkspace(): React.JSX.Element {
|
|||
</OptionsAccordion>
|
||||
<OptionsAccordion>
|
||||
<Tooltip title={t('EditWorkspace.ClickToExpand')}>
|
||||
<OptionsAccordionSummary expandIcon={<ExpandMoreIcon />}>
|
||||
<OptionsAccordionSummary expandIcon={<ExpandMoreIcon />} data-testid='preference-section-miscOptions'>
|
||||
{t('EditWorkspace.MiscOptions')}
|
||||
</OptionsAccordionSummary>
|
||||
</Tooltip>
|
||||
|
|
@ -410,6 +410,7 @@ export default function EditWorkspace(): React.JSX.Element {
|
|||
edge='end'
|
||||
color='primary'
|
||||
checked={hibernateWhenUnused}
|
||||
data-testid='hibernate-when-unused-switch'
|
||||
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
workspaceSetter({ ...workspace, hibernateWhenUnused: event.target.checked });
|
||||
}}
|
||||
|
|
@ -489,7 +490,7 @@ export default function EditWorkspace(): React.JSX.Element {
|
|||
</FlexGrow>
|
||||
{!isEqual(omit(workspace, nonConfigFields), omit(originalWorkspace, nonConfigFields)) && (
|
||||
<SaveCancelButtonsContainer>
|
||||
<Button color='primary' variant='contained' disableElevation onClick={onSave}>
|
||||
<Button color='primary' variant='contained' disableElevation onClick={onSave} data-testid='edit-workspace-save-button'>
|
||||
{t('EditWorkspace.Save')}
|
||||
</Button>
|
||||
<Button variant='contained' disableElevation onClick={() => void window.remote.closeCurrentWindow()}>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue