From 96c4cc7b4751e9324ec6c46c0d017babae04f154 Mon Sep 17 00:00:00 2001 From: Nasyarobby Putra Date: Sat, 22 Aug 2026 00:48:44 +0700 Subject: [PATCH] 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 --- packages/web/CONTRIBUTING.md | 42 ++++++ .../components/workflow/AddTriggerDialog.jsx | 68 ++++++++++ .../src/components/workflow/TriggerCard.jsx | 65 --------- .../src/components/workflow/TriggersTab.jsx | 3 +- packages/web/src/hooks/useRouteDrivenModal.js | 86 ++++++++++++ packages/web/src/pages/AuthProfilesPage.jsx | 50 +++---- packages/web/src/pages/ProfilesPage.jsx | 127 ++++++++---------- packages/web/src/pages/SecretsPage.jsx | 71 +++++----- packages/web/src/pages/UsersPage.jsx | 50 +++---- packages/web/src/pages/VariablesPage.jsx | 119 ++++++++-------- 10 files changed, 375 insertions(+), 306 deletions(-) create mode 100644 packages/web/CONTRIBUTING.md create mode 100644 packages/web/src/components/workflow/AddTriggerDialog.jsx create mode 100644 packages/web/src/hooks/useRouteDrivenModal.js diff --git a/packages/web/CONTRIBUTING.md b/packages/web/CONTRIBUTING.md new file mode 100644 index 0000000..1f83885 --- /dev/null +++ b/packages/web/CONTRIBUTING.md @@ -0,0 +1,42 @@ +# Contributing (web UI) + +Conventions for editors, dialogs, and routing in `packages/web`. + +## Entity editing patterns + +### Modal + route deep-link (simple entities) + +Use a list page with a route-driven editor modal for: + +- Secrets (`/secrets`, `/secrets/new`, `/secrets/:owner/:name/edit`) +- Variables +- Profiles +- Auth profiles (`/auth`, …) +- Users + +Prefer [`useRouteDrivenModal`](src/hooks/useRouteDrivenModal.js) to open/close the modal from the URL, keep a single `openedRouteKey` guard, and navigate back to the list path on close. + +Navigate to `/…/new` or `/…/:id/edit` from list actions; do not open the editor with local state alone when a deep-link route exists. + +### Full-page editors + +Scripts and workflows use dedicated full-page editors (not list+modal). Keep create/edit as their own routes and leave the list page for browsing only. + +### Known exception: Responses + +The Responses page may keep inline editing (no modal / no full-page editor). Treat that as intentional; do not refactor it to modal+route unless product requirements change. + +## Components + +| Use | For | +|---|---| +| `ConfirmDialog` | Deletes and other yes/no confirmations | +| `FormInput` / `FormSelect` | Form fields in modals and pages | +| `Modal` | Custom dialog chrome when `ConfirmDialog` is not enough | + +## Naming: `*Dialog` vs `*Modal` + +- **`*Dialog`** — confirmations and pickers (e.g. `ConfirmDialog`, `AddTriggerDialog`, `AddScriptDialog`, `DuplicateWorkflowDialog`) +- **`*Modal`** — entity editors (e.g. `SecretEditorModal`, `ProfileEditorModal`, `AuthEditorModal`) + +When adding a new overlay, pick the suffix from the table above. Do not rename existing `*EditorModal` components solely for consistency with older code. diff --git a/packages/web/src/components/workflow/AddTriggerDialog.jsx b/packages/web/src/components/workflow/AddTriggerDialog.jsx new file mode 100644 index 0000000..a7424c2 --- /dev/null +++ b/packages/web/src/components/workflow/AddTriggerDialog.jsx @@ -0,0 +1,68 @@ +import { useState } from "react"; + +export function AddTriggerDialog({ open, onClose, onPick }) { + const [kind, setKind] = useState("HTTP"); + if (!open) return null; + + return ( + +
+

Add trigger

+
+ + + +
+
+ + +
+
+
+ +
+
+ ); +} diff --git a/packages/web/src/components/workflow/TriggerCard.jsx b/packages/web/src/components/workflow/TriggerCard.jsx index 8296d75..581f274 100644 --- a/packages/web/src/components/workflow/TriggerCard.jsx +++ b/packages/web/src/components/workflow/TriggerCard.jsx @@ -519,68 +519,3 @@ function FailureAlertFields({ trigger, disabled, onChange, alertDestinations }) ); } - -export function AddTriggerDialog({ open, onClose, onPick }) { - const [kind, setKind] = useState("HTTP"); - if (!open) return null; - - return ( - -
-

Add trigger

-
- - - -
-
- - -
-
-
- -
-
- ); -} diff --git a/packages/web/src/components/workflow/TriggersTab.jsx b/packages/web/src/components/workflow/TriggersTab.jsx index 977108d..00028cf 100644 --- a/packages/web/src/components/workflow/TriggersTab.jsx +++ b/packages/web/src/components/workflow/TriggersTab.jsx @@ -15,7 +15,8 @@ import { } from "@dnd-kit/sortable"; import { LuPlus } from "react-icons/lu"; import { newCronTrigger, newHttpTrigger, newWorkflowTrigger } from "../../lib/workflow-doc.js"; -import { AddTriggerDialog, TriggerCard } from "./TriggerCard.jsx"; +import { AddTriggerDialog } from "./AddTriggerDialog.jsx"; +import { TriggerCard } from "./TriggerCard.jsx"; export function TriggersTab({ doc, diff --git a/packages/web/src/hooks/useRouteDrivenModal.js b/packages/web/src/hooks/useRouteDrivenModal.js new file mode 100644 index 0000000..8406984 --- /dev/null +++ b/packages/web/src/hooks/useRouteDrivenModal.js @@ -0,0 +1,86 @@ +import { useEffect, useRef, useState } from "react"; +import { useNavigate } from "react-router-dom"; + +const EMPTY_DEPS = Object.freeze([]); + +/** + * Open/close an editor modal driven by list / new / edit routes. + * + * Pages supply route flags, list path, and builders for the editor payload. + * The hook owns `editor` state, the opened-route guard ref, and navigation + * back to the list when closing from a deep-linked route. + * + * @param {object} options + * @param {boolean} options.isNewRoute + * @param {boolean} options.isEditRoute + * @param {string | (() => string)} options.listPath + * @param {() => string} [options.newRouteKey] defaults to `"new"` + * @param {() => boolean} [options.canOpenNew] return false to wait (e.g. owners loading) + * @param {() => void} [options.onBeforeOpenNew] side effects before opening (e.g. sync filter) + * @param {() => object} options.buildNewEditor + * @param {() => string} options.editRouteKey + * @param {() => boolean} [options.canOpenEdit] return false to wait / prepare (e.g. sync owner) + * @param {() => void} [options.onOpenEdit] after key is claimed (e.g. highlight row) + * @param {() => object} options.buildEditEditor + * @param {unknown[]} [options.newDeps] extra deps for the new-route effect + * @param {unknown[]} [options.editDeps] extra deps for the edit-route effect + */ +export function useRouteDrivenModal({ + isNewRoute, + isEditRoute, + listPath, + newRouteKey = () => "new", + canOpenNew, + onBeforeOpenNew, + buildNewEditor, + editRouteKey, + canOpenEdit, + onOpenEdit, + buildEditEditor, + newDeps = EMPTY_DEPS, + editDeps = EMPTY_DEPS, +}) { + const navigate = useNavigate(); + const [editor, setEditor] = useState(null); + const openedRouteKey = useRef(null); + + function resolveListPath() { + return typeof listPath === "function" ? listPath() : listPath; + } + + function closeEditor() { + setEditor(null); + openedRouteKey.current = null; + if (isNewRoute || isEditRoute) { + navigate(resolveListPath(), { replace: true }); + } + } + + useEffect(() => { + if (!isNewRoute) return; + const key = newRouteKey(); + if (openedRouteKey.current === key) return; + if (canOpenNew && !canOpenNew()) return; + onBeforeOpenNew?.(); + openedRouteKey.current = key; + setEditor(buildNewEditor()); + // Callers pass explicit newDeps; builders close over latest render values. + // eslint-disable-next-line react-hooks/exhaustive-deps -- intentional + }, [isNewRoute, ...newDeps]); + + useEffect(() => { + if (!isEditRoute) { + if (!isNewRoute) openedRouteKey.current = null; + return; + } + if (canOpenEdit && !canOpenEdit()) return; + const key = editRouteKey(); + if (openedRouteKey.current === key) return; + openedRouteKey.current = key; + onOpenEdit?.(); + setEditor(buildEditEditor()); + // eslint-disable-next-line react-hooks/exhaustive-deps -- intentional + }, [isEditRoute, isNewRoute, ...editDeps]); + + return { editor, setEditor, closeEditor, openedRouteKey }; +} diff --git a/packages/web/src/pages/AuthProfilesPage.jsx b/packages/web/src/pages/AuthProfilesPage.jsx index 06f1922..446d9a3 100644 --- a/packages/web/src/pages/AuthProfilesPage.jsx +++ b/packages/web/src/pages/AuthProfilesPage.jsx @@ -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>, 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 (
diff --git a/packages/web/src/pages/ProfilesPage.jsx b/packages/web/src/pages/ProfilesPage.jsx index 0fb4314..12f5224 100644 --- a/packages/web/src/pages/ProfilesPage.jsx +++ b/packages/web/src/pages/ProfilesPage.jsx @@ -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; diff --git a/packages/web/src/pages/SecretsPage.jsx b/packages/web/src/pages/SecretsPage.jsx index 5333ead..7bc5325 100644 --- a/packages/web/src/pages/SecretsPage.jsx +++ b/packages/web/src/pages/SecretsPage.jsx @@ -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; diff --git a/packages/web/src/pages/UsersPage.jsx b/packages/web/src/pages/UsersPage.jsx index 114ddb7..9b5acd0 100644 --- a/packages/web/src/pages/UsersPage.jsx +++ b/packages/web/src/pages/UsersPage.jsx @@ -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 (
diff --git a/packages/web/src/pages/VariablesPage.jsx b/packages/web/src/pages/VariablesPage.jsx index 48b5bf5..7266ee4 100644 --- a/packages/web/src/pages/VariablesPage.jsx +++ b/packages/web/src/pages/VariablesPage.jsx @@ -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;