refactor(web): route-driven modal hook and CRUD conventions
Extract useRouteDrivenModal for the five list editors, move AddTriggerDialog into its own file, and document modal vs full-page CRUD patterns in CONTRIBUTING.md. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useState } from "react";
|
||||
import { useLocation, useNavigate, useParams } from "react-router-dom";
|
||||
import { LuEye, LuEyeOff, LuPencil, LuPlus, LuTrash2 } from "react-icons/lu";
|
||||
import { errorMessage } from "../api/client.js";
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
} from "../api/hooks.js";
|
||||
import { AuthEditorModal } from "../components/AuthEditorModal.jsx";
|
||||
import { ConfirmDialog } from "../components/ConfirmDialog.jsx";
|
||||
import { useRouteDrivenModal } from "../hooks/useRouteDrivenModal.js";
|
||||
import { formatTime } from "../lib/format.jsx";
|
||||
|
||||
function CredDisplay({ field, fieldKey, authId, cache, onRevealed }) {
|
||||
@@ -149,43 +150,24 @@ export function AuthProfilesPage() {
|
||||
|
||||
const { data: auths = [], isLoading } = useHttpAuths();
|
||||
const del = useDeleteHttpAuth();
|
||||
const [editor, setEditor] = useState(null);
|
||||
const [confirmDelete, setConfirmDelete] = useState(null);
|
||||
/** @type {[Record<string, Record<string, string>>, Function]} */
|
||||
const [revealCache, setRevealCache] = useState({});
|
||||
const openedRouteKey = useRef(null);
|
||||
|
||||
function closeEditor() {
|
||||
setEditor(null);
|
||||
openedRouteKey.current = null;
|
||||
if (isNewRoute || isEditRoute) {
|
||||
navigate("/auth", { replace: true });
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!isNewRoute) return;
|
||||
if (openedRouteKey.current === "new") return;
|
||||
openedRouteKey.current = "new";
|
||||
setEditor({ mode: "add" });
|
||||
}, [isNewRoute]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isEditRoute) {
|
||||
if (!isNewRoute) openedRouteKey.current = null;
|
||||
return;
|
||||
}
|
||||
if (isLoading) return;
|
||||
const key = `edit:${routeName}`;
|
||||
if (openedRouteKey.current === key) return;
|
||||
openedRouteKey.current = key;
|
||||
const auth = auths.find((a) => a.name === routeName);
|
||||
if (!auth) {
|
||||
setEditor({ mode: "add" });
|
||||
return;
|
||||
}
|
||||
setEditor({ mode: "edit", auth });
|
||||
}, [isEditRoute, isNewRoute, routeName, isLoading, auths]);
|
||||
const { editor, closeEditor } = useRouteDrivenModal({
|
||||
isNewRoute,
|
||||
isEditRoute,
|
||||
listPath: "/auth",
|
||||
buildNewEditor: () => ({ mode: "add" }),
|
||||
editRouteKey: () => `edit:${routeName}`,
|
||||
canOpenEdit: () => !isLoading,
|
||||
buildEditEditor: () => {
|
||||
const auth = auths.find((a) => a.name === routeName);
|
||||
if (!auth) return { mode: "add" };
|
||||
return { mode: "edit", auth };
|
||||
},
|
||||
editDeps: [routeName, isLoading, auths],
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
|
||||
@@ -7,6 +7,7 @@ import { FormSelect } from "../components/FormControls.jsx";
|
||||
import { Modal } from "../components/Modal.jsx";
|
||||
import { ProfileEditorModal } from "../components/ProfileEditorModal.jsx";
|
||||
import { ScriptIcon } from "../components/ScriptIcon.jsx";
|
||||
import { useRouteDrivenModal } from "../hooks/useRouteDrivenModal.js";
|
||||
import { formatTime } from "../lib/format.jsx";
|
||||
|
||||
function previewConfig(config) {
|
||||
@@ -42,84 +43,74 @@ export function ProfilesPage() {
|
||||
);
|
||||
const { data: profiles = [], isLoading } = useProfiles(ownerFilter || undefined);
|
||||
const del = useDeleteProfile();
|
||||
const [editor, setEditor] = useState(null);
|
||||
const [confirmDelete, setConfirmDelete] = useState(null);
|
||||
const [highlightName, setHighlightName] = useState(() => routeName || "");
|
||||
const highlightRef = useRef(null);
|
||||
const openedRouteKey = useRef(null);
|
||||
|
||||
const listPath = ownerFilter
|
||||
? `/profiles?owner=${encodeURIComponent(ownerFilter)}`
|
||||
: "/profiles";
|
||||
|
||||
function closeEditor() {
|
||||
setEditor(null);
|
||||
openedRouteKey.current = null;
|
||||
if (isNewRoute || isEditRoute) {
|
||||
navigate(listPath, { replace: true });
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!isNewRoute) return;
|
||||
const key = `new:${params.get("owner") || ""}:${params.get("name") || ""}`;
|
||||
if (openedRouteKey.current === key) return;
|
||||
if (!params.get("owner") && !ownerFilter && owners.length === 0) return;
|
||||
const owner = params.get("owner") || ownerFilter || owners[0] || "default";
|
||||
if (params.get("owner")) setOwnerFilter(params.get("owner"));
|
||||
openedRouteKey.current = key;
|
||||
setEditor({
|
||||
mode: "add",
|
||||
initial: {
|
||||
owner,
|
||||
name: params.get("name") || "",
|
||||
script: params.get("script") || "",
|
||||
config: {},
|
||||
description: "",
|
||||
},
|
||||
});
|
||||
}, [isNewRoute, params, owners, ownerFilter]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isEditRoute) {
|
||||
if (!isNewRoute) openedRouteKey.current = null;
|
||||
return;
|
||||
}
|
||||
if (ownerFilter !== routeOwner) {
|
||||
setOwnerFilter(routeOwner);
|
||||
return;
|
||||
}
|
||||
if (isLoading) return;
|
||||
const key = `edit:${routeOwner}/${routeName}`;
|
||||
if (openedRouteKey.current === key) return;
|
||||
openedRouteKey.current = key;
|
||||
setHighlightName(routeName);
|
||||
const row = profiles.find((p) => p.owner === routeOwner && p.name === routeName);
|
||||
if (row) {
|
||||
setEditor({
|
||||
mode: "edit",
|
||||
const { editor, closeEditor } = useRouteDrivenModal({
|
||||
isNewRoute,
|
||||
isEditRoute,
|
||||
listPath,
|
||||
newRouteKey: () => `new:${params.get("owner") || ""}:${params.get("name") || ""}`,
|
||||
canOpenNew: () => Boolean(params.get("owner") || ownerFilter || owners.length > 0),
|
||||
onBeforeOpenNew: () => {
|
||||
if (params.get("owner")) setOwnerFilter(params.get("owner"));
|
||||
},
|
||||
buildNewEditor: () => {
|
||||
const owner = params.get("owner") || ownerFilter || owners[0] || "default";
|
||||
return {
|
||||
mode: "add",
|
||||
initial: {
|
||||
owner: row.owner,
|
||||
name: row.name,
|
||||
script: row.script,
|
||||
config: row.config,
|
||||
description: row.description,
|
||||
owner,
|
||||
name: params.get("name") || "",
|
||||
script: params.get("script") || "",
|
||||
config: {},
|
||||
description: "",
|
||||
},
|
||||
usageCount: row.usageCount ?? 0,
|
||||
});
|
||||
return;
|
||||
}
|
||||
setEditor({
|
||||
mode: "add",
|
||||
initial: {
|
||||
owner: routeOwner,
|
||||
name: routeName,
|
||||
script: "",
|
||||
config: {},
|
||||
description: "",
|
||||
},
|
||||
});
|
||||
}, [isEditRoute, isNewRoute, routeOwner, routeName, ownerFilter, isLoading, profiles]);
|
||||
};
|
||||
},
|
||||
editRouteKey: () => `edit:${routeOwner}/${routeName}`,
|
||||
canOpenEdit: () => {
|
||||
if (ownerFilter !== routeOwner) {
|
||||
setOwnerFilter(routeOwner);
|
||||
return false;
|
||||
}
|
||||
return !isLoading;
|
||||
},
|
||||
onOpenEdit: () => setHighlightName(routeName),
|
||||
buildEditEditor: () => {
|
||||
const row = profiles.find((p) => p.owner === routeOwner && p.name === routeName);
|
||||
if (row) {
|
||||
return {
|
||||
mode: "edit",
|
||||
initial: {
|
||||
owner: row.owner,
|
||||
name: row.name,
|
||||
script: row.script,
|
||||
config: row.config,
|
||||
description: row.description,
|
||||
},
|
||||
usageCount: row.usageCount ?? 0,
|
||||
};
|
||||
}
|
||||
return {
|
||||
mode: "add",
|
||||
initial: {
|
||||
owner: routeOwner,
|
||||
name: routeName,
|
||||
script: "",
|
||||
config: {},
|
||||
description: "",
|
||||
},
|
||||
};
|
||||
},
|
||||
newDeps: [params, owners, ownerFilter],
|
||||
editDeps: [routeOwner, routeName, ownerFilter, isLoading, profiles],
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!highlightName || isLoading) return;
|
||||
|
||||
@@ -6,6 +6,7 @@ import { useDeleteSecret, useOwners, useSecrets } from "../api/hooks.js";
|
||||
import { ConfirmDialog } from "../components/ConfirmDialog.jsx";
|
||||
import { FormSelect } from "../components/FormControls.jsx";
|
||||
import { SecretEditorModal } from "../components/SecretEditorModal.jsx";
|
||||
import { useRouteDrivenModal } from "../hooks/useRouteDrivenModal.js";
|
||||
import { formatTime } from "../lib/format.jsx";
|
||||
|
||||
export function SecretsPage() {
|
||||
@@ -22,56 +23,46 @@ export function SecretsPage() {
|
||||
);
|
||||
const { data: secrets = [], isLoading } = useSecrets(ownerFilter || undefined);
|
||||
const del = useDeleteSecret();
|
||||
const [editor, setEditor] = useState(null);
|
||||
const [confirmDelete, setConfirmDelete] = useState(null);
|
||||
const [highlightName, setHighlightName] = useState(() => routeName || "");
|
||||
const highlightRef = useRef(null);
|
||||
const openedRouteKey = useRef(null);
|
||||
|
||||
const listPath = ownerFilter
|
||||
? `/secrets?owner=${encodeURIComponent(ownerFilter)}`
|
||||
: "/secrets";
|
||||
|
||||
function closeEditor() {
|
||||
setEditor(null);
|
||||
openedRouteKey.current = null;
|
||||
if (isNewRoute || isEditRoute) {
|
||||
navigate(listPath, { replace: true });
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!isNewRoute) return;
|
||||
const key = `new:${params.get("owner") || ""}:${params.get("name") || ""}`;
|
||||
if (openedRouteKey.current === key) return;
|
||||
if (!params.get("owner") && !ownerFilter && owners.length === 0) return;
|
||||
const owner = params.get("owner") || ownerFilter || owners[0] || "default";
|
||||
if (params.get("owner")) setOwnerFilter(params.get("owner"));
|
||||
openedRouteKey.current = key;
|
||||
setEditor({
|
||||
mode: "add",
|
||||
initial: { owner, name: params.get("name") || "" },
|
||||
});
|
||||
}, [isNewRoute, params, owners, ownerFilter]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isEditRoute) {
|
||||
if (!isNewRoute) openedRouteKey.current = null;
|
||||
return;
|
||||
}
|
||||
if (ownerFilter !== routeOwner) {
|
||||
setOwnerFilter(routeOwner);
|
||||
return;
|
||||
}
|
||||
const key = `edit:${routeOwner}/${routeName}`;
|
||||
if (openedRouteKey.current === key) return;
|
||||
openedRouteKey.current = key;
|
||||
setHighlightName(routeName);
|
||||
setEditor({
|
||||
const { editor, closeEditor } = useRouteDrivenModal({
|
||||
isNewRoute,
|
||||
isEditRoute,
|
||||
listPath,
|
||||
newRouteKey: () => `new:${params.get("owner") || ""}:${params.get("name") || ""}`,
|
||||
canOpenNew: () => Boolean(params.get("owner") || ownerFilter || owners.length > 0),
|
||||
onBeforeOpenNew: () => {
|
||||
if (params.get("owner")) setOwnerFilter(params.get("owner"));
|
||||
},
|
||||
buildNewEditor: () => {
|
||||
const owner = params.get("owner") || ownerFilter || owners[0] || "default";
|
||||
return {
|
||||
mode: "add",
|
||||
initial: { owner, name: params.get("name") || "" },
|
||||
};
|
||||
},
|
||||
editRouteKey: () => `edit:${routeOwner}/${routeName}`,
|
||||
canOpenEdit: () => {
|
||||
if (ownerFilter !== routeOwner) {
|
||||
setOwnerFilter(routeOwner);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
onOpenEdit: () => setHighlightName(routeName),
|
||||
buildEditEditor: () => ({
|
||||
mode: "replace",
|
||||
initial: { owner: routeOwner, name: routeName },
|
||||
});
|
||||
}, [isEditRoute, isNewRoute, routeOwner, routeName, ownerFilter]);
|
||||
}),
|
||||
newDeps: [params, owners, ownerFilter],
|
||||
editDeps: [routeOwner, routeName, ownerFilter],
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!highlightName || isLoading) return;
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useState } from "react";
|
||||
import { useLocation, useNavigate, useParams } from "react-router-dom";
|
||||
import { LuPencil, LuPlus, LuTrash2 } from "react-icons/lu";
|
||||
import { errorMessage } from "../api/client.js";
|
||||
import { useDeleteUser, useUsers } from "../api/hooks.js";
|
||||
import { ConfirmDialog } from "../components/ConfirmDialog.jsx";
|
||||
import { UserEditorModal } from "../components/UserEditorModal.jsx";
|
||||
import { useRouteDrivenModal } from "../hooks/useRouteDrivenModal.js";
|
||||
|
||||
export function UsersPage() {
|
||||
const navigate = useNavigate();
|
||||
@@ -15,41 +16,22 @@ export function UsersPage() {
|
||||
|
||||
const { data: users = [], isLoading } = useUsers();
|
||||
const del = useDeleteUser();
|
||||
const [editor, setEditor] = useState(null);
|
||||
const [confirmDelete, setConfirmDelete] = useState(null);
|
||||
const openedRouteKey = useRef(null);
|
||||
|
||||
function closeEditor() {
|
||||
setEditor(null);
|
||||
openedRouteKey.current = null;
|
||||
if (isNewRoute || isEditRoute) {
|
||||
navigate("/users", { replace: true });
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!isNewRoute) return;
|
||||
if (openedRouteKey.current === "new") return;
|
||||
openedRouteKey.current = "new";
|
||||
setEditor({ mode: "add" });
|
||||
}, [isNewRoute]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isEditRoute) {
|
||||
if (!isNewRoute) openedRouteKey.current = null;
|
||||
return;
|
||||
}
|
||||
if (isLoading) return;
|
||||
const key = `edit:${routeUsername}`;
|
||||
if (openedRouteKey.current === key) return;
|
||||
openedRouteKey.current = key;
|
||||
const user = users.find((u) => u.username === routeUsername);
|
||||
if (!user) {
|
||||
setEditor({ mode: "add" });
|
||||
return;
|
||||
}
|
||||
setEditor({ mode: "edit", user });
|
||||
}, [isEditRoute, isNewRoute, routeUsername, isLoading, users]);
|
||||
const { editor, closeEditor } = useRouteDrivenModal({
|
||||
isNewRoute,
|
||||
isEditRoute,
|
||||
listPath: "/users",
|
||||
buildNewEditor: () => ({ mode: "add" }),
|
||||
editRouteKey: () => `edit:${routeUsername}`,
|
||||
canOpenEdit: () => !isLoading,
|
||||
buildEditEditor: () => {
|
||||
const user = users.find((u) => u.username === routeUsername);
|
||||
if (!user) return { mode: "add" };
|
||||
return { mode: "edit", user };
|
||||
},
|
||||
editDeps: [routeUsername, isLoading, users],
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
|
||||
@@ -6,6 +6,7 @@ import { useDeleteVariable, useOwners, useVariables } from "../api/hooks.js";
|
||||
import { ConfirmDialog } from "../components/ConfirmDialog.jsx";
|
||||
import { FormSelect } from "../components/FormControls.jsx";
|
||||
import { VariableEditorModal } from "../components/VariableEditorModal.jsx";
|
||||
import { useRouteDrivenModal } from "../hooks/useRouteDrivenModal.js";
|
||||
import { formatTime } from "../lib/format.jsx";
|
||||
|
||||
function displayValue(value) {
|
||||
@@ -27,80 +28,70 @@ export function VariablesPage() {
|
||||
);
|
||||
const { data: variables = [], isLoading } = useVariables(ownerFilter || undefined);
|
||||
const del = useDeleteVariable();
|
||||
const [editor, setEditor] = useState(null);
|
||||
const [confirmDelete, setConfirmDelete] = useState(null);
|
||||
const [highlightName, setHighlightName] = useState(() => routeName || "");
|
||||
const highlightRef = useRef(null);
|
||||
const openedRouteKey = useRef(null);
|
||||
|
||||
const listPath = ownerFilter
|
||||
? `/variables?owner=${encodeURIComponent(ownerFilter)}`
|
||||
: "/variables";
|
||||
|
||||
function closeEditor() {
|
||||
setEditor(null);
|
||||
openedRouteKey.current = null;
|
||||
if (isNewRoute || isEditRoute) {
|
||||
navigate(listPath, { replace: true });
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!isNewRoute) return;
|
||||
const key = `new:${params.get("owner") || ""}:${params.get("name") || ""}`;
|
||||
if (openedRouteKey.current === key) return;
|
||||
if (!params.get("owner") && !ownerFilter && owners.length === 0) return;
|
||||
const owner = params.get("owner") || ownerFilter || owners[0] || "default";
|
||||
if (params.get("owner")) setOwnerFilter(params.get("owner"));
|
||||
openedRouteKey.current = key;
|
||||
setEditor({
|
||||
mode: "add",
|
||||
initial: {
|
||||
owner,
|
||||
name: params.get("name") || "",
|
||||
type: "string",
|
||||
value: "",
|
||||
},
|
||||
});
|
||||
}, [isNewRoute, params, owners, ownerFilter]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isEditRoute) {
|
||||
if (!isNewRoute) openedRouteKey.current = null;
|
||||
return;
|
||||
}
|
||||
if (ownerFilter !== routeOwner) {
|
||||
setOwnerFilter(routeOwner);
|
||||
return;
|
||||
}
|
||||
if (isLoading) return;
|
||||
const key = `edit:${routeOwner}/${routeName}`;
|
||||
if (openedRouteKey.current === key) return;
|
||||
openedRouteKey.current = key;
|
||||
setHighlightName(routeName);
|
||||
const row = variables.find((v) => v.owner === routeOwner && v.name === routeName);
|
||||
if (row) {
|
||||
setEditor({
|
||||
mode: "edit",
|
||||
const { editor, closeEditor } = useRouteDrivenModal({
|
||||
isNewRoute,
|
||||
isEditRoute,
|
||||
listPath,
|
||||
newRouteKey: () => `new:${params.get("owner") || ""}:${params.get("name") || ""}`,
|
||||
canOpenNew: () => Boolean(params.get("owner") || ownerFilter || owners.length > 0),
|
||||
onBeforeOpenNew: () => {
|
||||
if (params.get("owner")) setOwnerFilter(params.get("owner"));
|
||||
},
|
||||
buildNewEditor: () => {
|
||||
const owner = params.get("owner") || ownerFilter || owners[0] || "default";
|
||||
return {
|
||||
mode: "add",
|
||||
initial: {
|
||||
owner: row.owner,
|
||||
name: row.name,
|
||||
type: row.type,
|
||||
value: row.value,
|
||||
owner,
|
||||
name: params.get("name") || "",
|
||||
type: "string",
|
||||
value: "",
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
setEditor({
|
||||
mode: "add",
|
||||
initial: {
|
||||
owner: routeOwner,
|
||||
name: routeName,
|
||||
type: "string",
|
||||
value: "",
|
||||
},
|
||||
});
|
||||
}, [isEditRoute, isNewRoute, routeOwner, routeName, ownerFilter, isLoading, variables]);
|
||||
};
|
||||
},
|
||||
editRouteKey: () => `edit:${routeOwner}/${routeName}`,
|
||||
canOpenEdit: () => {
|
||||
if (ownerFilter !== routeOwner) {
|
||||
setOwnerFilter(routeOwner);
|
||||
return false;
|
||||
}
|
||||
return !isLoading;
|
||||
},
|
||||
onOpenEdit: () => setHighlightName(routeName),
|
||||
buildEditEditor: () => {
|
||||
const row = variables.find((v) => v.owner === routeOwner && v.name === routeName);
|
||||
if (row) {
|
||||
return {
|
||||
mode: "edit",
|
||||
initial: {
|
||||
owner: row.owner,
|
||||
name: row.name,
|
||||
type: row.type,
|
||||
value: row.value,
|
||||
},
|
||||
};
|
||||
}
|
||||
return {
|
||||
mode: "add",
|
||||
initial: {
|
||||
owner: routeOwner,
|
||||
name: routeName,
|
||||
type: "string",
|
||||
value: "",
|
||||
},
|
||||
};
|
||||
},
|
||||
newDeps: [params, owners, ownerFilter],
|
||||
editDeps: [routeOwner, routeName, ownerFilter, isLoading, variables],
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!highlightName || isLoading) return;
|
||||
|
||||
Reference in New Issue
Block a user