Added support for redirecting to a specific control

This commit is contained in:
Colin Dawson 2026-02-01 00:13:01 +00:00
parent c7428be21b
commit 49bac1091a
7 changed files with 135 additions and 55 deletions

View File

@ -12,6 +12,7 @@ import LoginForm from "./modules/frame/components/LoginForm";
import Redirect from "./components/common/Redirect"; import Redirect from "./components/common/Redirect";
import Mainframe from "./modules/frame/components/Mainframe"; import Mainframe from "./modules/frame/components/Mainframe";
import EmailUserAction from "./modules/frame/components/EmailUserAction"; import EmailUserAction from "./modules/frame/components/EmailUserAction";
import { HashNavigationProvider } from "./utils/HashNavigationContext";
import HomePage from "./modules/homepage/HomePage"; import HomePage from "./modules/homepage/HomePage";
import Profile from "./modules/profile/Profile"; import Profile from "./modules/profile/Profile";
@ -458,6 +459,7 @@ function App() {
return ( return (
<HelmetProvider> <HelmetProvider>
<HashNavigationProvider>
<Helmet htmlAttributes={htmlAttributes}> <Helmet htmlAttributes={htmlAttributes}>
<title>{config.applicationName}</title> <title>{config.applicationName}</title>
</Helmet> </Helmet>
@ -494,6 +496,7 @@ function App() {
</Routes> </Routes>
<ToastContainer /> <ToastContainer />
</main> </main>
</HashNavigationProvider>
</HelmetProvider> </HelmetProvider>
); );
} }

View File

@ -1,35 +1,53 @@
import React, { useEffect, useState, useCallback, useMemo } from "react"; import React, { useEffect, useState, useCallback, useMemo } from "react";
import { useLocation } from "react-router-dom"; import { useHashSegment } from "../../utils/HashNavigationContext";
import TabHeader from "./TabHeader"; import TabHeader from "./TabHeader";
interface HorizontalTabsProps { interface HorizontalTabsProps {
children: JSX.Element[]; children: JSX.Element[];
initialTab?: string; initialTab?: string;
hashSegment?: number;
} }
const HorizontalTabs: React.FC<HorizontalTabsProps> = ({ const HorizontalTabs: React.FC<HorizontalTabsProps> = ({
children, children,
initialTab, initialTab,
hashSegment,
}) => { }) => {
const location = useLocation(); const hashValue = useHashSegment(
hashSegment !== undefined ? hashSegment : -1,
);
const [activeTab, setActiveTab] = useState<string>(""); const [activeTab, setActiveTab] = useState<string>("");
// Set initial tab on mount // Set initial tab on mount
useEffect(() => { useEffect(() => {
if (children.length > 0) { if (children.length > 0) {
// Check for hash in URL first, then fall back to initialTab prop // Only use hash if hashSegment was explicitly provided
const hashTab = location.hash.slice(1); // Remove the # character const useHash = hashSegment !== undefined;
const tabToSelect = hashTab || initialTab || children[0].props.id;
// Validate that the hash matches one of our tab IDs
const hashMatchesTab =
useHash &&
hashValue &&
children.some((child) => child.props.id === hashValue);
// Use id if available, otherwise fall back to label
const firstTabId = children[0].props.id || children[0].props.label;
const tabToSelect =
(hashMatchesTab ? hashValue : initialTab) || firstTabId;
setActiveTab(tabToSelect); setActiveTab(tabToSelect);
} }
}, [children, initialTab, location.hash]); }, [children, initialTab, hashValue, hashSegment]);
const onClickTabItem = useCallback((tab: string) => { const onClickTabItem = useCallback((tab: string) => {
setActiveTab((prev) => (prev !== tab ? tab : prev)); setActiveTab((prev) => (prev !== tab ? tab : prev));
}, []); }, []);
const activeTabChildren = useMemo(() => { const activeTabChildren = useMemo(() => {
const match = children.find((child) => child.props.id === activeTab); const match = children.find((child) => {
const tabId = child.props.id || child.props.label;
return tabId === activeTab;
});
return match ? match.props.children : <></>; return match ? match.props.children : <></>;
}, [children, activeTab]); }, [children, activeTab]);
@ -43,12 +61,13 @@ const HorizontalTabs: React.FC<HorizontalTabsProps> = ({
<ul className="tab-list"> <ul className="tab-list">
{children.map((child) => { {children.map((child) => {
const { id, label } = child.props; const { id, label } = child.props;
const tabId = id || label;
return ( return (
<TabHeader <TabHeader
key={label} key={label}
label={label} label={label}
isActive={id === activeTab} isActive={tabId === activeTab}
onClick={() => onClickTabItem(id)} onClick={() => onClickTabItem(tabId)}
/> />
); );
})} })}

View File

@ -53,7 +53,9 @@ const DomainsDetails: React.FC<DomainsDetailsProps> = ({ isEditMode }) => {
return ( return (
<div> <div>
<h1>{heading}</h1> <h1>{heading}</h1>
<HorizontalTabs initialTab={initialTab}>{tabs}</HorizontalTabs> <HorizontalTabs initialTab={initialTab} hashSegment={0}>
{tabs}
</HorizontalTabs>
</div> </div>
); );
}; };

View File

@ -53,9 +53,9 @@ const AddUserToRole: React.FC<LocAddUserToRoleProps> = ({ isEditMode }) => {
if (response) { if (response) {
toast.info(t("UserAddedToRole")); toast.info(t("UserAddedToRole"));
if (buttonName === labelSave) if (buttonName === "save")
form.setState({ form.setState({
redirect: `/domains/edit/${domainId}?tab=SecurityRoles&roleId=${roleId}&innerTab=Users`, redirect: `/domains/edit/${domainId}#securityRoles/${roleId}/users`,
}); });
} }
} catch (ex: any) { } catch (ex: any) {

View File

@ -10,6 +10,7 @@ import RolesTable from "./RolesTable";
import Button, { ButtonType } from "../../../../components/common/Button"; import Button, { ButtonType } from "../../../../components/common/Button";
import Loading from "../../../../components/common/Loading"; import Loading from "../../../../components/common/Loading";
import Permission from "../../../../components/common/Permission"; import Permission from "../../../../components/common/Permission";
import { useHashSegment } from "../../../../utils/HashNavigationContext";
const initialPagedData: Paginated<GetRoleResponse> = { const initialPagedData: Paginated<GetRoleResponse> = {
page: 1, page: 1,
@ -24,6 +25,7 @@ interface RolesEditorProps {
onSelectRole?: (keyValue: any) => void; onSelectRole?: (keyValue: any) => void;
onUnselectRole?: () => void; onUnselectRole?: () => void;
initialRoleId?: string; initialRoleId?: string;
hashSegment?: number;
} }
const RolesEditor: React.FC<RolesEditorProps> = ({ const RolesEditor: React.FC<RolesEditorProps> = ({
@ -31,8 +33,12 @@ const RolesEditor: React.FC<RolesEditorProps> = ({
onSelectRole, onSelectRole,
onUnselectRole, onUnselectRole,
initialRoleId, initialRoleId,
hashSegment,
}) => { }) => {
const { t } = useTranslation<typeof Namespaces.Common>(); const { t } = useTranslation<typeof Namespaces.Common>();
const hashRoleId = useHashSegment(
hashSegment !== undefined ? hashSegment : -1,
);
const [loaded, setLoaded] = useState(false); const [loaded, setLoaded] = useState(false);
const [pagedData, setPagedData] = const [pagedData, setPagedData] =
useState<Paginated<GetRoleResponse>>(initialPagedData); useState<Paginated<GetRoleResponse>>(initialPagedData);
@ -79,17 +85,20 @@ const RolesEditor: React.FC<RolesEditorProps> = ({
void loadInitial(); void loadInitial();
}, [changePage]); // eslint-disable-line react-hooks/exhaustive-deps }, [changePage]); // eslint-disable-line react-hooks/exhaustive-deps
// Auto-select role when initialRoleId is provided // Auto-select role when initialRoleId or hashRoleId is provided
useEffect(() => { useEffect(() => {
if (initialRoleId && pagedData.data.length > 0 && onSelectRole) { const roleIdToSelect =
hashSegment !== undefined ? hashRoleId : initialRoleId;
if (roleIdToSelect && pagedData.data.length > 0 && onSelectRole) {
const roleToSelect = pagedData.data.find( const roleToSelect = pagedData.data.find(
(role) => role.id.toString() === initialRoleId, (role) => role.id.toString() === roleIdToSelect,
); );
if (roleToSelect) { if (roleToSelect) {
onSelectRole(roleToSelect); onSelectRole(roleToSelect);
} }
} }
}, [initialRoleId, pagedData.data, onSelectRole]); }, [hashRoleId, initialRoleId, pagedData.data, onSelectRole, hashSegment]);
const onSort = async (nextSortColumn: Column<GetRoleResponse>) => { const onSort = async (nextSortColumn: Column<GetRoleResponse>) => {
const { page, pageSize } = pagedData; const { page, pageSize } = pagedData;

View File

@ -38,14 +38,14 @@ const SecurityRolesTab: React.FC<SecurityRolesTabProps> = ({
if (canViewRoleAccess) { if (canViewRoleAccess) {
tabs.push( tabs.push(
<Tab key={1} label={t("RoleAccess")}> <Tab id="roleAccess" key={1} label={t("RoleAccess")}>
<RoleAccessEditor role={selectedRole} /> <RoleAccessEditor role={selectedRole} />
</Tab>, </Tab>,
); );
} }
if (canViewRoleUsers) { if (canViewRoleUsers) {
tabs.push( tabs.push(
<Tab key={2} label={t("Users")}> <Tab id="users" key={2} label={t("Users")}>
<UserRoleEditor role={selectedRole} /> <UserRoleEditor role={selectedRole} />
</Tab>, </Tab>,
); );
@ -59,12 +59,15 @@ const SecurityRolesTab: React.FC<SecurityRolesTabProps> = ({
onSelectRole={onSelectRow} onSelectRole={onSelectRow}
onUnselectRole={onUnselectRow} onUnselectRole={onUnselectRow}
initialRoleId={initialRoleId} initialRoleId={initialRoleId}
hashSegment={1}
/> />
</div> </div>
<div> <div>
{selectedRole !== undefined && {selectedRole !== undefined &&
(canViewRoleAccess || canViewRoleUsers) && ( (canViewRoleAccess || canViewRoleUsers) && (
<HorizontalTabs initialTab={initialInnerTab}>{tabs}</HorizontalTabs> <HorizontalTabs initialTab={initialInnerTab} hashSegment={2}>
{tabs}
</HorizontalTabs>
)} )}
</div> </div>
</div> </div>

View File

@ -0,0 +1,44 @@
import React, { createContext, useContext, useMemo } from "react";
import { useLocation } from "react-router-dom";
interface HashNavigationContextValue {
segments: string[];
getSegment: (index: number) => string | undefined;
}
const HashNavigationContext = createContext<HashNavigationContextValue>({
segments: [],
getSegment: () => undefined,
});
export const HashNavigationProvider: React.FC<{
children: React.ReactNode;
}> = ({ children }) => {
const location = useLocation();
const value = useMemo(() => {
// Parse hash into segments, removing the leading #
const hash = location.hash.slice(1);
const segments = hash ? hash.split("/") : [];
return {
segments,
getSegment: (index: number) => segments[index],
};
}, [location.hash]);
return (
<HashNavigationContext.Provider value={value}>
{children}
</HashNavigationContext.Provider>
);
};
export const useHashNavigation = () => {
return useContext(HashNavigationContext);
};
export const useHashSegment = (index: number): string | undefined => {
const { getSegment } = useHashNavigation();
return getSegment(index);
};