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",
|
"manageTools": "Go to Tools",
|
||||||
"edit": "Edit",
|
"edit": "Edit",
|
||||||
"delete": "Delete",
|
"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": {
|
"modals": {
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import { ActiveState } from '../models/misc';
|
|||||||
import { selectToken } from '../preferences/preferenceSlice';
|
import { selectToken } from '../preferences/preferenceSlice';
|
||||||
import { APIActionType, APIToolType, UserToolType } from './types';
|
import { APIActionType, APIToolType, UserToolType } from './types';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { areObjectsEqual } from '../utils/objectUtils';
|
||||||
|
|
||||||
export default function ToolConfig({
|
export default function ToolConfig({
|
||||||
tool,
|
tool,
|
||||||
@@ -34,7 +35,35 @@ export default function ToolConfig({
|
|||||||
);
|
);
|
||||||
const [actionModalState, setActionModalState] =
|
const [actionModalState, setActionModalState] =
|
||||||
React.useState<ActiveState>('INACTIVE');
|
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 { 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) => {
|
const handleCheckboxChange = (actionIndex: number, property: string) => {
|
||||||
setTool({
|
setTool({
|
||||||
...tool,
|
...tool,
|
||||||
@@ -79,6 +108,14 @@ export default function ToolConfig({
|
|||||||
token,
|
token,
|
||||||
)
|
)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
|
// Update initialState to match current state
|
||||||
|
setInitialState({
|
||||||
|
customName,
|
||||||
|
authKey,
|
||||||
|
config: tool.config,
|
||||||
|
actions: 'actions' in tool ? tool.actions : [],
|
||||||
|
});
|
||||||
|
setHasUnsavedChanges(false);
|
||||||
handleGoBack();
|
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">
|
<div className="flex items-center gap-3 text-sm text-eerie-black dark:text-bright-gray">
|
||||||
<button
|
<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]"
|
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" />
|
<img src={ArrowLeft} alt="left-arrow" className="h-3 w-3" />
|
||||||
</button>
|
</button>
|
||||||
@@ -376,6 +413,51 @@ export default function ToolConfig({
|
|||||||
setModalState={setActionModalState}
|
setModalState={setActionModalState}
|
||||||
handleSubmit={handleAddNewAction}
|
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>
|
||||||
</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