mirror of
https://github.com/arc53/DocsGPT.git
synced 2025-11-29 08:33:20 +00:00
(feat:tools) warn on unsaved changes, deep compare
This commit is contained in:
@@ -122,7 +122,10 @@
|
||||
"manageTools": "Go to Tools",
|
||||
"edit": "Edit",
|
||||
"delete": "Delete",
|
||||
"deleteWarning": "Are you sure you want to delete {toolName}?"
|
||||
"deleteWarning": "Are you sure you want to delete {toolName}?",
|
||||
"unsavedChanges": "You have unsaved changes that will be lost if you leave without saving.",
|
||||
"leaveWithoutSaving": "Leave without Saving",
|
||||
"saveAndLeave": "Save and Leave"
|
||||
}
|
||||
},
|
||||
"modals": {
|
||||
|
||||
@@ -15,6 +15,7 @@ import { ActiveState } from '../models/misc';
|
||||
import { selectToken } from '../preferences/preferenceSlice';
|
||||
import { APIActionType, APIToolType, UserToolType } from './types';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { areObjectsEqual } from '../utils/objectUtils';
|
||||
|
||||
export default function ToolConfig({
|
||||
tool,
|
||||
@@ -34,7 +35,35 @@ export default function ToolConfig({
|
||||
);
|
||||
const [actionModalState, setActionModalState] =
|
||||
React.useState<ActiveState>('INACTIVE');
|
||||
const [initialState, setInitialState] = React.useState({
|
||||
customName: tool.customName || '',
|
||||
authKey: 'token' in tool.config ? tool.config.token : '',
|
||||
config: tool.config,
|
||||
actions: 'actions' in tool ? tool.actions : [],
|
||||
});
|
||||
const [hasUnsavedChanges, setHasUnsavedChanges] = React.useState(false);
|
||||
const [showUnsavedModal, setShowUnsavedModal] = React.useState(false);
|
||||
const { t } = useTranslation();
|
||||
|
||||
const handleBackClick = () => {
|
||||
if (hasUnsavedChanges) {
|
||||
setShowUnsavedModal(true);
|
||||
} else {
|
||||
handleGoBack();
|
||||
}
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
const currentState = {
|
||||
customName,
|
||||
authKey,
|
||||
config: tool.config,
|
||||
actions: 'actions' in tool ? tool.actions : [],
|
||||
};
|
||||
|
||||
setHasUnsavedChanges(!areObjectsEqual(initialState, currentState));
|
||||
}, [customName, authKey, tool]);
|
||||
|
||||
const handleCheckboxChange = (actionIndex: number, property: string) => {
|
||||
setTool({
|
||||
...tool,
|
||||
@@ -79,6 +108,14 @@ export default function ToolConfig({
|
||||
token,
|
||||
)
|
||||
.then(() => {
|
||||
// Update initialState to match current state
|
||||
setInitialState({
|
||||
customName,
|
||||
authKey,
|
||||
config: tool.config,
|
||||
actions: 'actions' in tool ? tool.actions : [],
|
||||
});
|
||||
setHasUnsavedChanges(false);
|
||||
handleGoBack();
|
||||
});
|
||||
};
|
||||
@@ -124,7 +161,7 @@ export default function ToolConfig({
|
||||
<div className="flex items-center gap-3 text-sm text-eerie-black dark:text-bright-gray">
|
||||
<button
|
||||
className="rounded-full border p-3 text-sm text-gray-400 dark:border-0 dark:bg-[#28292D] dark:text-gray-500 dark:hover:bg-[#2E2F34]"
|
||||
onClick={handleGoBack}
|
||||
onClick={handleBackClick}
|
||||
>
|
||||
<img src={ArrowLeft} alt="left-arrow" className="h-3 w-3" />
|
||||
</button>
|
||||
@@ -376,6 +413,51 @@ export default function ToolConfig({
|
||||
setModalState={setActionModalState}
|
||||
handleSubmit={handleAddNewAction}
|
||||
/>
|
||||
{showUnsavedModal && (
|
||||
<ConfirmationModal
|
||||
message={t('settings.tools.unsavedChanges', {
|
||||
defaultValue:
|
||||
'You have unsaved changes that will be lost if you leave without saving.',
|
||||
})}
|
||||
modalState="ACTIVE"
|
||||
setModalState={(state) => setShowUnsavedModal(state === 'ACTIVE')}
|
||||
submitLabel={t('settings.tools.leaveWithoutSaving', {
|
||||
defaultValue: 'Leave without Saving',
|
||||
})}
|
||||
handleSubmit={() => {
|
||||
setShowUnsavedModal(false);
|
||||
handleGoBack();
|
||||
}}
|
||||
cancelLabel={t('settings.tools.saveAndLeave', {
|
||||
defaultValue: 'Save and Leave',
|
||||
})}
|
||||
handleCancel={() => {
|
||||
// First save changes, then go back
|
||||
userService
|
||||
.updateTool(
|
||||
{
|
||||
id: tool.id,
|
||||
name: tool.name,
|
||||
displayName: tool.displayName,
|
||||
customName: customName,
|
||||
description: tool.description,
|
||||
config:
|
||||
tool.name === 'api_tool'
|
||||
? tool.config
|
||||
: { token: authKey },
|
||||
actions: 'actions' in tool ? tool.actions : [],
|
||||
status: tool.status,
|
||||
},
|
||||
token,
|
||||
)
|
||||
.then(() => {
|
||||
setShowUnsavedModal(false);
|
||||
handleGoBack();
|
||||
});
|
||||
}}
|
||||
variant="danger"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
29
frontend/src/utils/objectUtils.ts
Normal file
29
frontend/src/utils/objectUtils.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* Deeply compares two objects for equality
|
||||
* @param obj1 First object to compare
|
||||
* @param obj2 Second object to compare
|
||||
* @returns boolean indicating if objects are equal
|
||||
*/
|
||||
export function areObjectsEqual(obj1: any, obj2: any): boolean {
|
||||
if (obj1 === obj2) return true;
|
||||
if (obj1 == null || obj2 == null) return false;
|
||||
if (typeof obj1 !== 'object' || typeof obj2 !== 'object') return false;
|
||||
|
||||
if (Array.isArray(obj1) && Array.isArray(obj2)) {
|
||||
if (obj1.length !== obj2.length) return false;
|
||||
return obj1.every((val, idx) => areObjectsEqual(val, obj2[idx]));
|
||||
}
|
||||
|
||||
if (obj1 instanceof Date && obj2 instanceof Date) {
|
||||
return obj1.getTime() === obj2.getTime();
|
||||
}
|
||||
|
||||
const keys1 = Object.keys(obj1);
|
||||
const keys2 = Object.keys(obj2);
|
||||
|
||||
if (keys1.length !== keys2.length) return false;
|
||||
|
||||
return keys1.every((key) => {
|
||||
return keys2.includes(key) && areObjectsEqual(obj1[key], obj2[key]);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user