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 Mainframe from "./modules/frame/components/Mainframe";
import EmailUserAction from "./modules/frame/components/EmailUserAction";
import { HashNavigationProvider } from "./utils/HashNavigationContext";
import HomePage from "./modules/homepage/HomePage";
import Profile from "./modules/profile/Profile";
@ -458,42 +459,44 @@ function App() {
return (
<HelmetProvider>
<Helmet htmlAttributes={htmlAttributes}>
<title>{config.applicationName}</title>
</Helmet>
<main>
<Routes>
<Route path="/env" element={<EnvPage />} />
{loginRoute}
<Route
path="/forgot-password"
element={
<LoginFrame>
<ForgotPassword />
</LoginFrame>
}
/>
<Route
path="/404"
element={
<LoginFrame>
<NotFound />
</LoginFrame>
}
/>
<Route
path="/emailuseraction/:token"
element={
<LoginFrame>
<EmailUserAction />
</LoginFrame>
}
/>
{secureRoutes}
<Route path="*" element={<Navigate replace to="/404" />} />
</Routes>
<ToastContainer />
</main>
<HashNavigationProvider>
<Helmet htmlAttributes={htmlAttributes}>
<title>{config.applicationName}</title>
</Helmet>
<main>
<Routes>
<Route path="/env" element={<EnvPage />} />
{loginRoute}
<Route
path="/forgot-password"
element={
<LoginFrame>
<ForgotPassword />
</LoginFrame>
}
/>
<Route
path="/404"
element={
<LoginFrame>
<NotFound />
</LoginFrame>
}
/>
<Route
path="/emailuseraction/:token"
element={
<LoginFrame>
<EmailUserAction />
</LoginFrame>
}
/>
{secureRoutes}
<Route path="*" element={<Navigate replace to="/404" />} />
</Routes>
<ToastContainer />
</main>
</HashNavigationProvider>
</HelmetProvider>
);
}

View File

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

View File

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

View File

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

View File

@ -10,6 +10,7 @@ import RolesTable from "./RolesTable";
import Button, { ButtonType } from "../../../../components/common/Button";
import Loading from "../../../../components/common/Loading";
import Permission from "../../../../components/common/Permission";
import { useHashSegment } from "../../../../utils/HashNavigationContext";
const initialPagedData: Paginated<GetRoleResponse> = {
page: 1,
@ -24,6 +25,7 @@ interface RolesEditorProps {
onSelectRole?: (keyValue: any) => void;
onUnselectRole?: () => void;
initialRoleId?: string;
hashSegment?: number;
}
const RolesEditor: React.FC<RolesEditorProps> = ({
@ -31,8 +33,12 @@ const RolesEditor: React.FC<RolesEditorProps> = ({
onSelectRole,
onUnselectRole,
initialRoleId,
hashSegment,
}) => {
const { t } = useTranslation<typeof Namespaces.Common>();
const hashRoleId = useHashSegment(
hashSegment !== undefined ? hashSegment : -1,
);
const [loaded, setLoaded] = useState(false);
const [pagedData, setPagedData] =
useState<Paginated<GetRoleResponse>>(initialPagedData);
@ -79,17 +85,20 @@ const RolesEditor: React.FC<RolesEditorProps> = ({
void loadInitial();
}, [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(() => {
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(
(role) => role.id.toString() === initialRoleId,
(role) => role.id.toString() === roleIdToSelect,
);
if (roleToSelect) {
onSelectRole(roleToSelect);
}
}
}, [initialRoleId, pagedData.data, onSelectRole]);
}, [hashRoleId, initialRoleId, pagedData.data, onSelectRole, hashSegment]);
const onSort = async (nextSortColumn: Column<GetRoleResponse>) => {
const { page, pageSize } = pagedData;

View File

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