mirror of
https://github.com/tiddly-gittly/TidGi-Desktop.git
synced 2026-01-21 12:02:57 -08:00
fix: review
This commit is contained in:
parent
34afec89a6
commit
8c0927aef0
6 changed files with 119 additions and 26 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue