diff --git a/web/public/locales/en/views/settings.json b/web/public/locales/en/views/settings.json index 2d0e78d11..745606ba2 100644 --- a/web/public/locales/en/views/settings.json +++ b/web/public/locales/en/views/settings.json @@ -1480,6 +1480,7 @@ "deleteProfile": "Delete Profile", "deleteProfileConfirm": "Delete profile \"{{profile}}\" from all cameras? This cannot be undone.", "deleteSuccess": "Profile '{{profile}}' deleted", + "createSuccess": "Profile '{{profile}}' created", "removeOverride": "Remove Profile Override", "deleteSection": "Delete Section Overrides", "deleteSectionConfirm": "Remove the {{section}} overrides for profile {{profile}} on {{camera}}?", diff --git a/web/src/pages/Settings.tsx b/web/src/pages/Settings.tsx index 99e7941e1..22f25855b 100644 --- a/web/src/pages/Settings.tsx +++ b/web/src/pages/Settings.tsx @@ -660,32 +660,20 @@ export default function Settings() { const [editingProfile, setEditingProfile] = useState< Record >({}); - const [newProfiles, setNewProfiles] = useState([]); const [profilesUIEnabled, setProfilesUIEnabled] = useState(false); const allProfileNames = useMemo(() => { - const names = new Set(); - if (config?.profiles) { - Object.keys(config.profiles).forEach((p) => names.add(p)); - } - newProfiles.forEach((p) => names.add(p)); - return [...names].sort(); - }, [config, newProfiles]); + if (!config?.profiles) return []; + return Object.keys(config.profiles).sort(); + }, [config]); const profileFriendlyNames = useMemo(() => { const map = new Map(); if (profilesData?.profiles) { profilesData.profiles.forEach((p) => map.set(p.name, p.friendly_name)); } - // Include pending (unsaved) profile definitions - for (const [key, data] of Object.entries(pendingDataBySection)) { - if (key.startsWith("__profile_def__.") && data?.friendly_name) { - const id = key.slice("__profile_def__.".length); - map.set(id, String(data.friendly_name)); - } - } return map; - }, [profilesData, pendingDataBySection]); + }, [profilesData]); const navigate = useNavigate(); @@ -842,27 +830,6 @@ export default function Settings() { for (const key of pendingKeys) { const pendingData = pendingDataBySection[key]; - // Handle top-level profile definition saves - if (key.startsWith("__profile_def__.")) { - const profileId = key.replace("__profile_def__.", ""); - try { - const configData = { profiles: { [profileId]: pendingData } }; - await axios.put("config/set", { - requires_restart: 0, - config_data: configData, - }); - setPendingDataBySection((prev) => { - const { [key]: _, ...rest } = prev; - return rest; - }); - savedKeys.push(key); - successCount++; - } catch { - failCount++; - } - continue; - } - try { const payload = prepareSectionSavePayload({ pendingDataKey: key, @@ -912,11 +879,6 @@ export default function Settings() { // Refresh config from server once await mutate("config"); - // If any profile definitions were saved, refresh profiles data too - if (savedKeys.some((key) => key.startsWith("__profile_def__."))) { - await mutate("profiles"); - } - // Clear hasChanges in sidebar for all successfully saved sections if (savedKeys.length > 0) { setSectionStatusByKey((prev) => { @@ -995,12 +957,6 @@ export default function Settings() { setUnsavedChanges(false); setEditingProfile({}); - // Clear new profiles that now exist in top-level config - if (config) { - const savedNames = new Set(Object.keys(config.profiles ?? {})); - setNewProfiles((prev) => prev.filter((p) => !savedNames.has(p))); - } - setSectionStatusByKey((prev) => { const updated = { ...prev }; for (const key of pendingKeys) { @@ -1015,7 +971,7 @@ export default function Settings() { } return updated; }); - }, [pendingDataBySection, pendingKeyToMenuKey, config]); + }, [pendingDataBySection, pendingKeyToMenuKey]); const handleDialog = useCallback( (save: boolean) => { @@ -1100,43 +1056,6 @@ export default function Settings() { [], ); - const handleAddProfile = useCallback((id: string, friendlyName: string) => { - setNewProfiles((prev) => (prev.includes(id) ? prev : [...prev, id])); - - // Stage the top-level profile definition for saving - setPendingDataBySection((prev) => ({ - ...prev, - [`__profile_def__.${id}`]: { friendly_name: friendlyName }, - })); - }, []); - - const handleRemoveNewProfile = useCallback((name: string) => { - setNewProfiles((prev) => prev.filter((p) => p !== name)); - // Clear any editing state for this profile - setEditingProfile((prev) => { - const updated = { ...prev }; - for (const key of Object.keys(updated)) { - if (updated[key] === name) { - delete updated[key]; - } - } - return updated; - }); - // Clear any pending data for this profile - setPendingDataBySection((prev) => { - const profileSegment = `profiles.${name}.`; - const updated = { ...prev }; - let changed = false; - for (const key of Object.keys(updated)) { - if (key.includes(profileSegment)) { - delete updated[key]; - changed = true; - } - } - return changed ? updated : prev; - }); - }, []); - const handleDeleteProfileSection = useCallback( async (camera: string, section: string, profile: string) => { try { @@ -1177,22 +1096,16 @@ export default function Settings() { const profileState: ProfileState = useMemo( () => ({ editingProfile, - newProfiles, allProfileNames, profileFriendlyNames, onSelectProfile: handleSelectProfile, - onAddProfile: handleAddProfile, - onRemoveNewProfile: handleRemoveNewProfile, onDeleteProfileSection: handleDeleteProfileSection, }), [ editingProfile, - newProfiles, allProfileNames, profileFriendlyNames, handleSelectProfile, - handleAddProfile, - handleRemoveNewProfile, handleDeleteProfileSection, ], ); diff --git a/web/src/types/profile.ts b/web/src/types/profile.ts index 0bcb95032..ea3273eca 100644 --- a/web/src/types/profile.ts +++ b/web/src/types/profile.ts @@ -18,7 +18,6 @@ export type ProfilesApiResponse = { export type ProfileState = { editingProfile: Record; - newProfiles: string[]; allProfileNames: string[]; profileFriendlyNames: Map; onSelectProfile: ( @@ -26,8 +25,6 @@ export type ProfileState = { section: string, profile: string | null, ) => void; - onAddProfile: (id: string, friendlyName: string) => void; - onRemoveNewProfile: (name: string) => void; onDeleteProfileSection: ( camera: string, section: string, diff --git a/web/src/views/settings/ProfilesView.tsx b/web/src/views/settings/ProfilesView.tsx index eafc9903f..b9c00b6d5 100644 --- a/web/src/views/settings/ProfilesView.tsx +++ b/web/src/views/settings/ProfilesView.tsx @@ -165,16 +165,42 @@ export default function ProfilesView({ return data; }, [config, allProfileNames]); + const [addingProfile, setAddingProfile] = useState(false); + const handleAddSubmit = useCallback( - (data: AddProfileForm) => { + async (data: AddProfileForm) => { const id = data.name.trim(); const friendlyName = data.friendly_name.trim(); if (!id || !friendlyName) return; - profileState?.onAddProfile(id, friendlyName); - setAddDialogOpen(false); - addForm.reset(); + + setAddingProfile(true); + try { + await axios.put("config/set", { + requires_restart: 0, + config_data: { + profiles: { [id]: { friendly_name: friendlyName } }, + }, + }); + await updateConfig(); + await updateProfiles(); + toast.success( + t("profiles.createSuccess", { + ns: "views/settings", + profile: friendlyName, + }), + { position: "top-center" }, + ); + setAddDialogOpen(false); + addForm.reset(); + } catch { + toast.error(t("toast.save.error.noMessage", { ns: "common" }), { + position: "top-center", + }); + } finally { + setAddingProfile(false); + } }, - [profileState, addForm], + [updateConfig, updateProfiles, addForm, t], ); const handleActivateProfile = useCallback( @@ -213,14 +239,6 @@ export default function ProfilesView({ const handleDeleteProfile = useCallback(async () => { if (!deleteProfile || !config) return; - // If this is an unsaved (new) profile, just remove it from local state - const isNewProfile = profileState?.newProfiles.includes(deleteProfile); - if (isNewProfile) { - profileState?.onRemoveNewProfile(deleteProfile); - setDeleteProfile(null); - return; - } - setDeleting(true); try { @@ -254,9 +272,6 @@ export default function ProfilesView({ await updateConfig(); await updateProfiles(); - // Also clean up local newProfiles state if this profile was in it - profileState?.onRemoveNewProfile(deleteProfile); - toast.success( t("profiles.deleteSuccess", { ns: "views/settings", @@ -281,7 +296,6 @@ export default function ProfilesView({ deleteProfile, activeProfile, config, - profileState, profileFriendlyNames, updateConfig, updateProfiles, @@ -609,6 +623,7 @@ export default function ProfilesView({ type="button" variant="outline" onClick={() => setAddDialogOpen(false)} + disabled={addingProfile} > {t("button.cancel", { ns: "common" })} @@ -616,10 +631,14 @@ export default function ProfilesView({ type="submit" variant="select" disabled={ + addingProfile || !addForm.watch("friendly_name").trim() || !addForm.watch("name").trim() } > + {addingProfile && ( + + )} {t("button.add", { ns: "common" })}