diff --git a/frontend/src/locale/en.json b/frontend/src/locale/en.json index b9859635..65759996 100644 --- a/frontend/src/locale/en.json +++ b/frontend/src/locale/en.json @@ -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": { diff --git a/frontend/src/settings/ToolConfig.tsx b/frontend/src/settings/ToolConfig.tsx index 50b67d0d..68b68951 100644 --- a/frontend/src/settings/ToolConfig.tsx +++ b/frontend/src/settings/ToolConfig.tsx @@ -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('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({
@@ -376,6 +413,51 @@ export default function ToolConfig({ setModalState={setActionModalState} handleSubmit={handleAddNewAction} /> + {showUnsavedModal && ( + 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" + /> + )}
); diff --git a/frontend/src/utils/objectUtils.ts b/frontend/src/utils/objectUtils.ts new file mode 100644 index 00000000..45d5313d --- /dev/null +++ b/frontend/src/utils/objectUtils.ts @@ -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]); + }); +}