fix: review

This commit is contained in:
lin onetwo 2025-12-30 13:47:29 +08:00
parent 34afec89a6
commit 8c0927aef0
6 changed files with 119 additions and 26 deletions

View file

@ -56,6 +56,10 @@ Feature: Sub-Wiki Functionality
Then I should see "page body and workspaces" elements with selectors:
| div[data-testid^='workspace-']:has-text('wiki') |
| div[data-testid^='workspace-']:has-text('SubWikiPreload') |
# Enable file system watch for testing (default is false in production)
When I update workspace "wiki" settings:
| property | value |
| enableFileSystemWatch | true |
When I click on a "default wiki workspace button" 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
@ -84,6 +88,10 @@ Feature: Sub-Wiki Functionality
Then I should see "page body and workspaces" elements with selectors:
| div[data-testid^='workspace-']:has-text('wiki') |
| div[data-testid^='workspace-']:has-text('SubWikiTagTree') |
# Enable file system watch for testing (default is false in production)
When I update workspace "wiki" settings:
| property | value |
| enableFileSystemWatch | true |
When I click on a "default wiki workspace button" 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
@ -113,6 +121,10 @@ Feature: Sub-Wiki Functionality
Then I should see "page body and workspaces" elements with selectors:
| div[data-testid^='workspace-']:has-text('wiki') |
| div[data-testid^='workspace-']:has-text('SubWikiFilter') |
# Enable file system watch for testing (default is false in production)
When I update workspace "wiki" settings:
| property | value |
| enableFileSystemWatch | true |
When I click on a "default wiki workspace button" 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
@ -161,6 +173,10 @@ Feature: Sub-Wiki Functionality
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')"
# Enable file system watch for testing (default is false in production)
When I update workspace "wiki" settings:
| property | value |
| enableFileSystemWatch | true |
When I click on a "default wiki workspace button" 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

View file

@ -603,8 +603,20 @@ export async function revertCommit(repoPath: string, commitHash: string, commitM
/**
* Undo a commit by resetting to the previous commit and keeping changes as unstaged
* This is similar to GitHub Desktop's "Undo" feature
* Only works on the HEAD commit to prevent unexpected behavior
*/
export async function undoCommit(repoPath: string, commitHash: string): Promise<void> {
// Verify that the provided commitHash is actually the HEAD commit
const headResult = await gitExec(['rev-parse', 'HEAD'], repoPath);
if (headResult.exitCode !== 0) {
throw new Error('Failed to get HEAD commit');
}
const headCommit = headResult.stdout.trim();
if (commitHash !== headCommit) {
throw new Error('Can only undo the most recent commit (HEAD). The provided commit is not HEAD.');
}
// Get the parent commit of the current commit
const parentResult = await gitExec(['rev-parse', `${commitHash}^`], repoPath);

View file

@ -4,6 +4,37 @@ import { PreferenceChannel } from '@/constants/channels';
import type { HunspellLanguages } from '@/constants/hunspellLanguages';
import type { BehaviorSubject } from 'rxjs';
/**
* Deep equality check for comparing values (handles primitives, arrays, and objects)
* Used to determine if a value differs from its default
*/
function isEqual(a: unknown, b: unknown): boolean {
// Handle primitives and null/undefined
if (a === b) return true;
if (a == null || b == null) return false;
if (typeof a !== 'object' || typeof b !== 'object') return false;
// Handle arrays
if (Array.isArray(a) && Array.isArray(b)) {
if (a.length !== b.length) return false;
return a.every((item, index) => isEqual(item, b[index]));
}
// One is array, other is not
if (Array.isArray(a) || Array.isArray(b)) return false;
// Handle objects
const keysA = Object.keys(a);
const keysB = Object.keys(b);
if (keysA.length !== keysB.length) return false;
return keysA.every(key => {
const valueA = (a as Record<string, unknown>)[key];
const valueB = (b as Record<string, unknown>)[key];
return isEqual(valueA, valueB);
});
}
export interface IPreferences {
allowPrerelease: boolean;
alwaysOnTop: boolean;
@ -99,12 +130,8 @@ export function getPreferenceDifferencesFromDefaults(preferences: IPreferences,
const defaultValue = defaults[key];
const preferenceValue = preferences[key];
// For complex types like objects and arrays, do deep comparison
if (typeof defaultValue === 'object' && typeof preferenceValue === 'object') {
if (JSON.stringify(defaultValue) !== JSON.stringify(preferenceValue)) {
(differences as Record<string, unknown>)[key] = preferenceValue;
}
} else if (defaultValue !== preferenceValue) {
// Use deep equality check for all types
if (!isEqual(defaultValue, preferenceValue)) {
(differences as Record<string, unknown>)[key] = preferenceValue;
}
});

View file

@ -5,6 +5,37 @@ import { ProxyPropertyType } from 'electron-ipc-cat/common';
import { BehaviorSubject, Observable } from 'rxjs';
import { SetOptional } from 'type-fest';
/**
* Deep equality check for comparing values (handles primitives, arrays, and objects)
* Used to determine if a value differs from its default
*/
function isEqual(a: unknown, b: unknown): boolean {
// Handle primitives and null/undefined
if (a === b) return true;
if (a == null || b == null) return false;
if (typeof a !== 'object' || typeof b !== 'object') return false;
// Handle arrays
if (Array.isArray(a) && Array.isArray(b)) {
if (a.length !== b.length) return false;
return a.every((item, index) => isEqual(item, b[index]));
}
// One is array, other is not
if (Array.isArray(a) || Array.isArray(b)) return false;
// Handle objects
const keysA = Object.keys(a);
const keysB = Object.keys(b);
if (keysA.length !== keysB.length) return false;
return keysA.every(key => {
const valueA = (a as Record<string, unknown>)[key];
const valueB = (b as Record<string, unknown>)[key];
return isEqual(valueA, valueB);
});
}
/**
* Fields that not part of config that user can edit. Change of these field won't show "save" button on edit page.
*/
@ -416,7 +447,7 @@ export function getDifferencesFromDefaults(workspace: IWikiWorkspace): Partial<I
const workspaceValue = workspace[typedKey];
// Include field if it has a value and differs from default, or if there's no default defined
if (defaultValue === undefined || defaultValue !== workspaceValue) {
if (defaultValue === undefined || !isEqual(defaultValue, workspaceValue)) {
(differences as unknown as Record<string, unknown>)[typedKey as string] = workspaceValue;
}
});

View file

@ -170,12 +170,17 @@ export function CommitDetailsPanel(
const handleConfirmEditMessage = async (): Promise<void> => {
if (isAmending || !commit) return;
// Validate that commit message is not empty after trimming
const trimmedMessage = newCommitMessage.trim();
if (!trimmedMessage) {
return;
}
setIsAmending(true);
try {
const workspace = await window.service.workspace.get(workspaceID);
if (!workspace || !('wikiFolderLocation' in workspace)) return;
await window.service.git.amendCommitMessage(workspace.wikiFolderLocation, newCommitMessage.trim());
await window.service.git.amendCommitMessage(workspace.wikiFolderLocation, trimmedMessage);
setIsEditMessageOpen(false);
if (onCommitSuccess) {
onCommitSuccess();
@ -509,7 +514,7 @@ export function CommitDetailsPanel(
</DialogContent>
<DialogActions>
<Button onClick={handleCloseEditMessage}>{t('Common.Cancel', { defaultValue: '取消' })}</Button>
<Button onClick={handleConfirmEditMessage} disabled={isAmending} variant='contained'>
<Button onClick={handleConfirmEditMessage} disabled={isAmending || !newCommitMessage.trim()} variant='contained'>
{isAmending ? t('GitLog.Committing', { defaultValue: '提交中...' }) : t('GitLog.EditCommitMessageConfirm', { defaultValue: '保存' })}
</Button>
</DialogActions>

View file

@ -141,6 +141,20 @@ export function useCommitSelection({
}
}, [lastChangeType, entries, setSelectedCommit]);
// Auto-select uncommitted changes after undo
useEffect(() => {
if (lastChangeType === 'undo' && entries.length > 0) {
// Find uncommitted entry (hash === '')
const uncommittedEntry = entries.find((entry) => entry.hash === '');
if (uncommittedEntry) {
setSelectedCommit(uncommittedEntry);
} else {
// If no uncommitted changes, deselect
setSelectedCommit(undefined);
}
}
}, [lastChangeType, entries, setSelectedCommit]);
// Maintain selection across refreshes by hash
// Skip if we should select first (manual commit) or if a commit just happened (auto-selection in progress)
useEffect(() => {
@ -161,23 +175,11 @@ export function useCommitSelection({
}, [setShouldSelectFirst]);
const handleUndoSuccess = useCallback(() => {
// After undo, select the uncommitted changes if available
// The entries will be refreshed automatically, so we set a flag to select uncommitted
// We use a callback approach: find uncommitted entry after data refreshes
const selectUncommitted = () => {
const uncommittedEntry = entries.find((entry) => entry.hash === '');
if (uncommittedEntry) {
setSelectedCommit(uncommittedEntry);
} else {
// If no uncommitted changes, deselect
setSelectedCommit(undefined);
}
};
// Schedule the selection for after entries are updated
// Use setTimeout to ensure entries are already loaded
setTimeout(selectUncommitted, 0);
}, [entries, setSelectedCommit]);
// After undo, we want to select uncommitted changes
// Set a special flag that will be handled by the effect above
// Using lastChangeType 'undo' will trigger the selection logic
setLastChangeType('undo');
}, [setLastChangeType]);
const handleSearch = useCallback(
(parameters: ISearchParameters) => {