From 05a10846a895de264b366b713f041e13f6940153 Mon Sep 17 00:00:00 2001 From: Albrecht Degering Date: Sat, 11 Jul 2026 00:55:23 +0200 Subject: [PATCH] Support deep links into IDM assignments --- webui/src/features/IdmPage.tsx | 31 ++++++++++++++++++++++++------- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/webui/src/features/IdmPage.tsx b/webui/src/features/IdmPage.tsx index 467b5bf..b48e859 100644 --- a/webui/src/features/IdmPage.tsx +++ b/webui/src/features/IdmPage.tsx @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useMemo, useState, type FormEvent } from "react"; +import { useCallback, useEffect, useMemo, useRef, useState, type FormEvent } from "react"; import { Edit3, RefreshCw, Save, XCircle } from "lucide-react"; import { ApiError, @@ -219,6 +219,15 @@ function sourceLabel(value: string): string { return SOURCE_OPTIONS.find((item) => item.value === value)?.label ?? value; } +function idmInitialQuery(): { assignmentId: string; functionId: string } { + if (typeof window === "undefined") return { assignmentId: "", functionId: "" }; + const params = new URLSearchParams(window.location.search); + return { + assignmentId: params.get("assignment_id") || "", + functionId: params.get("function_id") || "" + }; +} + export default function IdmPage({ settings, auth }: IdmPageProps) { const [model, setModel] = useState(EMPTY_MODEL); const [assignments, setAssignments] = useState([]); @@ -237,6 +246,8 @@ export default function IdmPage({ settings, auth }: IdmPageProps) { const [busy, setBusy] = useState(false); const [error, setError] = useState(""); const [success, setSuccess] = useState(""); + const initialQuery = useMemo(() => idmInitialQuery(), []); + const appliedInitialQueryRef = useRef(false); const canManage = hasScope(auth, "idm:organization_assignment:write") || hasScope(auth, "organizations:function:assign"); const canSearchIdentities = canManage || hasScope(auth, "idm:organization_identity:read") || hasScope(auth, "admin:users:read"); @@ -286,10 +297,7 @@ export default function IdmPage({ settings, auth }: IdmPageProps) { if (assignmentDraft.acting_for_account_id) values.add(assignmentDraft.acting_for_account_id); return Array.from(values).sort((left, right) => left.localeCompare(right)); }, [actingForOptionById, actingForOptions, assignmentDraft.acting_for_account_id, identityOptionById, sourceAssignment]); - const initialFunctionFilter = useMemo(() => { - if (typeof window === "undefined") return ""; - return new URLSearchParams(window.location.search).get("function_id") || ""; - }, []); + const initialFunctionFilter = initialQuery.functionId; const hasDirtyAssignmentDraft = isAssignmentDirty(assignmentDraft); const hasDirtySettingsDraft = canReadSettings && isSettingsDirty(settingsDraft, idmSettings); const hasDirtyDraft = hasDirtyAssignmentDraft || hasDirtySettingsDraft; @@ -307,7 +315,16 @@ export default function IdmPage({ settings, auth }: IdmPageProps) { setAssignments(nextAssignments.assignments); setIdmSettings(nextSettings); if (nextSettings) setSettingsDraft(settingsDraftFrom(nextSettings)); - if (initialFunctionFilter && nextModel.functions.some((item) => item.id === initialFunctionFilter)) { + if (!appliedInitialQueryRef.current && initialQuery.assignmentId) { + const initialAssignment = nextAssignments.assignments.find((item) => item.id === initialQuery.assignmentId); + if (initialAssignment) { + appliedInitialQueryRef.current = true; + setEditingAssignmentId(initialAssignment.id); + setAssignmentDraft(assignmentDraftFrom(initialAssignment)); + } + } + if (!appliedInitialQueryRef.current && initialFunctionFilter && nextModel.functions.some((item) => item.id === initialFunctionFilter)) { + appliedInitialQueryRef.current = true; setAssignmentDraft((draft) => draft.function_id ? draft : { ...draft, function_id: initialFunctionFilter }); } if (canSearchIdentities) { @@ -326,7 +343,7 @@ export default function IdmPage({ settings, auth }: IdmPageProps) { } finally { setLoading(false); } - }, [canReadSettings, canSearchIdentities, initialFunctionFilter, settings]); + }, [canReadSettings, canSearchIdentities, initialFunctionFilter, initialQuery.assignmentId, settings]); useEffect(() => { void loadData();