mirror of
https://github.com/tiddly-gittly/TidGi-Desktop.git
synced 2025-12-05 18:20:39 -08:00
Fix/menu sync (#659)
* fix: sync not config remote * fix: git worker no log * Update src/services/git/registerMenu.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update menuItems.ts --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
parent
d242d9a63c
commit
b4ebaa66df
8 changed files with 353 additions and 10 deletions
128
features/stepDefinitions/sync.ts
Normal file
128
features/stepDefinitions/sync.ts
Normal file
|
|
@ -0,0 +1,128 @@
|
||||||
|
import { Then, When } from '@cucumber/cucumber';
|
||||||
|
import { exec as gitExec } from 'dugite';
|
||||||
|
import fs from 'fs-extra';
|
||||||
|
import path from 'path';
|
||||||
|
import { wikiTestRootPath } from '../supports/paths';
|
||||||
|
import type { ApplicationWorld } from './application';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a bare git repository to use as a local remote for testing sync
|
||||||
|
*/
|
||||||
|
When('I create a bare git repository at {string}', async function(this: ApplicationWorld, repoPath: string) {
|
||||||
|
const actualPath = repoPath.replace('{tmpDir}', wikiTestRootPath);
|
||||||
|
|
||||||
|
// Remove if exists
|
||||||
|
if (await fs.pathExists(actualPath)) {
|
||||||
|
await fs.remove(actualPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create bare repository
|
||||||
|
await fs.ensureDir(actualPath);
|
||||||
|
await gitExec(['init', '--bare'], actualPath);
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify that a commit with specific message exists in remote repository
|
||||||
|
*/
|
||||||
|
Then('the remote repository {string} should contain commit with message {string}', async function(this: ApplicationWorld, remotePath: string, commitMessage: string) {
|
||||||
|
const actualRemotePath = remotePath.replace('{tmpDir}', wikiTestRootPath);
|
||||||
|
|
||||||
|
// Clone the remote to a temporary location to inspect it
|
||||||
|
const temporaryClonePath = path.join(wikiTestRootPath, `temp-clone-${Date.now()}`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await gitExec(['clone', actualRemotePath, temporaryClonePath], wikiTestRootPath);
|
||||||
|
|
||||||
|
// Check all branches for the commit message
|
||||||
|
const branchResult = await gitExec(['branch', '-a'], temporaryClonePath);
|
||||||
|
if (branchResult.exitCode !== 0) {
|
||||||
|
throw new Error(`Failed to list branches: ${branchResult.stderr}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to find commits in any branch
|
||||||
|
let foundCommit = false;
|
||||||
|
const branches = branchResult.stdout.split('\n').filter(b => b.trim());
|
||||||
|
|
||||||
|
for (const branch of branches) {
|
||||||
|
const branchName = branch.trim().replace('* ', '').replace('remotes/origin/', '');
|
||||||
|
if (!branchName) continue;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Checkout the branch
|
||||||
|
await gitExec(['checkout', branchName], temporaryClonePath);
|
||||||
|
|
||||||
|
// Get commit log
|
||||||
|
const result = await gitExec(['log', '--oneline', '-10'], temporaryClonePath);
|
||||||
|
if (result.exitCode === 0 && result.stdout.includes(commitMessage)) {
|
||||||
|
foundCommit = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Branch might not exist or be checkable, continue to next
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!foundCommit) {
|
||||||
|
// Get all logs from all branches for error message
|
||||||
|
const allLogsResult = await gitExec(['log', '--all', '--oneline', '-20'], temporaryClonePath);
|
||||||
|
throw new Error(`Commit with message "${commitMessage}" not found in any branch. Available commits:\n${allLogsResult.stdout}\n\nBranches:\n${branchResult.stdout}`);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
// Clean up temporary clone
|
||||||
|
if (await fs.pathExists(temporaryClonePath)) {
|
||||||
|
await fs.remove(temporaryClonePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify that a file exists in remote repository
|
||||||
|
*/
|
||||||
|
Then('the remote repository {string} should contain file {string}', async function(this: ApplicationWorld, remotePath: string, filePath: string) {
|
||||||
|
const actualRemotePath = remotePath.replace('{tmpDir}', wikiTestRootPath);
|
||||||
|
|
||||||
|
// Clone the remote to a temporary location to inspect it
|
||||||
|
const temporaryClonePath = path.join(wikiTestRootPath, `temp-clone-${Date.now()}`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await gitExec(['clone', actualRemotePath, temporaryClonePath], wikiTestRootPath);
|
||||||
|
|
||||||
|
// Check all branches for the file
|
||||||
|
const branchResult = await gitExec(['branch', '-a'], temporaryClonePath);
|
||||||
|
if (branchResult.exitCode !== 0) {
|
||||||
|
throw new Error(`Failed to list branches: ${branchResult.stderr}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
let foundFile = false;
|
||||||
|
const branches = branchResult.stdout.split('\n').filter(b => b.trim());
|
||||||
|
|
||||||
|
for (const branch of branches) {
|
||||||
|
const branchName = branch.trim().replace('* ', '').replace('remotes/origin/', '');
|
||||||
|
if (!branchName) continue;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Checkout the branch
|
||||||
|
await gitExec(['checkout', branchName], temporaryClonePath);
|
||||||
|
|
||||||
|
const fileFullPath = path.join(temporaryClonePath, filePath);
|
||||||
|
if (await fs.pathExists(fileFullPath)) {
|
||||||
|
foundFile = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Branch might not exist or be checkable, continue to next
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!foundFile) {
|
||||||
|
throw new Error(`File "${filePath}" not found in any branch of remote repository`);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
// Clean up temporary clone
|
||||||
|
if (await fs.pathExists(temporaryClonePath)) {
|
||||||
|
await fs.remove(temporaryClonePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { DataTable, Then, When } from '@cucumber/cucumber';
|
import { DataTable, Then, When } from '@cucumber/cucumber';
|
||||||
|
import { wikiTestRootPath } from '../supports/paths';
|
||||||
import type { ApplicationWorld } from './application';
|
import type { ApplicationWorld } from './application';
|
||||||
|
|
||||||
When('I wait for {float} seconds', async function(seconds: number) {
|
When('I wait for {float} seconds', async function(seconds: number) {
|
||||||
|
|
@ -243,15 +244,54 @@ When('I type {string} in {string} element with selector {string}', async functio
|
||||||
throw new Error('No current window is available');
|
throw new Error('No current window is available');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Replace {tmpDir} placeholder with actual test root path
|
||||||
|
const actualText = text.replace('{tmpDir}', wikiTestRootPath);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await currentWindow.waitForSelector(selector, { timeout: 10000 });
|
await currentWindow.waitForSelector(selector, { timeout: 10000 });
|
||||||
const element = currentWindow.locator(selector);
|
const element = currentWindow.locator(selector);
|
||||||
await element.fill(text);
|
await element.fill(actualText);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new Error(`Failed to type in ${elementComment} element with selector "${selector}": ${error as Error}`);
|
throw new Error(`Failed to type in ${elementComment} element with selector "${selector}": ${error as Error}`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
When('I type in {string} elements with selectors:', async function(this: ApplicationWorld, elementDescriptions: string, dataTable: DataTable) {
|
||||||
|
const currentWindow = this.currentWindow;
|
||||||
|
if (!currentWindow) {
|
||||||
|
throw new Error('No current window is available');
|
||||||
|
}
|
||||||
|
|
||||||
|
const descriptions = elementDescriptions.split(' and ').map(d => d.trim());
|
||||||
|
const rows = dataTable.raw();
|
||||||
|
const errors: string[] = [];
|
||||||
|
|
||||||
|
if (descriptions.length !== rows.length) {
|
||||||
|
throw new Error(`Mismatch: ${descriptions.length} element descriptions but ${rows.length} text/selector pairs provided`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type in elements sequentially to maintain order
|
||||||
|
for (let index = 0; index < rows.length; index++) {
|
||||||
|
const [text, selector] = rows[index];
|
||||||
|
const elementComment = descriptions[index];
|
||||||
|
|
||||||
|
// Replace {tmpDir} placeholder with actual test root path
|
||||||
|
const actualText = text.replace('{tmpDir}', wikiTestRootPath);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await currentWindow.waitForSelector(selector, { timeout: 10000 });
|
||||||
|
const element = currentWindow.locator(selector);
|
||||||
|
await element.fill(actualText);
|
||||||
|
} catch (error) {
|
||||||
|
errors.push(`Failed to type in "${elementComment}" with selector "${selector}": ${error as Error}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (errors.length > 0) {
|
||||||
|
throw new Error(`Failed to type in some elements:\n${errors.join('\n')}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
When('I clear text in {string} element with selector {string}', async function(this: ApplicationWorld, elementComment: string, selector: string) {
|
When('I clear text in {string} element with selector {string}', async function(this: ApplicationWorld, elementComment: string, selector: string) {
|
||||||
const currentWindow = this.currentWindow;
|
const currentWindow = this.currentWindow;
|
||||||
if (!currentWindow) {
|
if (!currentWindow) {
|
||||||
|
|
|
||||||
55
features/sync.feature
Normal file
55
features/sync.feature
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
Feature: Git Sync
|
||||||
|
As a user
|
||||||
|
I want to sync my wiki to a remote repository
|
||||||
|
So that I can backup and share my content
|
||||||
|
|
||||||
|
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')"
|
||||||
|
Then the browser view should be loaded and visible
|
||||||
|
And I wait for SSE and watch-fs to be ready
|
||||||
|
And I wait for "git initialization" log marker "[test-id-git-init-complete]"
|
||||||
|
|
||||||
|
@git @sync
|
||||||
|
Scenario: Sync to local remote repository via application menu (commit and push)
|
||||||
|
# Setup a bare git repository as local remote
|
||||||
|
When I create a bare git repository at "{tmpDir}/remote-repo-menu.git"
|
||||||
|
# Configure sync via edit workspace window
|
||||||
|
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 "saveAndSyncOptions accordion and syncToCloud toggle" elements with selectors:
|
||||||
|
| [data-testid='preference-section-saveAndSyncOptions'] |
|
||||||
|
| [data-testid='synced-local-workspace-switch'] |
|
||||||
|
And I wait for 1 seconds
|
||||||
|
When I type in "git url input and github username input and github email input and github token input" elements with selectors:
|
||||||
|
| {tmpDir}/remote-repo-menu.git | label:has-text('Git仓库线上网址') + * input, label:has-text('Git Repo URL') + * input, input[aria-label='Git仓库线上网址'], input[aria-label='Git Repo URL'] |
|
||||||
|
| test-user | [data-testid='github-userName-input'] input |
|
||||||
|
| test@tidgi.test | [data-testid='github-email-input'] input |
|
||||||
|
| test-token | [data-testid='github-token-input'] input |
|
||||||
|
When I click on a "save workspace button" element with selector "[data-testid='edit-workspace-save-button']"
|
||||||
|
# Wait for workspace to be saved (workspace.update triggers a restart which takes time)
|
||||||
|
And I wait for 5 seconds
|
||||||
|
When I switch to "main" window
|
||||||
|
# Create a new tiddler to trigger sync
|
||||||
|
When I create file "{tmpDir}/wiki/tiddlers/SyncMenuTestTiddler.tid" with content:
|
||||||
|
"""
|
||||||
|
created: 20250226090000000
|
||||||
|
modified: 20250226090000000
|
||||||
|
title: SyncMenuTestTiddler
|
||||||
|
tags: SyncTest
|
||||||
|
|
||||||
|
This is a test tiddler for sync via menu feature.
|
||||||
|
"""
|
||||||
|
Then I wait for tiddler "SyncMenuTestTiddler" to be added by watch-fs
|
||||||
|
# Clear previous test markers to ensure we're testing fresh sync operation
|
||||||
|
When I clear test-id markers from logs
|
||||||
|
# Use application menu to sync (commit and push)
|
||||||
|
When I click menu "知识库 > 立即同步云端"
|
||||||
|
# Wait for git sync to complete (not just commit)
|
||||||
|
Then I wait for "git sync completed" log marker "[test-id-git-sync-complete]"
|
||||||
|
# Verify the commit was pushed to remote by cloning the remote and checking
|
||||||
|
Then the remote repository "{tmpDir}/remote-repo-menu.git" should contain commit with message "使用太记桌面版备份"
|
||||||
|
And the remote repository "{tmpDir}/remote-repo-menu.git" should contain file "tiddlers/SyncMenuTestTiddler.tid"
|
||||||
|
|
@ -326,17 +326,35 @@ export class Git implements IGitService {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
observable.subscribe(this.getWorkerMessageObserver(wikiFolderPath, () => {}, reject, workspaceID));
|
|
||||||
let hasChanges = false;
|
let hasChanges = false;
|
||||||
observable.subscribe({
|
observable.subscribe({
|
||||||
next: (messageObject: IGitLogMessage) => {
|
next: (messageObject: IGitLogMessage) => {
|
||||||
|
// Log the message
|
||||||
if (messageObject.level === 'error') {
|
if (messageObject.level === 'error') {
|
||||||
|
const errorMessage = (messageObject.error).message;
|
||||||
|
// if workspace exists, show notification in workspace, else use dialog instead
|
||||||
|
if (workspaceID === undefined) {
|
||||||
|
this.createFailedDialog(errorMessage, wikiFolderPath);
|
||||||
|
} else {
|
||||||
|
this.createFailedNotification(errorMessage, workspaceID);
|
||||||
|
}
|
||||||
|
// Reject the promise on error to prevent service restart
|
||||||
|
reject(messageObject.error);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const { meta } = messageObject;
|
const { message, meta, level } = messageObject;
|
||||||
if (typeof meta === 'object' && meta !== null && 'step' in meta && stepsAboutChange.includes((meta as { step: GitStep }).step)) {
|
if (typeof meta === 'object' && meta !== null && 'step' in meta) {
|
||||||
hasChanges = true;
|
this.popGitErrorNotificationToUser((meta as { step: GitStep }).step, message);
|
||||||
|
// Check if this step indicates changes
|
||||||
|
if (stepsAboutChange.includes((meta as { step: GitStep }).step)) {
|
||||||
|
hasChanges = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
logger.log(level, translateMessage(message), meta);
|
||||||
|
},
|
||||||
|
error: (error) => {
|
||||||
|
// this normally won't happen. And will become unhandled error. Because Observable error can't be catch, don't know why.
|
||||||
|
reject(error as Error);
|
||||||
},
|
},
|
||||||
complete: () => {
|
complete: () => {
|
||||||
resolve(hasChanges);
|
resolve(hasChanges);
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
* Utility functions for creating Git-related menu items
|
* Utility functions for creating Git-related menu items
|
||||||
* This file is safe to import from both frontend and backend code
|
* This file is safe to import from both frontend and backend code
|
||||||
*/
|
*/
|
||||||
import type { IGitService } from '@services/git/interface';
|
import type { IGitService, IGitUserInfos } from '@services/git/interface';
|
||||||
import { DeferredMenuItemConstructorOptions } from '@services/menu/interface';
|
import { DeferredMenuItemConstructorOptions } from '@services/menu/interface';
|
||||||
import { IWindowService } from '@services/windows/interface';
|
import { IWindowService } from '@services/windows/interface';
|
||||||
import { WindowNames } from '@services/windows/WindowProperties';
|
import { WindowNames } from '@services/windows/WindowProperties';
|
||||||
|
|
@ -118,6 +118,7 @@ export function createBackupMenuItems(
|
||||||
* @param gitService Git service instance (or Pick with commitAndSync)
|
* @param gitService Git service instance (or Pick with commitAndSync)
|
||||||
* @param aiEnabled Whether AI-generated commit messages are enabled
|
* @param aiEnabled Whether AI-generated commit messages are enabled
|
||||||
* @param isOnline Whether the network is online (optional, defaults to true)
|
* @param isOnline Whether the network is online (optional, defaults to true)
|
||||||
|
* @param userInfo User authentication info for git operations
|
||||||
* @returns Array of menu items
|
* @returns Array of menu items
|
||||||
*/
|
*/
|
||||||
export function createSyncMenuItems(
|
export function createSyncMenuItems(
|
||||||
|
|
@ -125,7 +126,8 @@ export function createSyncMenuItems(
|
||||||
t: TFunction,
|
t: TFunction,
|
||||||
gitService: Pick<IGitService, 'commitAndSync'>,
|
gitService: Pick<IGitService, 'commitAndSync'>,
|
||||||
aiEnabled: boolean,
|
aiEnabled: boolean,
|
||||||
isOnline?: boolean,
|
isOnline: boolean,
|
||||||
|
userInfo: IGitUserInfos | undefined,
|
||||||
): DeferredMenuItemConstructorOptions[];
|
): DeferredMenuItemConstructorOptions[];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -135,6 +137,7 @@ export function createSyncMenuItems(
|
||||||
* @param gitService Git service instance (or Pick with commitAndSync)
|
* @param gitService Git service instance (or Pick with commitAndSync)
|
||||||
* @param aiEnabled Whether AI-generated commit messages are enabled
|
* @param aiEnabled Whether AI-generated commit messages are enabled
|
||||||
* @param isOnline Whether the network is online (optional, defaults to true)
|
* @param isOnline Whether the network is online (optional, defaults to true)
|
||||||
|
* @param userInfo User authentication info for git operations
|
||||||
* @param useDeferred Set to false for context menu
|
* @param useDeferred Set to false for context menu
|
||||||
* @returns Array of menu items
|
* @returns Array of menu items
|
||||||
*/
|
*/
|
||||||
|
|
@ -144,6 +147,7 @@ export function createSyncMenuItems(
|
||||||
gitService: Pick<IGitService, 'commitAndSync'>,
|
gitService: Pick<IGitService, 'commitAndSync'>,
|
||||||
aiEnabled: boolean,
|
aiEnabled: boolean,
|
||||||
isOnline: boolean,
|
isOnline: boolean,
|
||||||
|
userInfo: IGitUserInfos | undefined,
|
||||||
useDeferred: false,
|
useDeferred: false,
|
||||||
): import('electron').MenuItemConstructorOptions[];
|
): import('electron').MenuItemConstructorOptions[];
|
||||||
|
|
||||||
|
|
@ -152,14 +156,15 @@ export function createSyncMenuItems(
|
||||||
t: TFunction,
|
t: TFunction,
|
||||||
gitService: Pick<IGitService, 'commitAndSync'>,
|
gitService: Pick<IGitService, 'commitAndSync'>,
|
||||||
aiEnabled: boolean,
|
aiEnabled: boolean,
|
||||||
isOnline: boolean = true,
|
isOnline: boolean,
|
||||||
|
userInfo: IGitUserInfos | undefined,
|
||||||
_useDeferred: boolean = true,
|
_useDeferred: boolean = true,
|
||||||
): DeferredMenuItemConstructorOptions[] | import('electron').MenuItemConstructorOptions[] {
|
): DeferredMenuItemConstructorOptions[] | import('electron').MenuItemConstructorOptions[] {
|
||||||
if (!isWikiWorkspace(workspace) || !workspace.gitUrl) {
|
if (!isWikiWorkspace(workspace) || !workspace.gitUrl) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const { wikiFolderLocation } = workspace;
|
const { wikiFolderLocation, gitUrl } = workspace;
|
||||||
const offlineText = isOnline ? '' : ` (${t('ContextMenu.NoNetworkConnection')})`;
|
const offlineText = isOnline ? '' : ` (${t('ContextMenu.NoNetworkConnection')})`;
|
||||||
|
|
||||||
if (aiEnabled) {
|
if (aiEnabled) {
|
||||||
|
|
@ -172,6 +177,8 @@ export function createSyncMenuItems(
|
||||||
dir: wikiFolderLocation,
|
dir: wikiFolderLocation,
|
||||||
commitOnly: false,
|
commitOnly: false,
|
||||||
commitMessage: t('LOG.CommitBackupMessage'),
|
commitMessage: t('LOG.CommitBackupMessage'),
|
||||||
|
remoteUrl: gitUrl,
|
||||||
|
userInfo,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
@ -183,6 +190,8 @@ export function createSyncMenuItems(
|
||||||
dir: wikiFolderLocation,
|
dir: wikiFolderLocation,
|
||||||
commitOnly: false,
|
commitOnly: false,
|
||||||
// Don't provide commitMessage to trigger AI generation
|
// Don't provide commitMessage to trigger AI generation
|
||||||
|
remoteUrl: gitUrl,
|
||||||
|
userInfo,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
@ -197,6 +206,8 @@ export function createSyncMenuItems(
|
||||||
await gitService.commitAndSync(workspace, {
|
await gitService.commitAndSync(workspace, {
|
||||||
dir: wikiFolderLocation,
|
dir: wikiFolderLocation,
|
||||||
commitOnly: false,
|
commitOnly: false,
|
||||||
|
remoteUrl: gitUrl,
|
||||||
|
userInfo,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,12 @@
|
||||||
|
import type { IAuthenticationService } from '@services/auth/interface';
|
||||||
import { container } from '@services/container';
|
import { container } from '@services/container';
|
||||||
|
import type { IContextService } from '@services/context/interface';
|
||||||
import type { IGitService } from '@services/git/interface';
|
import type { IGitService } from '@services/git/interface';
|
||||||
import { i18n } from '@services/libs/i18n';
|
import { i18n } from '@services/libs/i18n';
|
||||||
import type { IMenuService } from '@services/menu/interface';
|
import type { IMenuService } from '@services/menu/interface';
|
||||||
import { DeferredMenuItemConstructorOptions } from '@services/menu/interface';
|
import { DeferredMenuItemConstructorOptions } from '@services/menu/interface';
|
||||||
import serviceIdentifier from '@services/serviceIdentifier';
|
import serviceIdentifier from '@services/serviceIdentifier';
|
||||||
|
import { SupportedStorageServices } from '@services/types';
|
||||||
import type { IWindowService } from '@services/windows/interface';
|
import type { IWindowService } from '@services/windows/interface';
|
||||||
import { WindowNames } from '@services/windows/WindowProperties';
|
import { WindowNames } from '@services/windows/WindowProperties';
|
||||||
import type { IWorkspaceService } from '@services/workspaces/interface';
|
import type { IWorkspaceService } from '@services/workspaces/interface';
|
||||||
|
|
@ -14,12 +17,23 @@ export async function registerMenu(): Promise<void> {
|
||||||
const windowService = container.get<IWindowService>(serviceIdentifier.Window);
|
const windowService = container.get<IWindowService>(serviceIdentifier.Window);
|
||||||
const workspaceService = container.get<IWorkspaceService>(serviceIdentifier.Workspace);
|
const workspaceService = container.get<IWorkspaceService>(serviceIdentifier.Workspace);
|
||||||
const gitService = container.get<IGitService>(serviceIdentifier.Git);
|
const gitService = container.get<IGitService>(serviceIdentifier.Git);
|
||||||
|
const authService = container.get<IAuthenticationService>(serviceIdentifier.Authentication);
|
||||||
|
const contextService = container.get<IContextService>(serviceIdentifier.Context);
|
||||||
|
|
||||||
const hasActiveWikiWorkspace = async (): Promise<boolean> => {
|
const hasActiveWikiWorkspace = async (): Promise<boolean> => {
|
||||||
const activeWorkspace = await workspaceService.getActiveWorkspace();
|
const activeWorkspace = await workspaceService.getActiveWorkspace();
|
||||||
return activeWorkspace !== undefined && isWikiWorkspace(activeWorkspace);
|
return activeWorkspace !== undefined && isWikiWorkspace(activeWorkspace);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const hasActiveSyncableWorkspace = async (): Promise<boolean> => {
|
||||||
|
const activeWorkspace = await workspaceService.getActiveWorkspace();
|
||||||
|
if (!activeWorkspace || !isWikiWorkspace(activeWorkspace)) return false;
|
||||||
|
if (activeWorkspace.storageService === SupportedStorageServices.local) return false;
|
||||||
|
if (!activeWorkspace.gitUrl) return false;
|
||||||
|
const userInfo = await authService.getStorageServiceUserInfo(activeWorkspace.storageService);
|
||||||
|
return userInfo !== undefined;
|
||||||
|
};
|
||||||
|
|
||||||
// Build commit and sync menu items with dynamic enabled/click that checks activeWorkspace at runtime
|
// Build commit and sync menu items with dynamic enabled/click that checks activeWorkspace at runtime
|
||||||
const commitMenuItems: DeferredMenuItemConstructorOptions[] = [
|
const commitMenuItems: DeferredMenuItemConstructorOptions[] = [
|
||||||
{
|
{
|
||||||
|
|
@ -67,8 +81,84 @@ export async function registerMenu(): Promise<void> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Build sync menu items
|
||||||
const syncMenuItems: DeferredMenuItemConstructorOptions[] = [];
|
const syncMenuItems: DeferredMenuItemConstructorOptions[] = [];
|
||||||
|
|
||||||
|
const isOnline = await contextService.isOnline();
|
||||||
|
const offlineText = isOnline ? '' : ` (${i18n.t('ContextMenu.NoNetworkConnection')})`;
|
||||||
|
|
||||||
|
if (aiGenerateBackupTitleEnabled) {
|
||||||
|
syncMenuItems.push(
|
||||||
|
{
|
||||||
|
label: () => i18n.t('ContextMenu.SyncNow') + offlineText,
|
||||||
|
id: 'sync-now',
|
||||||
|
visible: hasActiveSyncableWorkspace,
|
||||||
|
enabled: async () => {
|
||||||
|
const online = await contextService.isOnline();
|
||||||
|
return online && await hasActiveSyncableWorkspace();
|
||||||
|
},
|
||||||
|
click: async () => {
|
||||||
|
const activeWorkspace = await workspaceService.getActiveWorkspace();
|
||||||
|
if (activeWorkspace !== undefined && isWikiWorkspace(activeWorkspace)) {
|
||||||
|
const userInfo = await authService.getStorageServiceUserInfo(activeWorkspace.storageService);
|
||||||
|
await gitService.commitAndSync(activeWorkspace, {
|
||||||
|
dir: activeWorkspace.wikiFolderLocation,
|
||||||
|
commitOnly: false,
|
||||||
|
commitMessage: i18n.t('LOG.CommitBackupMessage'),
|
||||||
|
remoteUrl: activeWorkspace.gitUrl,
|
||||||
|
userInfo,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: () => i18n.t('ContextMenu.SyncNow') + i18n.t('ContextMenu.WithAI') + offlineText,
|
||||||
|
id: 'sync-now-ai',
|
||||||
|
visible: hasActiveSyncableWorkspace,
|
||||||
|
enabled: async () => {
|
||||||
|
const online = await contextService.isOnline();
|
||||||
|
return online && await hasActiveSyncableWorkspace();
|
||||||
|
},
|
||||||
|
click: async () => {
|
||||||
|
const activeWorkspace = await workspaceService.getActiveWorkspace();
|
||||||
|
if (activeWorkspace !== undefined && isWikiWorkspace(activeWorkspace)) {
|
||||||
|
const userInfo = await authService.getStorageServiceUserInfo(activeWorkspace.storageService);
|
||||||
|
await gitService.commitAndSync(activeWorkspace, {
|
||||||
|
dir: activeWorkspace.wikiFolderLocation,
|
||||||
|
commitOnly: false,
|
||||||
|
// Don't provide commitMessage to trigger AI generation
|
||||||
|
remoteUrl: activeWorkspace.gitUrl,
|
||||||
|
userInfo,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
syncMenuItems.push({
|
||||||
|
label: () => i18n.t('ContextMenu.SyncNow') + offlineText,
|
||||||
|
id: 'sync-now',
|
||||||
|
visible: hasActiveSyncableWorkspace,
|
||||||
|
enabled: async () => {
|
||||||
|
const online = await contextService.isOnline();
|
||||||
|
return online && await hasActiveSyncableWorkspace();
|
||||||
|
},
|
||||||
|
click: async () => {
|
||||||
|
const activeWorkspace = await workspaceService.getActiveWorkspace();
|
||||||
|
if (activeWorkspace !== undefined && isWikiWorkspace(activeWorkspace)) {
|
||||||
|
const userInfo = await authService.getStorageServiceUserInfo(activeWorkspace.storageService);
|
||||||
|
await gitService.commitAndSync(activeWorkspace, {
|
||||||
|
dir: activeWorkspace.wikiFolderLocation,
|
||||||
|
commitOnly: false,
|
||||||
|
commitMessage: i18n.t('LOG.CommitBackupMessage'),
|
||||||
|
remoteUrl: activeWorkspace.gitUrl,
|
||||||
|
userInfo,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Add to Wiki menu - basic items (each item checks for active wiki workspace)
|
// Add to Wiki menu - basic items (each item checks for active wiki workspace)
|
||||||
await menuService.insertMenu(
|
await menuService.insertMenu(
|
||||||
'Wiki',
|
'Wiki',
|
||||||
|
|
|
||||||
|
|
@ -204,7 +204,7 @@ export async function getWorkspaceMenuTemplate(
|
||||||
if (userInfo !== undefined) {
|
if (userInfo !== undefined) {
|
||||||
const isOnline = await service.context.isOnline();
|
const isOnline = await service.context.isOnline();
|
||||||
|
|
||||||
const syncItems = createSyncMenuItems(workspace, t, service.git, aiGenerateBackupTitleEnabled, isOnline, false);
|
const syncItems = createSyncMenuItems(workspace, t, service.git, aiGenerateBackupTitleEnabled, isOnline, userInfo, false);
|
||||||
template.push(...syncItems);
|
template.push(...syncItems);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ export function useForm(
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await window.service.workspace.update(workspace.id, workspace);
|
await window.service.workspace.update(workspace.id, workspace);
|
||||||
|
await window.service.native.log('info', '[test-id-WORKSPACE_SAVED]', { workspaceId: workspace.id, workspaceName: workspace.name });
|
||||||
if (requestRestartAfterSave) {
|
if (requestRestartAfterSave) {
|
||||||
requestRestartCountDown();
|
requestRestartCountDown();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue