refactor(workspace-settings): extract useWorkspaceSetting hook
Deduplicate the "save a workspace setting" plumbing shared by HtmlEmbedSettings and TrackerSettings (workspace atom read, isLoading state, updateWorkspace + atom merge forcing settings[key], success/error notifications) into a new feature-scoped hook useWorkspaceSetting(key). - Each component keeps its own interaction model: html-embed is an optimistic toggle with revert-on-failure; tracker is edit-then-save on an explicit button. - Unify error handling on the better pattern: surface err.response?.data?.message and use console.error (html-embed previously used console.log + a generic message). No user-facing behavior change; client typecheck clean. Test-coverage follow-ups (untested trackerHead injection in ShareSeoController and the no-op audit branch) tracked in #100.
This commit is contained in:
@@ -1,9 +1,6 @@
|
||||
import { workspaceAtom } from "@/features/user/atoms/current-user-atom.ts";
|
||||
import { useAtom } from "jotai";
|
||||
import { useState } from "react";
|
||||
import { updateWorkspace } from "@/features/workspace/services/workspace-service.ts";
|
||||
import { useWorkspaceSetting } from "@/features/workspace/hooks/use-workspace-setting.ts";
|
||||
import { Switch, Stack, Paper, Group, Text, List } from "@mantine/core";
|
||||
import { notifications } from "@mantine/notifications";
|
||||
import useUserRole from "@/hooks/use-user-role.tsx";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
@@ -18,40 +15,18 @@ import { useTranslation } from "react-i18next";
|
||||
*/
|
||||
export default function HtmlEmbedSettings() {
|
||||
const { t } = useTranslation();
|
||||
const [workspace, setWorkspace] = useAtom(workspaceAtom);
|
||||
const { workspace, isLoading, save } = useWorkspaceSetting("htmlEmbed");
|
||||
const { isAdmin } = useUserRole();
|
||||
|
||||
const [checked, setChecked] = useState<boolean>(
|
||||
workspace?.settings?.htmlEmbed ?? false,
|
||||
);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
async function handleToggle(value: boolean) {
|
||||
setIsLoading(true);
|
||||
const previous = checked;
|
||||
setChecked(value); // optimistic update
|
||||
try {
|
||||
const updated = await updateWorkspace({ htmlEmbed: value });
|
||||
// Force settings.htmlEmbed to the new value so the atom is consistent even
|
||||
// if the response shape omits it.
|
||||
setWorkspace({
|
||||
...updated,
|
||||
settings: {
|
||||
...updated.settings,
|
||||
htmlEmbed: value,
|
||||
},
|
||||
});
|
||||
notifications.show({ message: t("Updated successfully") });
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
setChecked(previous); // revert on failure
|
||||
notifications.show({
|
||||
message: t("Failed to update data"),
|
||||
color: "red",
|
||||
});
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
const ok = await save(value);
|
||||
if (!ok) setChecked(previous); // revert on failure
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import { workspaceAtom } from "@/features/user/atoms/current-user-atom.ts";
|
||||
import { useAtom } from "jotai";
|
||||
import { useState } from "react";
|
||||
import { updateWorkspace } from "@/features/workspace/services/workspace-service.ts";
|
||||
import { useWorkspaceSetting } from "@/features/workspace/hooks/use-workspace-setting.ts";
|
||||
import {
|
||||
Button,
|
||||
Group,
|
||||
@@ -10,7 +8,6 @@ import {
|
||||
Text,
|
||||
Textarea,
|
||||
} from "@mantine/core";
|
||||
import { notifications } from "@mantine/notifications";
|
||||
import useUserRole from "@/hooks/use-user-role.tsx";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
@@ -25,36 +22,15 @@ import { useTranslation } from "react-i18next";
|
||||
*/
|
||||
export default function TrackerSettings() {
|
||||
const { t } = useTranslation();
|
||||
const [workspace, setWorkspace] = useAtom(workspaceAtom);
|
||||
const { workspace, isLoading, save } = useWorkspaceSetting("trackerHead");
|
||||
const { isAdmin } = useUserRole();
|
||||
|
||||
const [value, setValue] = useState<string>(
|
||||
workspace?.settings?.trackerHead ?? "",
|
||||
);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
async function handleSave() {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const updated = await updateWorkspace({ trackerHead: value });
|
||||
setWorkspace({
|
||||
...updated,
|
||||
settings: {
|
||||
...updated.settings,
|
||||
trackerHead: value,
|
||||
},
|
||||
});
|
||||
notifications.show({ message: t("Updated successfully") });
|
||||
} catch (err) {
|
||||
console.error("Failed to update tracker settings", err);
|
||||
notifications.show({
|
||||
message:
|
||||
(err as any)?.response?.data?.message ?? t("Failed to update data"),
|
||||
color: "red",
|
||||
});
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
await save(value);
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
import { workspaceAtom } from "@/features/user/atoms/current-user-atom.ts";
|
||||
import { useAtom } from "jotai";
|
||||
import { useCallback, useState } from "react";
|
||||
import { updateWorkspace } from "@/features/workspace/services/workspace-service.ts";
|
||||
import { IWorkspace } from "@/features/workspace/types/workspace.types.ts";
|
||||
import { notifications } from "@mantine/notifications";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
/**
|
||||
* Workspace setting keys that this hook can persist. Each key is both a
|
||||
* write-only field on the update payload and a read field under
|
||||
* `workspace.settings`, so the value type is derived from the settings shape.
|
||||
*/
|
||||
type WorkspaceSettingKey = "htmlEmbed" | "trackerHead";
|
||||
type WorkspaceSettingValue<K extends WorkspaceSettingKey> =
|
||||
NonNullable<IWorkspace["settings"][K]>;
|
||||
|
||||
/**
|
||||
* Shared "save a workspace setting" plumbing extracted from the individual
|
||||
* settings components. Owns the `isLoading` state and the persist-then-merge
|
||||
* flow (call `updateWorkspace`, merge the response back into the workspace atom
|
||||
* while forcing `settings[key]` to the saved value, and surface a success/error
|
||||
* notification). Callers keep their own interaction model (optimistic toggle,
|
||||
* edit-then-save, etc.) on top of this.
|
||||
*/
|
||||
export function useWorkspaceSetting<K extends WorkspaceSettingKey>(key: K) {
|
||||
const [workspace, setWorkspace] = useAtom(workspaceAtom);
|
||||
const { t } = useTranslation();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const save = useCallback(
|
||||
async (value: WorkspaceSettingValue<K>): Promise<boolean> => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const updated = await updateWorkspace({
|
||||
[key]: value,
|
||||
} as Partial<IWorkspace>);
|
||||
// Force settings[key] to the new value so the atom is consistent even
|
||||
// if the response shape omits it.
|
||||
setWorkspace({
|
||||
...updated,
|
||||
settings: {
|
||||
...updated.settings,
|
||||
[key]: value,
|
||||
},
|
||||
});
|
||||
notifications.show({ message: t("Updated successfully") });
|
||||
return true;
|
||||
} catch (err) {
|
||||
console.error(`Failed to update workspace setting "${key}"`, err);
|
||||
notifications.show({
|
||||
message:
|
||||
(err as any)?.response?.data?.message ?? t("Failed to update data"),
|
||||
color: "red",
|
||||
});
|
||||
return false;
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
},
|
||||
[key, setWorkspace, t],
|
||||
);
|
||||
|
||||
return { workspace, isLoading, save };
|
||||
}
|
||||
Reference in New Issue
Block a user