Use console layout for employee page
Signed-off-by: Bryan Frimin <bryan@getprobo.com>
This commit is contained in:
@@ -1,402 +0,0 @@
|
|||||||
import { Link, Navigate, Outlet, useParams } from "react-router";
|
|
||||||
import {
|
|
||||||
DropdownSeparator,
|
|
||||||
IconArrowBoxLeft,
|
|
||||||
IconCircleQuestionmark,
|
|
||||||
UserDropdown as UserDropdownRoot,
|
|
||||||
UserDropdownItem,
|
|
||||||
Skeleton,
|
|
||||||
Dropdown,
|
|
||||||
Button,
|
|
||||||
DropdownItem,
|
|
||||||
IconChevronGrabberVertical,
|
|
||||||
IconLock,
|
|
||||||
IconKey,
|
|
||||||
IconPeopleAdd,
|
|
||||||
IconPlusLarge,
|
|
||||||
IconCheckmark1,
|
|
||||||
IconClock,
|
|
||||||
IconMagnifyingGlass,
|
|
||||||
useToast,
|
|
||||||
Logo,
|
|
||||||
Toasts,
|
|
||||||
ConfirmDialog,
|
|
||||||
Avatar,
|
|
||||||
Badge,
|
|
||||||
Input,
|
|
||||||
} from "@probo/ui";
|
|
||||||
import { useTranslate } from "@probo/i18n";
|
|
||||||
import { graphql } from "relay-runtime";
|
|
||||||
import { useLazyLoadQuery } from "react-relay";
|
|
||||||
import type { EmployeeLayoutQuery as EmployeeLayoutQueryType } from "./__generated__/EmployeeLayoutQuery.graphql";
|
|
||||||
import { Suspense, useState, useEffect, useMemo, use } from "react";
|
|
||||||
import { ErrorBoundary } from "react-error-boundary";
|
|
||||||
import { PageError } from "/components/PageError";
|
|
||||||
import { PermissionsProvider } from "/providers/PermissionsProvider";
|
|
||||||
import { PermissionsContext } from "/providers/PermissionsContext";
|
|
||||||
|
|
||||||
const EmployeeLayoutQuery = graphql`
|
|
||||||
query EmployeeLayoutQuery($organizationId: ID!) {
|
|
||||||
viewer {
|
|
||||||
id
|
|
||||||
# user {
|
|
||||||
# fullName
|
|
||||||
# email
|
|
||||||
# }
|
|
||||||
}
|
|
||||||
organization: node(id: $organizationId) {
|
|
||||||
... on Organization {
|
|
||||||
id
|
|
||||||
name
|
|
||||||
logoUrl
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
export function EmployeeLayout() {
|
|
||||||
const { organizationId } = useParams();
|
|
||||||
|
|
||||||
if (!organizationId) {
|
|
||||||
return <Navigate to="/" />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Suspense fallback={<Skeleton className="w-full h-screen" />}>
|
|
||||||
<PermissionsProvider>
|
|
||||||
<EmployeeLayoutContent organizationId={organizationId} />
|
|
||||||
</PermissionsProvider>
|
|
||||||
</Suspense>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function EmployeeLayoutContent({ organizationId }: { organizationId: string }) {
|
|
||||||
const data = useLazyLoadQuery<EmployeeLayoutQueryType>(EmployeeLayoutQuery, {
|
|
||||||
organizationId,
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="text-txt-primary bg-level-0">
|
|
||||||
<header className="absolute z-2 left-0 right-0 px-4 flex items-center border-b border-border-solid h-12 bg-level-0">
|
|
||||||
<Logo className="w-12 h-5" />
|
|
||||||
<svg
|
|
||||||
className="mx-3 text-txt-tertiary"
|
|
||||||
width="8"
|
|
||||||
height="18"
|
|
||||||
viewBox="0 0 8 18"
|
|
||||||
fill="none"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<path d="M1 17L7 1" stroke="currentColor" />
|
|
||||||
</svg>
|
|
||||||
<div className="mr-auto">
|
|
||||||
<OrganizationSelector currentOrganization={data.organization} />
|
|
||||||
</div>
|
|
||||||
<Suspense fallback={<Skeleton className="w-32 h-8" />}>
|
|
||||||
<UserDropdown />
|
|
||||||
</Suspense>
|
|
||||||
</header>
|
|
||||||
<main className="overflow-y-auto w-full pt-12 h-[calc(100vh-3rem)]">
|
|
||||||
<div className="px-8 pb-8 pt-8">
|
|
||||||
<ErrorBoundary FallbackComponent={PageError}>
|
|
||||||
<Outlet />
|
|
||||||
</ErrorBoundary>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
<Toasts />
|
|
||||||
<ConfirmDialog />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Organization {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
logoUrl?: string | null;
|
|
||||||
authenticationMethod: string;
|
|
||||||
authStatus: "authenticated" | "unauthenticated" | "expired";
|
|
||||||
loginUrl: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface OrganizationsResponse {
|
|
||||||
organizations: Organization[];
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Invitation {
|
|
||||||
id: string;
|
|
||||||
email: string;
|
|
||||||
fullName: string;
|
|
||||||
role: string;
|
|
||||||
expiresAt: string;
|
|
||||||
acceptedAt?: string | null;
|
|
||||||
createdAt: string;
|
|
||||||
organization: {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
interface InvitationsResponse {
|
|
||||||
invitations: Invitation[];
|
|
||||||
}
|
|
||||||
|
|
||||||
function OrganizationSelector({
|
|
||||||
currentOrganization,
|
|
||||||
}: {
|
|
||||||
currentOrganization: EmployeeLayoutQueryType["response"]["organization"];
|
|
||||||
}) {
|
|
||||||
const [organizations, setOrganizations] = useState<Organization[]>([]);
|
|
||||||
const [pendingInvitationsCount, setPendingInvitationsCount] = useState(0);
|
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
|
||||||
const [error, setError] = useState<string | null>(null);
|
|
||||||
const [search, setSearch] = useState("");
|
|
||||||
const { __ } = useTranslate();
|
|
||||||
|
|
||||||
const filteredOrganizations = useMemo(() => {
|
|
||||||
if (!search.trim()) {
|
|
||||||
return organizations;
|
|
||||||
}
|
|
||||||
return organizations.filter((org) =>
|
|
||||||
org.name.toLowerCase().includes(search.toLowerCase())
|
|
||||||
);
|
|
||||||
}, [organizations, search]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const fetchData = async () => {
|
|
||||||
try {
|
|
||||||
setIsLoading(true);
|
|
||||||
|
|
||||||
const [orgsResponse, invitationsResponse] = await Promise.all([
|
|
||||||
fetch("/connect/organizations", { credentials: "include" }),
|
|
||||||
fetch("/connect/invitations", { credentials: "include" }),
|
|
||||||
]);
|
|
||||||
|
|
||||||
if (!orgsResponse.ok) {
|
|
||||||
throw new Error("Failed to fetch organizations");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!invitationsResponse.ok) {
|
|
||||||
throw new Error("Failed to fetch invitations");
|
|
||||||
}
|
|
||||||
|
|
||||||
const orgsData: OrganizationsResponse = await orgsResponse.json();
|
|
||||||
const invitationsData: InvitationsResponse =
|
|
||||||
await invitationsResponse.json();
|
|
||||||
|
|
||||||
const pendingCount = invitationsData.invitations.filter(
|
|
||||||
(inv) => !inv.acceptedAt
|
|
||||||
).length;
|
|
||||||
|
|
||||||
setOrganizations(orgsData.organizations);
|
|
||||||
setPendingInvitationsCount(pendingCount);
|
|
||||||
setError(null);
|
|
||||||
} catch (err) {
|
|
||||||
setError(err instanceof Error ? err.message : "Unknown error");
|
|
||||||
console.error("Failed to fetch data:", err);
|
|
||||||
} finally {
|
|
||||||
setIsLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
fetchData();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
return (
|
|
||||||
<div className="flex items-center gap-1">
|
|
||||||
<Button className="-ml-3" variant="tertiary" disabled>
|
|
||||||
{__("Error loading organizations")}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex items-center gap-1">
|
|
||||||
<Dropdown
|
|
||||||
toggle={
|
|
||||||
<Button
|
|
||||||
className="-ml-3"
|
|
||||||
variant="tertiary"
|
|
||||||
iconAfter={IconChevronGrabberVertical}
|
|
||||||
disabled={isLoading}
|
|
||||||
>
|
|
||||||
{isLoading ? __("Loading...") : currentOrganization?.name || ""}
|
|
||||||
</Button>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{!isLoading && organizations.length > 0 && (
|
|
||||||
<div className="px-3 py-2 border-b border-border-low">
|
|
||||||
<Input
|
|
||||||
icon={IconMagnifyingGlass}
|
|
||||||
placeholder={__("Search organizations...")}
|
|
||||||
value={search}
|
|
||||||
onValueChange={setSearch}
|
|
||||||
onKeyDown={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
}}
|
|
||||||
autoFocus
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className="max-h-150 overflow-y-auto scrollbar-thin scrollbar-thumb-gray-300 scrollbar-track-transparent hover:scrollbar-thumb-gray-400">
|
|
||||||
{isLoading ? (
|
|
||||||
<div className="px-3 py-2 text-gray-500">
|
|
||||||
{__("Loading organizations...")}
|
|
||||||
</div>
|
|
||||||
) : filteredOrganizations.length === 0 ? (
|
|
||||||
<div className="px-3 py-2 text-txt-secondary">
|
|
||||||
{__("No organizations found")}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
filteredOrganizations.map((organization) => {
|
|
||||||
const isAuthenticated =
|
|
||||||
organization.authStatus === "authenticated";
|
|
||||||
const isExpired = organization.authStatus === "expired";
|
|
||||||
const needsAuth = organization.authStatus === "unauthenticated";
|
|
||||||
|
|
||||||
const targetUrl = isAuthenticated
|
|
||||||
? `/organizations/${organization.id}`
|
|
||||||
: organization.loginUrl;
|
|
||||||
|
|
||||||
const isSAMLUrl = targetUrl.includes("/connect/saml/");
|
|
||||||
|
|
||||||
const logoUrl = organization.logoUrl;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DropdownItem asChild key={organization.id}>
|
|
||||||
{isSAMLUrl ? (
|
|
||||||
<a href={targetUrl} className="flex items-center gap-2">
|
|
||||||
<Avatar name={organization.name} src={logoUrl} />
|
|
||||||
<span className="flex-1">{organization.name}</span>
|
|
||||||
{isAuthenticated && (
|
|
||||||
<IconCheckmark1 size={16} className="text-green-600" />
|
|
||||||
)}
|
|
||||||
{isExpired && (
|
|
||||||
<IconClock size={16} className="text-orange-600" />
|
|
||||||
)}
|
|
||||||
{needsAuth && (
|
|
||||||
<IconLock size={16} className="text-gray-400" />
|
|
||||||
)}
|
|
||||||
</a>
|
|
||||||
) : (
|
|
||||||
<Link to={targetUrl} className="flex items-center gap-2">
|
|
||||||
<Avatar name={organization.name} src={logoUrl} />
|
|
||||||
<span className="flex-1">{organization.name}</span>
|
|
||||||
{isAuthenticated && (
|
|
||||||
<IconCheckmark1 size={16} className="text-green-600" />
|
|
||||||
)}
|
|
||||||
{isExpired && (
|
|
||||||
<IconClock size={16} className="text-orange-600" />
|
|
||||||
)}
|
|
||||||
{needsAuth && (
|
|
||||||
<IconLock size={16} className="text-gray-400" />
|
|
||||||
)}
|
|
||||||
</Link>
|
|
||||||
)}
|
|
||||||
</DropdownItem>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<DropdownSeparator />
|
|
||||||
{pendingInvitationsCount > 0 && (
|
|
||||||
<DropdownItem asChild>
|
|
||||||
<Link to="/">
|
|
||||||
<IconPeopleAdd size={16} />
|
|
||||||
<span className="flex-1">{__("Invitations")}</span>
|
|
||||||
<Badge variant="info" size="sm">
|
|
||||||
{pendingInvitationsCount}
|
|
||||||
</Badge>
|
|
||||||
</Link>
|
|
||||||
</DropdownItem>
|
|
||||||
)}
|
|
||||||
<DropdownItem asChild>
|
|
||||||
<Link to="/organizations/new">
|
|
||||||
<IconPlusLarge size={16} />
|
|
||||||
{__("Add organization")}
|
|
||||||
</Link>
|
|
||||||
</DropdownItem>
|
|
||||||
</Dropdown>
|
|
||||||
{pendingInvitationsCount > 0 && (
|
|
||||||
<Link to="/" className="relative" title={__("Invitations")}>
|
|
||||||
<Button variant="tertiary" icon={IconPeopleAdd} />
|
|
||||||
<Badge
|
|
||||||
variant="info"
|
|
||||||
size="sm"
|
|
||||||
className="absolute -top-1 -right-1 min-w-[20px] h-5 flex items-center justify-center"
|
|
||||||
>
|
|
||||||
{pendingInvitationsCount}
|
|
||||||
</Badge>
|
|
||||||
</Link>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function UserDropdown() {
|
|
||||||
const { __ } = useTranslate();
|
|
||||||
const { toast } = useToast();
|
|
||||||
const { isAuthorized } = use(PermissionsContext);
|
|
||||||
const user = {
|
|
||||||
fullName: "",
|
|
||||||
email: "",
|
|
||||||
};
|
|
||||||
// const user = useLazyLoadQuery<EmployeeLayoutQueryType>(EmployeeLayoutQuery, {
|
|
||||||
// organizationId,
|
|
||||||
// }).viewer.user;
|
|
||||||
|
|
||||||
const handleLogout: React.MouseEventHandler<HTMLAnchorElement> = async (
|
|
||||||
e
|
|
||||||
) => {
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
fetch("/connect/logout", {
|
|
||||||
method: "DELETE",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
body: JSON.stringify({}),
|
|
||||||
})
|
|
||||||
.then(async (res) => {
|
|
||||||
if (!res.ok) {
|
|
||||||
const error = await res.json();
|
|
||||||
throw new Error(error.message || __("Failed to login"));
|
|
||||||
}
|
|
||||||
|
|
||||||
window.location.reload();
|
|
||||||
})
|
|
||||||
.catch((e) => {
|
|
||||||
toast({
|
|
||||||
title: __("Error"),
|
|
||||||
description: e.message as string,
|
|
||||||
variant: "error",
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<UserDropdownRoot fullName={user.fullName} email={user.email}>
|
|
||||||
{isAuthorized("Organization", "deleteOrganization") && (
|
|
||||||
<UserDropdownItem
|
|
||||||
to="/me/api-keys"
|
|
||||||
icon={IconKey}
|
|
||||||
label={__("API Keys")}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<UserDropdownItem
|
|
||||||
to="mailto:support@getprobo.com"
|
|
||||||
icon={IconCircleQuestionmark}
|
|
||||||
label={__("Help")}
|
|
||||||
/>
|
|
||||||
<DropdownSeparator />
|
|
||||||
<UserDropdownItem
|
|
||||||
variant="danger"
|
|
||||||
to="/logout"
|
|
||||||
icon={IconArrowBoxLeft}
|
|
||||||
label="Logout"
|
|
||||||
onClick={handleLogout}
|
|
||||||
/>
|
|
||||||
</UserDropdownRoot>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -28,20 +28,18 @@ export const employeeDocumentsPageQuery = graphql`
|
|||||||
export function EmployeeDocumentsPage(props: {
|
export function EmployeeDocumentsPage(props: {
|
||||||
queryRef: PreloadedQuery<EmployeeDocumentsPageQuery>;
|
queryRef: PreloadedQuery<EmployeeDocumentsPageQuery>;
|
||||||
}) {
|
}) {
|
||||||
|
const { queryRef } = props;
|
||||||
const { __ } = useTranslate();
|
const { __ } = useTranslate();
|
||||||
const organizationId = useOrganizationId();
|
const organizationId = useOrganizationId();
|
||||||
|
|
||||||
const { queryRef } = props;
|
|
||||||
const {
|
const {
|
||||||
viewer: {
|
viewer: { signableDocuments },
|
||||||
signableDocuments: { edges: initialDocuments },
|
|
||||||
},
|
|
||||||
} = usePreloadedQuery<EmployeeDocumentsPageQuery>(
|
} = usePreloadedQuery<EmployeeDocumentsPageQuery>(
|
||||||
employeeDocumentsPageQuery,
|
employeeDocumentsPageQuery,
|
||||||
queryRef
|
queryRef
|
||||||
);
|
);
|
||||||
|
|
||||||
const documents = initialDocuments.map((edge) => edge.node).filter(Boolean);
|
const documents = signableDocuments.edges.map((edge) => edge.node);
|
||||||
|
|
||||||
usePageTitle(__("Documents"));
|
usePageTitle(__("Documents"));
|
||||||
|
|
||||||
|
|||||||
@@ -22,20 +22,18 @@ function EmployeeDocumentsPageLoader() {
|
|||||||
}, [loadQuery, organizationId]);
|
}, [loadQuery, organizationId]);
|
||||||
|
|
||||||
if (!queryRef) {
|
if (!queryRef) {
|
||||||
return <PageSkeleton />;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return <EmployeeDocumentsPage queryRef={queryRef} />;
|
||||||
<Suspense fallback={<PageSkeleton />}>
|
|
||||||
<EmployeeDocumentsPage queryRef={queryRef} />
|
|
||||||
</Suspense>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function () {
|
export default function () {
|
||||||
return (
|
return (
|
||||||
<CoreRelayProvider>
|
<CoreRelayProvider>
|
||||||
<EmployeeDocumentsPageLoader />
|
<Suspense fallback={<PageSkeleton />}>
|
||||||
|
<EmployeeDocumentsPageLoader />
|
||||||
|
</Suspense>
|
||||||
</CoreRelayProvider>
|
</CoreRelayProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ export function DomainSettingsPage(props: {
|
|||||||
|
|
||||||
const { organization } = usePreloadedQuery<DomainSettingsPageQuery>(
|
const { organization } = usePreloadedQuery<DomainSettingsPageQuery>(
|
||||||
domainSettingsPageQuery,
|
domainSettingsPageQuery,
|
||||||
queryRef,
|
queryRef
|
||||||
);
|
);
|
||||||
if (organization.__typename !== "Organization") {
|
if (organization.__typename !== "Organization") {
|
||||||
throw new Error("invalid type for node");
|
throw new Error("invalid type for node");
|
||||||
@@ -49,7 +49,7 @@ export function DomainSettingsPage(props: {
|
|||||||
</h3>
|
</h3>
|
||||||
<p className="text-txt-tertiary mb-4">
|
<p className="text-txt-tertiary mb-4">
|
||||||
{__(
|
{__(
|
||||||
"Add your own domain to make your trust center more professional",
|
"Add your own domain to make your trust center more professional"
|
||||||
)}
|
)}
|
||||||
</p>
|
</p>
|
||||||
<div className="flex justify-center">
|
<div className="flex justify-center">
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import {
|
|||||||
redirect,
|
redirect,
|
||||||
useRouteError,
|
useRouteError,
|
||||||
} from "react-router";
|
} from "react-router";
|
||||||
import { EmployeeLayout } from "./layouts/EmployeeLayout.tsx";
|
|
||||||
import { CenteredLayout, CenteredLayoutSkeleton } from "@probo/ui";
|
import { CenteredLayout, CenteredLayoutSkeleton } from "@probo/ui";
|
||||||
import { PageSkeleton } from "./components/skeletons/PageSkeleton.tsx";
|
import { PageSkeleton } from "./components/skeletons/PageSkeleton.tsx";
|
||||||
import { riskRoutes } from "./routes/riskRoutes.ts";
|
import { riskRoutes } from "./routes/riskRoutes.ts";
|
||||||
@@ -27,7 +26,6 @@ import { continualImprovementRoutes } from "./routes/continualImprovementRoutes.
|
|||||||
import { rightsRequestRoutes } from "./routes/rightsRequestRoutes.ts";
|
import { rightsRequestRoutes } from "./routes/rightsRequestRoutes.ts";
|
||||||
import { processingActivityRoutes } from "./routes/processingActivityRoutes.ts";
|
import { processingActivityRoutes } from "./routes/processingActivityRoutes.ts";
|
||||||
import { statesOfApplicabilityRoutes } from "./routes/statesOfApplicabilityRoutes.ts";
|
import { statesOfApplicabilityRoutes } from "./routes/statesOfApplicabilityRoutes.ts";
|
||||||
import { CoreRelayProvider } from "./providers/CoreRelayProvider.tsx";
|
|
||||||
import { lazy } from "@probo/react-lazy";
|
import { lazy } from "@probo/react-lazy";
|
||||||
import { routeFromAppRoute, type AppRoute } from "@probo/routes";
|
import { routeFromAppRoute, type AppRoute } from "@probo/routes";
|
||||||
import { Role } from "@probo/helpers";
|
import { Role } from "@probo/helpers";
|
||||||
@@ -121,15 +119,15 @@ const routes = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/organizations/:organizationId/employee",
|
path: "/organizations/:organizationId/employee",
|
||||||
Component: () => (
|
Fallback: () => "fallback employee...",
|
||||||
<CoreRelayProvider>
|
Component: lazy(
|
||||||
<EmployeeLayout />
|
() => import("./pages/iam/memberships/MembershipLayoutLoader")
|
||||||
</CoreRelayProvider>
|
|
||||||
),
|
),
|
||||||
ErrorBoundary: ErrorBoundary,
|
ErrorBoundary: ErrorBoundary,
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: "",
|
index: true,
|
||||||
|
// Component: () => "hello world",
|
||||||
Component: lazy(
|
Component: lazy(
|
||||||
() =>
|
() =>
|
||||||
import("./pages/organizations/employee/EmployeeDocumentsPageLoader")
|
import("./pages/organizations/employee/EmployeeDocumentsPageLoader")
|
||||||
|
|||||||
Reference in New Issue
Block a user