@pablo2410/shared-ui 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/hierarchy/orgHierarchy.ts","../../src/hierarchy/enterpriseOverride.ts","../../src/hierarchy/OrgHierarchyContext.tsx","../../src/hierarchy/HierarchyNavigator.tsx"],"sourcesContent":["/**\n * @oplytics/shared-ui/hierarchy — Org Hierarchy Types & Utilities\n *\n * Hierarchy: Enterprise > Business Unit > Site > Area > Asset\n *\n * Single source of truth for org-hierarchy types used by:\n * - Portal (direct DB via tRPC)\n * - All subdomains (via Portal Service API)\n *\n * The canonical data shape matches the Portal Service API response:\n * GET /api/service/hierarchy\n */\n\n/* ── Base Node ── */\n\nexport interface OrgHierarchyNode {\n id: number;\n name: string;\n code?: string;\n}\n\n/* ── Flat hierarchy data — matches Portal Service API /api/service/hierarchy ── */\n\nexport interface OrgHierarchyEnterprise extends OrgHierarchyNode {}\n\nexport interface OrgHierarchyBusinessUnit extends OrgHierarchyNode {\n enterpriseId: number;\n}\n\nexport interface OrgHierarchySite extends OrgHierarchyNode {\n enterpriseId: number;\n businessUnitId: number;\n location?: string | null;\n}\n\nexport interface OrgHierarchyArea extends OrgHierarchyNode {\n siteId: number;\n}\n\nexport interface OrgHierarchyAsset extends OrgHierarchyNode {\n siteId: number;\n areaId: number;\n assetType?: string;\n}\n\n/**\n * Canonical flat hierarchy payload.\n * Every consumer (Portal tRPC, Service API REST) must return this shape.\n */\nexport interface OrgHierarchyData {\n enterprises: OrgHierarchyEnterprise[];\n businessUnits: OrgHierarchyBusinessUnit[];\n sites: OrgHierarchySite[];\n areas: OrgHierarchyArea[];\n assets: OrgHierarchyAsset[];\n}\n\n/* ── Selection state ── */\n\nexport interface OrgHierarchySelection {\n enterprise: OrgHierarchyNode | null;\n businessUnit: OrgHierarchyNode | null;\n site: OrgHierarchyNode | null;\n area: OrgHierarchyNode | null;\n asset: OrgHierarchyNode | null;\n}\n\nexport const ORG_HIERARCHY_EMPTY: OrgHierarchySelection = {\n enterprise: null,\n businessUnit: null,\n site: null,\n area: null,\n asset: null,\n};\n\n/* ── Context value ── */\n\nexport interface OrgHierarchyContextValue {\n /** Current selection at each level */\n selection: OrgHierarchySelection;\n\n /** Flat lists filtered by current selection */\n enterprises: OrgHierarchyNode[];\n businessUnits: OrgHierarchyNode[];\n sites: OrgHierarchyNode[];\n areas: OrgHierarchyNode[];\n assets: OrgHierarchyNode[];\n\n /** Setters — selecting a level clears all levels below it */\n selectEnterprise: (node: OrgHierarchyNode | null) => void;\n selectBusinessUnit: (node: OrgHierarchyNode | null) => void;\n selectSite: (node: OrgHierarchyNode | null) => void;\n selectArea: (node: OrgHierarchyNode | null) => void;\n selectAsset: (node: OrgHierarchyNode | null) => void;\n\n /** Whether the user can switch enterprises (platform_admin only) */\n canSwitchEnterprise: boolean;\n\n /** Loading state */\n loading: boolean;\n\n /** Breadcrumb trail as an array of { level, node } */\n breadcrumbs: { level: string; node: OrgHierarchyNode }[];\n}\n\n/* ── Hierarchy levels (for iteration) ── */\n\nexport const ORG_HIERARCHY_LEVELS = [\n \"enterprise\",\n \"businessUnit\",\n \"site\",\n \"area\",\n \"asset\",\n] as const;\n\nexport type OrgHierarchyLevel = (typeof ORG_HIERARCHY_LEVELS)[number];\n\n/** Human-readable labels for each level */\nexport const ORG_HIERARCHY_LABELS: Record<OrgHierarchyLevel, string> = {\n enterprise: \"Enterprise\",\n businessUnit: \"Business Unit\",\n site: \"Site\",\n area: \"Area\",\n asset: \"Asset\",\n};\n","/**\n * Enterprise Override — shared helper for platform_admin enterprise switching.\n *\n * When a platform_admin switches enterprise in the Portal UI, a cookie\n * `oplytics-active-enterprise` is set with the selected enterprise ID.\n * This cookie is shared across all *.oplytics.digital subdomains.\n *\n * Each subdomain's createContext should call applyEnterpriseOverride()\n * to override the user's DB-stored enterpriseId with the cookie value.\n *\n * Used by: Portal, SQDCP, OEE Manager, Action Manager, Business Hub, Connect\n */\n\n/**\n * Cookie name for the platform_admin active enterprise override.\n * Shared across all *.oplytics.digital subdomains via the .oplytics.digital domain.\n */\nexport const ACTIVE_ENTERPRISE_COOKIE = \"oplytics-active-enterprise\";\n\n/**\n * Parse a single cookie value from a cookie header string.\n */\nfunction parseCookieValue(cookieHeader: string, name: string): string | null {\n const match = cookieHeader\n .split(\";\")\n .map((c) => c.trim())\n .find((c) => c.startsWith(`${name}=`));\n if (!match) return null;\n return decodeURIComponent(match.slice(name.length + 1));\n}\n\n/**\n * Apply the enterprise override for platform_admin users.\n *\n * If the user is a platform_admin and has the active enterprise cookie set,\n * returns a shallow copy of the user with the overridden enterpriseId.\n * Otherwise returns the user unchanged.\n *\n * @param user - The authenticated user (or null)\n * @param cookieHeader - The raw Cookie header string from the request\n * @returns The user with potentially overridden enterpriseId\n */\nexport function applyEnterpriseOverride<\n T extends { role: string; enterpriseId?: number | null },\n>(user: T | null, cookieHeader: string): T | null {\n if (!user || user.role !== \"platform_admin\") return user;\n\n const activeEnterprise = parseCookieValue(\n cookieHeader,\n ACTIVE_ENTERPRISE_COOKIE,\n );\n if (!activeEnterprise) return user;\n\n const parsedId = parseInt(activeEnterprise, 10);\n if (isNaN(parsedId) || parsedId <= 0) return user;\n\n // Return a shallow copy with the overridden enterpriseId\n return { ...user, enterpriseId: parsedId };\n}\n","/**\n * OrgHierarchyContext — shared React context factory for the Organisational Hierarchy.\n *\n * Usage:\n * const { OrgHierarchyProvider, useOrgHierarchy } = createOrgHierarchyContext({\n * storageKey: \"oplytics-portal-org-hierarchy\",\n * useHierarchyData: () => {\n * const { data, isLoading } = trpc.hierarchy.flat.useQuery(...);\n * return { data: data ?? null, isLoading };\n * },\n * });\n *\n * All selection state, cascade setters, breadcrumbs, and session persistence\n * are handled here — no per-subdomain duplication.\n */\nimport {\n createContext,\n useContext,\n useCallback,\n useMemo,\n useState,\n useEffect,\n type ReactNode,\n} from \"react\";\nimport type {\n OrgHierarchyData,\n OrgHierarchyNode,\n OrgHierarchySelection,\n OrgHierarchyContextValue,\n} from \"./orgHierarchy\";\nimport { ORG_HIERARCHY_EMPTY } from \"./orgHierarchy\";\n\n/* ── Session persistence helpers ── */\n\nfunction loadPersistedSelection(key: string): OrgHierarchySelection | null {\n try {\n const raw = sessionStorage.getItem(key);\n if (!raw) return null;\n return JSON.parse(raw) as OrgHierarchySelection;\n } catch {\n return null;\n }\n}\n\nfunction persistSelection(key: string, sel: OrgHierarchySelection) {\n try {\n sessionStorage.setItem(key, JSON.stringify(sel));\n } catch {\n // ignore storage errors\n }\n}\n\n/* ── Factory options ── */\n\nexport interface CreateOrgHierarchyContextOptions {\n /**\n * Unique sessionStorage key for this subdomain's selection persistence.\n * e.g. \"oplytics-portal-org-hierarchy\", \"oplytics-sqdcp-org-hierarchy\"\n */\n storageKey: string;\n\n /**\n * React hook that fetches the flat hierarchy data.\n * Must return { data, isLoading } where data is OrgHierarchyData | null.\n */\n useHierarchyData: () => {\n data: OrgHierarchyData | null;\n isLoading: boolean;\n };\n}\n\n/* ── Factory ── */\n\nexport function createOrgHierarchyContext(options: CreateOrgHierarchyContextOptions) {\n const { storageKey, useHierarchyData } = options;\n\n const OrgHierarchyCtx = createContext<OrgHierarchyContextValue>({\n selection: ORG_HIERARCHY_EMPTY,\n enterprises: [],\n businessUnits: [],\n sites: [],\n areas: [],\n assets: [],\n selectEnterprise: () => {},\n selectBusinessUnit: () => {},\n selectSite: () => {},\n selectArea: () => {},\n selectAsset: () => {},\n canSwitchEnterprise: false,\n loading: true,\n breadcrumbs: [],\n });\n\n function OrgHierarchyProvider({\n children,\n canSwitchEnterprise = false,\n onEnterpriseSwitched,\n }: {\n children: ReactNode;\n canSwitchEnterprise?: boolean;\n /**\n * Callback fired when a platform_admin switches enterprise.\n * Used to persist the switch to the backend (e.g. via auth.switchEnterprise mutation).\n * Only called when canSwitchEnterprise is true and the user explicitly changes enterprise.\n */\n onEnterpriseSwitched?: (enterpriseId: number | null) => void;\n }) {\n const [selection, setSelection] = useState<OrgHierarchySelection>(\n () => loadPersistedSelection(storageKey) || ORG_HIERARCHY_EMPTY\n );\n\n // Persist selection changes\n useEffect(() => {\n persistSelection(storageKey, selection);\n }, [selection]);\n\n // Fetch hierarchy data via the provided hook\n const { data: hierarchy, isLoading } = useHierarchyData();\n\n // ── Derive filtered lists from flat data ──\n\n const enterprises = useMemo<OrgHierarchyNode[]>(() => {\n if (!hierarchy?.enterprises) return [];\n return hierarchy.enterprises.map((e) => ({\n id: e.id,\n name: e.name,\n code: e.code,\n }));\n }, [hierarchy]);\n\n const businessUnits = useMemo<OrgHierarchyNode[]>(() => {\n if (!hierarchy?.businessUnits || !selection.enterprise) return [];\n return hierarchy.businessUnits\n .filter((bu) => bu.enterpriseId === selection.enterprise!.id)\n .map((bu) => ({ id: bu.id, name: bu.name, code: bu.code }));\n }, [hierarchy, selection.enterprise]);\n\n const sites = useMemo<OrgHierarchyNode[]>(() => {\n if (!hierarchy?.sites || !selection.enterprise) return [];\n let filtered = hierarchy.sites.filter(\n (s) => s.enterpriseId === selection.enterprise!.id\n );\n if (selection.businessUnit) {\n filtered = filtered.filter(\n (s) => s.businessUnitId === selection.businessUnit!.id\n );\n }\n return filtered.map((s) => ({ id: s.id, name: s.name, code: s.code }));\n }, [hierarchy, selection.enterprise, selection.businessUnit]);\n\n const areas = useMemo<OrgHierarchyNode[]>(() => {\n if (!hierarchy?.areas || !selection.site) return [];\n return hierarchy.areas\n .filter((a) => a.siteId === selection.site!.id)\n .map((a) => ({ id: a.id, name: a.name, code: a.code }));\n }, [hierarchy, selection.site]);\n\n const assets = useMemo<OrgHierarchyNode[]>(() => {\n if (!hierarchy?.assets || !selection.area) return [];\n return hierarchy.assets\n .filter((a) => a.areaId === selection.area!.id)\n .map((a) => ({ id: a.id, name: a.name, code: a.code }));\n }, [hierarchy, selection.area]);\n\n // Auto-select first enterprise for non-platform_admin users\n useEffect(() => {\n if (isLoading || enterprises.length === 0) return;\n if (!selection.enterprise) {\n if (!canSwitchEnterprise || enterprises.length === 1) {\n setSelection((prev) => ({ ...prev, enterprise: enterprises[0] }));\n }\n }\n }, [isLoading, enterprises, selection.enterprise, canSwitchEnterprise]);\n\n // ── Cascade setters ──\n\n const selectEnterprise = useCallback((node: OrgHierarchyNode | null) => {\n setSelection({\n enterprise: node,\n businessUnit: null,\n site: null,\n area: null,\n asset: null,\n });\n // Notify backend of enterprise switch (platform_admin only)\n if (canSwitchEnterprise && onEnterpriseSwitched) {\n onEnterpriseSwitched(node?.id ?? null);\n }\n }, [canSwitchEnterprise, onEnterpriseSwitched]);\n\n const selectBusinessUnit = useCallback((node: OrgHierarchyNode | null) => {\n setSelection((prev) => ({\n ...prev,\n businessUnit: node,\n site: null,\n area: null,\n asset: null,\n }));\n }, []);\n\n const selectSite = useCallback((node: OrgHierarchyNode | null) => {\n setSelection((prev) => ({\n ...prev,\n site: node,\n area: null,\n asset: null,\n }));\n }, []);\n\n const selectArea = useCallback((node: OrgHierarchyNode | null) => {\n setSelection((prev) => ({\n ...prev,\n area: node,\n asset: null,\n }));\n }, []);\n\n const selectAsset = useCallback((node: OrgHierarchyNode | null) => {\n setSelection((prev) => ({\n ...prev,\n asset: node,\n }));\n }, []);\n\n // ── Breadcrumbs ──\n\n const breadcrumbs = useMemo(() => {\n const crumbs: { level: string; node: OrgHierarchyNode }[] = [];\n if (selection.enterprise)\n crumbs.push({ level: \"Enterprise\", node: selection.enterprise });\n if (selection.businessUnit)\n crumbs.push({ level: \"Business Unit\", node: selection.businessUnit });\n if (selection.site)\n crumbs.push({ level: \"Site\", node: selection.site });\n if (selection.area)\n crumbs.push({ level: \"Area\", node: selection.area });\n if (selection.asset)\n crumbs.push({ level: \"Asset\", node: selection.asset });\n return crumbs;\n }, [selection]);\n\n // ── Context value ──\n\n const value = useMemo<OrgHierarchyContextValue>(\n () => ({\n selection,\n enterprises,\n businessUnits,\n sites,\n areas,\n assets,\n selectEnterprise,\n selectBusinessUnit,\n selectSite,\n selectArea,\n selectAsset,\n canSwitchEnterprise,\n loading: isLoading,\n breadcrumbs,\n }),\n [\n selection,\n enterprises,\n businessUnits,\n sites,\n areas,\n assets,\n selectEnterprise,\n selectBusinessUnit,\n selectSite,\n selectArea,\n selectAsset,\n canSwitchEnterprise,\n isLoading,\n breadcrumbs,\n ]\n );\n\n return (\n <OrgHierarchyCtx.Provider value={value}>\n {children}\n </OrgHierarchyCtx.Provider>\n );\n }\n\n function useOrgHierarchy(): OrgHierarchyContextValue {\n return useContext(OrgHierarchyCtx);\n }\n\n return { OrgHierarchyProvider, useOrgHierarchy };\n}\n\n/* ── Re-export types for convenience ── */\nexport type {\n OrgHierarchyNode,\n OrgHierarchyData,\n OrgHierarchySelection,\n OrgHierarchyContextValue,\n} from \"./orgHierarchy\";\n","/**\n * HierarchyNavigator — breadcrumb-style hierarchy switcher.\n *\n * Displays: Enterprise / BU / Site / Area / Asset\n * Each level is a clickable dropdown to switch context.\n * Platform admins can switch enterprises; others see their scoped enterprise.\n *\n * NOTE: This component requires the consuming app to provide:\n * - A hierarchy context via useHierarchy prop\n * - shadcn/ui DropdownMenu components via the renderDropdown prop\n *\n * For apps using the createOrgHierarchyContext factory, pass the useOrgHierarchy hook.\n *\n * Oplytics dark theme: #0A0E1A bg, #8C34E9 accent, #E2E8F0 text, #8890A0 muted\n */\nimport { cn } from \"../utils/cn\";\nimport type { OrgHierarchyNode, OrgHierarchyContextValue } from \"./orgHierarchy\";\nimport { ChevronRight, ChevronDown, Building2, Network, MapPin, LayoutGrid, Box } from \"lucide-react\";\nimport type { ReactNode } from \"react\";\n\n/* ── Level config ── */\n\ninterface LevelConfig {\n key: string;\n label: string;\n icon: React.ElementType;\n getNodes: () => OrgHierarchyNode[];\n getSelected: () => OrgHierarchyNode | null;\n onSelect: (node: OrgHierarchyNode | null) => void;\n canSwitch?: boolean;\n}\n\n/* ── Dropdown render prop types ── */\n\nexport interface DropdownRenderProps {\n trigger: ReactNode;\n items: Array<{\n key: number;\n label: string;\n code?: string;\n icon: React.ElementType;\n isActive: boolean;\n onClick: () => void;\n }>;\n align?: \"start\" | \"end\";\n}\n\n/* ── Single breadcrumb level ── */\n\nfunction BreadcrumbLevel({\n config,\n isLast,\n compact,\n renderDropdown,\n}: {\n config: LevelConfig;\n isLast: boolean;\n compact?: boolean;\n renderDropdown?: (props: DropdownRenderProps) => ReactNode;\n}) {\n const nodes = config.getNodes();\n const selected = config.getSelected();\n const Icon = config.icon;\n const canSwitch = config.canSwitch !== false;\n const hasMultiple = nodes.length > 1;\n\n if (!selected) return null;\n\n // If only one option or can't switch, show as static text\n if (!hasMultiple || !canSwitch) {\n return (\n <div className=\"flex items-center gap-1\">\n <div\n className={cn(\n \"flex items-center gap-1.5 px-2 py-1 rounded-md text-sm\",\n \"text-[#E2E8F0]\",\n compact && \"px-1.5 py-0.5 text-xs\"\n )}\n >\n <Icon className={cn(\"shrink-0 text-[#8890A0]\", compact ? \"h-3 w-3\" : \"h-3.5 w-3.5\")} />\n <span className=\"truncate max-w-[120px]\">{selected.name}</span>\n </div>\n {!isLast && (\n <ChevronRight className={cn(\"shrink-0 text-[#596475]\", compact ? \"h-3 w-3\" : \"h-3.5 w-3.5\")} />\n )}\n </div>\n );\n }\n\n // Multiple options — use renderDropdown if provided, otherwise basic button\n const trigger = (\n <button\n className={cn(\n \"flex items-center gap-1.5 px-2 py-1 rounded-md text-sm\",\n \"text-[#E2E8F0] hover:bg-[#1E2738] hover:text-white\",\n \"transition-colors cursor-pointer outline-none\",\n \"focus-visible:ring-1 focus-visible:ring-[#8C34E9]\",\n compact && \"px-1.5 py-0.5 text-xs\"\n )}\n >\n <Icon className={cn(\"shrink-0 text-[#8890A0]\", compact ? \"h-3 w-3\" : \"h-3.5 w-3.5\")} />\n <span className=\"truncate max-w-[120px]\">{selected.name}</span>\n <ChevronDown className={cn(\"shrink-0 text-[#596475]\", compact ? \"h-2.5 w-2.5\" : \"h-3 w-3\")} />\n </button>\n );\n\n const items = nodes.map((node) => ({\n key: node.id,\n label: node.name,\n code: node.code,\n icon: Icon,\n isActive: selected.id === node.id,\n onClick: () => config.onSelect(node),\n }));\n\n return (\n <div className=\"flex items-center gap-1\">\n {renderDropdown ? (\n renderDropdown({ trigger, items, align: \"start\" })\n ) : (\n // Fallback: simple select-style rendering\n <div className=\"relative group\">\n {trigger}\n </div>\n )}\n {!isLast && (\n <ChevronRight className={cn(\"shrink-0 text-[#596475]\", compact ? \"h-3 w-3\" : \"h-3.5 w-3.5\")} />\n )}\n </div>\n );\n}\n\n/* ── Drill-down prompt ── */\n\nfunction DrillDownPrompt({\n label,\n icon: Icon,\n nodes,\n onSelect,\n compact,\n renderDropdown,\n}: {\n label: string;\n icon: React.ElementType;\n nodes: OrgHierarchyNode[];\n onSelect: (node: OrgHierarchyNode) => void;\n compact?: boolean;\n renderDropdown?: (props: DropdownRenderProps) => ReactNode;\n}) {\n if (nodes.length === 0) return null;\n\n // Auto-select if only one option\n if (nodes.length === 1) {\n return (\n <div className=\"flex items-center gap-1\">\n <ChevronRight className={cn(\"shrink-0 text-[#596475]\", compact ? \"h-3 w-3\" : \"h-3.5 w-3.5\")} />\n <button\n onClick={() => onSelect(nodes[0])}\n className={cn(\n \"flex items-center gap-1.5 px-2 py-1 rounded-md text-sm\",\n \"text-[#596475] hover:bg-[#1E2738] hover:text-[#8890A0]\",\n \"transition-colors cursor-pointer\",\n compact && \"px-1.5 py-0.5 text-xs\"\n )}\n >\n <Icon className={cn(\"shrink-0\", compact ? \"h-3 w-3\" : \"h-3.5 w-3.5\")} />\n <span className=\"truncate\">{nodes[0].name}</span>\n </button>\n </div>\n );\n }\n\n const trigger = (\n <button\n className={cn(\n \"flex items-center gap-1.5 px-2 py-1 rounded-md text-sm\",\n \"text-[#596475] hover:bg-[#1E2738] hover:text-[#8890A0]\",\n \"transition-colors cursor-pointer outline-none\",\n \"border border-dashed border-[#2A2A3E] hover:border-[#596475]\",\n compact && \"px-1.5 py-0.5 text-xs\"\n )}\n >\n <Icon className={cn(\"shrink-0\", compact ? \"h-3 w-3\" : \"h-3.5 w-3.5\")} />\n <span className=\"truncate\">Select {label}</span>\n <ChevronDown className={cn(\"shrink-0\", compact ? \"h-2.5 w-2.5\" : \"h-3 w-3\")} />\n </button>\n );\n\n const items = nodes.map((node) => ({\n key: node.id,\n label: node.name,\n code: node.code,\n icon: Icon,\n isActive: false,\n onClick: () => onSelect(node),\n }));\n\n return (\n <div className=\"flex items-center gap-1\">\n <ChevronRight className={cn(\"shrink-0 text-[#596475]\", compact ? \"h-3 w-3\" : \"h-3.5 w-3.5\")} />\n {renderDropdown ? (\n renderDropdown({ trigger, items, align: \"start\" })\n ) : (\n <div className=\"relative group\">\n {trigger}\n </div>\n )}\n </div>\n );\n}\n\n/* ── Main component ── */\n\nexport interface HierarchyNavigatorProps {\n /** The hierarchy context value — pass from useOrgHierarchy() */\n hierarchy: OrgHierarchyContextValue;\n /** Show in compact mode (smaller text, tighter spacing) */\n compact?: boolean;\n /** Maximum depth to show (default: all 5 levels) */\n maxDepth?: number;\n /** CSS class for the container */\n className?: string;\n /**\n * Render prop for dropdown menus. If not provided, dropdowns won't open.\n * Consuming apps should pass a function that renders their shadcn/ui DropdownMenu.\n */\n renderDropdown?: (props: DropdownRenderProps) => ReactNode;\n}\n\nexport function HierarchyNavigator({\n hierarchy: ctx,\n compact = false,\n maxDepth = 5,\n className,\n renderDropdown,\n}: HierarchyNavigatorProps) {\n if (ctx.loading) {\n return (\n <div className={cn(\"flex items-center gap-2 animate-pulse\", className)}>\n <div className=\"h-6 w-24 rounded bg-[#1E2738]\" />\n <ChevronRight className=\"h-3.5 w-3.5 text-[#596475]\" />\n <div className=\"h-6 w-20 rounded bg-[#1E2738]\" />\n </div>\n );\n }\n\n // Build the level configs\n const levels: LevelConfig[] = [\n {\n key: \"enterprise\",\n label: \"Enterprise\",\n icon: Building2,\n getNodes: () => ctx.enterprises,\n getSelected: () => ctx.selection.enterprise,\n onSelect: ctx.selectEnterprise,\n canSwitch: ctx.canSwitchEnterprise,\n },\n {\n key: \"businessUnit\",\n label: \"Business Unit\",\n icon: Network,\n getNodes: () => ctx.businessUnits,\n getSelected: () => ctx.selection.businessUnit,\n onSelect: ctx.selectBusinessUnit,\n },\n {\n key: \"site\",\n label: \"Site\",\n icon: MapPin,\n getNodes: () => ctx.sites,\n getSelected: () => ctx.selection.site,\n onSelect: ctx.selectSite,\n },\n {\n key: \"area\",\n label: \"Area\",\n icon: LayoutGrid,\n getNodes: () => ctx.areas,\n getSelected: () => ctx.selection.area,\n onSelect: ctx.selectArea,\n },\n {\n key: \"asset\",\n label: \"Asset\",\n icon: Box,\n getNodes: () => ctx.assets,\n getSelected: () => ctx.selection.asset,\n onSelect: ctx.selectAsset,\n },\n ].slice(0, maxDepth);\n\n // Determine which levels are selected and which is the next drill-down\n const selectedLevels = levels.filter((l) => l.getSelected() !== null);\n const nextLevel = levels.find((l) => l.getSelected() === null && l.getNodes().length > 0);\n\n return (\n <nav\n className={cn(\"flex items-center flex-wrap gap-0.5\", className)}\n aria-label=\"Hierarchy navigation\"\n >\n {selectedLevels.map((level, idx) => (\n <BreadcrumbLevel\n key={level.key}\n config={level}\n isLast={idx === selectedLevels.length - 1 && !nextLevel}\n compact={compact}\n renderDropdown={renderDropdown}\n />\n ))}\n {nextLevel && (\n <DrillDownPrompt\n label={nextLevel.label}\n icon={nextLevel.icon}\n nodes={nextLevel.getNodes()}\n onSelect={nextLevel.onSelect}\n compact={compact}\n renderDropdown={renderDropdown}\n />\n )}\n {selectedLevels.length === 0 && !nextLevel && (\n <span className=\"text-sm text-[#596475] italic\">No hierarchy data available</span>\n )}\n </nav>\n );\n}\n"],"mappings":"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","names":["jsx"]}
@@ -0,0 +1,32 @@
1
+ /**
2
+ * @oplytics/shared-ui/hooks
3
+ *
4
+ * Shared utility hooks used across all Oplytics.digital subdomains.
5
+ */
6
+ /**
7
+ * Detects whether the viewport is at mobile width.
8
+ * Uses matchMedia for efficient, event-driven updates.
9
+ */
10
+ declare function useMobile(): boolean;
11
+ /**
12
+ * Tracks IME composition state for text inputs.
13
+ * Useful for preventing premature input handling during CJK text composition.
14
+ *
15
+ * @returns { isComposing, onCompositionStart, onCompositionEnd }
16
+ */
17
+ declare function useComposition(): {
18
+ isComposing: boolean;
19
+ onCompositionStart: () => void;
20
+ onCompositionEnd: () => void;
21
+ };
22
+ /**
23
+ * Returns a stable callback reference that always calls the latest version
24
+ * of the provided function. Useful for avoiding stale closures in effects
25
+ * and event handlers without adding the function to dependency arrays.
26
+ *
27
+ * @param fn - The function to persist
28
+ * @returns A stable reference that delegates to the latest fn
29
+ */
30
+ declare function usePersistFn<T extends (...args: any[]) => any>(fn: T): T;
31
+
32
+ export { useComposition, useMobile, usePersistFn };
@@ -0,0 +1,41 @@
1
+ // src/hooks/index.ts
2
+ import { useCallback, useEffect, useRef, useState } from "react";
3
+ var MOBILE_BREAKPOINT = 768;
4
+ function useMobile() {
5
+ const [isMobile, setIsMobile] = useState(() => {
6
+ if (typeof window === "undefined") return false;
7
+ return window.innerWidth < MOBILE_BREAKPOINT;
8
+ });
9
+ useEffect(() => {
10
+ const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`);
11
+ const onChange = () => setIsMobile(mql.matches);
12
+ mql.addEventListener("change", onChange);
13
+ setIsMobile(mql.matches);
14
+ return () => mql.removeEventListener("change", onChange);
15
+ }, []);
16
+ return isMobile;
17
+ }
18
+ function useComposition() {
19
+ const [isComposing, setIsComposing] = useState(false);
20
+ const onCompositionStart = useCallback(() => {
21
+ setIsComposing(true);
22
+ }, []);
23
+ const onCompositionEnd = useCallback(() => {
24
+ setIsComposing(false);
25
+ }, []);
26
+ return { isComposing, onCompositionStart, onCompositionEnd };
27
+ }
28
+ function usePersistFn(fn) {
29
+ const fnRef = useRef(fn);
30
+ fnRef.current = fn;
31
+ const persistFn = useCallback((...args) => {
32
+ return fnRef.current(...args);
33
+ }, []);
34
+ return persistFn;
35
+ }
36
+ export {
37
+ useComposition,
38
+ useMobile,
39
+ usePersistFn
40
+ };
41
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/hooks/index.ts"],"sourcesContent":["/**\n * @oplytics/shared-ui/hooks\n *\n * Shared utility hooks used across all Oplytics.digital subdomains.\n */\n\nimport { useCallback, useEffect, useRef, useState } from \"react\";\n\n// ─── useMobile ───────────────────────────────────────────────────────────────\n\nconst MOBILE_BREAKPOINT = 768;\n\n/**\n * Detects whether the viewport is at mobile width.\n * Uses matchMedia for efficient, event-driven updates.\n */\nexport function useMobile(): boolean {\n const [isMobile, setIsMobile] = useState<boolean>(() => {\n if (typeof window === \"undefined\") return false;\n return window.innerWidth < MOBILE_BREAKPOINT;\n });\n\n useEffect(() => {\n const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`);\n const onChange = () => setIsMobile(mql.matches);\n mql.addEventListener(\"change\", onChange);\n setIsMobile(mql.matches);\n return () => mql.removeEventListener(\"change\", onChange);\n }, []);\n\n return isMobile;\n}\n\n// ─── useComposition ──────────────────────────────────────────────────────────\n\n/**\n * Tracks IME composition state for text inputs.\n * Useful for preventing premature input handling during CJK text composition.\n *\n * @returns { isComposing, onCompositionStart, onCompositionEnd }\n */\nexport function useComposition() {\n const [isComposing, setIsComposing] = useState(false);\n\n const onCompositionStart = useCallback(() => {\n setIsComposing(true);\n }, []);\n\n const onCompositionEnd = useCallback(() => {\n setIsComposing(false);\n }, []);\n\n return { isComposing, onCompositionStart, onCompositionEnd };\n}\n\n// ─── usePersistFn ────────────────────────────────────────────────────────────\n\n/**\n * Returns a stable callback reference that always calls the latest version\n * of the provided function. Useful for avoiding stale closures in effects\n * and event handlers without adding the function to dependency arrays.\n *\n * @param fn - The function to persist\n * @returns A stable reference that delegates to the latest fn\n */\nexport function usePersistFn<T extends (...args: any[]) => any>(fn: T): T {\n const fnRef = useRef<T>(fn);\n fnRef.current = fn;\n\n const persistFn = useCallback((...args: any[]) => {\n return fnRef.current(...args);\n }, []);\n\n return persistFn as T;\n}\n"],"mappings":";AAMA,SAAS,aAAa,WAAW,QAAQ,gBAAgB;AAIzD,IAAM,oBAAoB;AAMnB,SAAS,YAAqB;AACnC,QAAM,CAAC,UAAU,WAAW,IAAI,SAAkB,MAAM;AACtD,QAAI,OAAO,WAAW,YAAa,QAAO;AAC1C,WAAO,OAAO,aAAa;AAAA,EAC7B,CAAC;AAED,YAAU,MAAM;AACd,UAAM,MAAM,OAAO,WAAW,eAAe,oBAAoB,CAAC,KAAK;AACvE,UAAM,WAAW,MAAM,YAAY,IAAI,OAAO;AAC9C,QAAI,iBAAiB,UAAU,QAAQ;AACvC,gBAAY,IAAI,OAAO;AACvB,WAAO,MAAM,IAAI,oBAAoB,UAAU,QAAQ;AAAA,EACzD,GAAG,CAAC,CAAC;AAEL,SAAO;AACT;AAUO,SAAS,iBAAiB;AAC/B,QAAM,CAAC,aAAa,cAAc,IAAI,SAAS,KAAK;AAEpD,QAAM,qBAAqB,YAAY,MAAM;AAC3C,mBAAe,IAAI;AAAA,EACrB,GAAG,CAAC,CAAC;AAEL,QAAM,mBAAmB,YAAY,MAAM;AACzC,mBAAe,KAAK;AAAA,EACtB,GAAG,CAAC,CAAC;AAEL,SAAO,EAAE,aAAa,oBAAoB,iBAAiB;AAC7D;AAYO,SAAS,aAAgD,IAAU;AACxE,QAAM,QAAQ,OAAU,EAAE;AAC1B,QAAM,UAAU;AAEhB,QAAM,YAAY,YAAY,IAAI,SAAgB;AAChD,WAAO,MAAM,QAAQ,GAAG,IAAI;AAAA,EAC9B,GAAG,CAAC,CAAC;AAEL,SAAO;AACT;","names":[]}
@@ -0,0 +1,336 @@
1
+ import React__default, { ReactNode } from 'react';
2
+ import * as react_jsx_runtime from 'react/jsx-runtime';
3
+
4
+ /**
5
+ * SharedSidebar — Config-driven sidebar component for all Oplytics subdomains.
6
+ *
7
+ * Replaces per-subdomain DashboardLayout sidebar implementations with a single
8
+ * shared component. Each subdomain provides its own configuration (menu items,
9
+ * service name, feature flags) and gets a consistent sidebar experience.
10
+ *
11
+ * Features:
12
+ * - Resizable sidebar with localStorage persistence
13
+ * - Collapsible icon mode with tooltips
14
+ * - Service branding (icon + name) in header
15
+ * - "Back to Service Hub/Portal" link (configurable)
16
+ * - Grouped menu sections with optional section labels
17
+ * - Admin-only menu items (role-gated)
18
+ * - User avatar footer with optional logout dropdown
19
+ * - Mobile-responsive with auto-collapse
20
+ *
21
+ * Usage:
22
+ * import { createServiceLayout } from "@shared/components/SharedSidebar";
23
+ * const MyLayout = createServiceLayout({ serviceName: "SQDCP", ... });
24
+ * // Then use <MyLayout>{children}</MyLayout> in your routes
25
+ *
26
+ * @module shared/components/SharedSidebar
27
+ */
28
+
29
+ interface MenuItem {
30
+ icon: React__default.ComponentType<{
31
+ className?: string;
32
+ }>;
33
+ label: string;
34
+ path: string;
35
+ /** Optional badge text (e.g. count) */
36
+ badge?: string | number;
37
+ }
38
+ interface MenuSection {
39
+ /** Section label displayed above items */
40
+ label?: string;
41
+ /** Alias for label — used by DashboardLayout */
42
+ title?: string;
43
+ items: MenuItem[];
44
+ adminOnly?: boolean;
45
+ }
46
+ interface SharedSidebarConfig {
47
+ /** Display name shown in sidebar header */
48
+ serviceName: string;
49
+ /** Icon component rendered next to service name */
50
+ serviceIcon: React__default.ReactNode;
51
+ /** Primary navigation items */
52
+ menuSections: MenuSection[];
53
+ /** Show "Back to Service Hub" / "Back to Portal" link at top of nav */
54
+ backLink?: {
55
+ label: string;
56
+ path: string;
57
+ };
58
+ /** localStorage key prefix for sidebar width persistence */
59
+ storageKeyPrefix?: string;
60
+ /** Default sidebar width in pixels (default: 260) */
61
+ defaultWidth?: number;
62
+ /** Minimum sidebar width in pixels (default: 200) */
63
+ minWidth?: number;
64
+ /** Maximum sidebar width in pixels (default: 480) */
65
+ maxWidth?: number;
66
+ }
67
+ interface SharedSidebarProps {
68
+ config: SharedSidebarConfig;
69
+ /** Current route location */
70
+ location: string;
71
+ /** Navigate to a path */
72
+ setLocation: (path: string) => void;
73
+ /** Current user object (null if not authenticated) */
74
+ user: {
75
+ name?: string;
76
+ email?: string;
77
+ role?: string;
78
+ } | null;
79
+ /** Logout handler */
80
+ onLogout?: () => void;
81
+ /** Whether sidebar is collapsed */
82
+ isCollapsed: boolean;
83
+ /** Toggle sidebar collapsed state */
84
+ toggleSidebar: () => void;
85
+ /** Whether user has admin role */
86
+ isAdmin: boolean;
87
+ /** Children rendered in SidebarInset main area */
88
+ children: React__default.ReactNode;
89
+ }
90
+ declare function isAdminRole(role?: string): boolean;
91
+ declare function getInitials(name?: string): string;
92
+ /**
93
+ * Determines if a menu item is active based on current location.
94
+ * Exact match for root paths, startsWith for nested paths.
95
+ */
96
+ declare function isMenuItemActive(itemPath: string, location: string, basePath?: string): boolean;
97
+ declare function useSidebarResize(config: {
98
+ storageKey: string;
99
+ defaultWidth: number;
100
+ minWidth: number;
101
+ maxWidth: number;
102
+ }): {
103
+ width: number;
104
+ isResizing: boolean;
105
+ startResize: (e: React__default.MouseEvent) => void;
106
+ };
107
+
108
+ interface DashboardLayoutUser {
109
+ name: string;
110
+ email?: string;
111
+ role?: string;
112
+ avatarUrl?: string;
113
+ }
114
+ interface DashboardLayoutProps {
115
+ /** Service/module name displayed in the sidebar header */
116
+ serviceName: string;
117
+ /** Optional service icon (ReactNode) */
118
+ serviceIcon?: ReactNode;
119
+ /** Menu sections with items */
120
+ menuSections: MenuSection[];
121
+ /** Currently active path/route */
122
+ activePath: string;
123
+ /** Handler when a menu item is clicked */
124
+ onNavigate: (path: string) => void;
125
+ /** Current user info */
126
+ user?: DashboardLayoutUser | null;
127
+ /** Whether the user is authenticated */
128
+ isAuthenticated?: boolean;
129
+ /** Whether auth state is loading */
130
+ isLoading?: boolean;
131
+ /** Logout handler */
132
+ onLogout?: () => void;
133
+ /** Login handler / redirect */
134
+ onLogin?: () => void;
135
+ /** Show "Back to Portal" link (default: false) */
136
+ showBackToPortal?: boolean;
137
+ /** Portal URL for "Back to Portal" link */
138
+ portalUrl?: string;
139
+ /** Whether user has admin role */
140
+ isAdmin?: boolean;
141
+ /** Admin menu sections (shown only for admins) */
142
+ adminSections?: MenuSection[];
143
+ /** Optional hierarchy navigator to render in the sidebar */
144
+ hierarchyNavigator?: ReactNode;
145
+ /** Optional reporting toolbar to render in the header */
146
+ reportingToolbar?: ReactNode;
147
+ /** Loading skeleton to show while auth is loading */
148
+ loadingSkeleton?: ReactNode;
149
+ /** Main content */
150
+ children: ReactNode;
151
+ /** Additional CSS class for the outer container */
152
+ className?: string;
153
+ }
154
+ declare function DashboardLayout({ serviceName, serviceIcon, menuSections, activePath, onNavigate, user, isAuthenticated, isLoading, onLogout, onLogin, showBackToPortal, portalUrl, isAdmin, adminSections, hierarchyNavigator, reportingToolbar, loadingSkeleton, children, className, }: DashboardLayoutProps): react_jsx_runtime.JSX.Element | null;
155
+
156
+ interface DashboardLayoutSkeletonProps {
157
+ className?: string;
158
+ }
159
+ declare function DashboardLayoutSkeleton({ className }: DashboardLayoutSkeletonProps): react_jsx_runtime.JSX.Element;
160
+
161
+ interface SharedPageHeaderProps {
162
+ /** Page title */
163
+ title: string;
164
+ /** Optional subtitle / description */
165
+ subtitle?: string;
166
+ /** Optional icon to display before the title */
167
+ icon?: ReactNode;
168
+ /** Optional back button handler — shows a back arrow when provided */
169
+ onBack?: () => void;
170
+ /** Optional action buttons to render on the right side */
171
+ actions?: ReactNode;
172
+ /** Optional breadcrumb trail (e.g. HierarchyNavigator) */
173
+ breadcrumbs?: ReactNode;
174
+ /** Whether to show a bottom border (default: true) */
175
+ showBorder?: boolean;
176
+ /** Compact mode — smaller spacing and text (default: false) */
177
+ compact?: boolean;
178
+ /** Additional CSS classes */
179
+ className?: string;
180
+ /** Children rendered below the header row */
181
+ children?: ReactNode;
182
+ }
183
+ declare function SharedPageHeader({ title, subtitle, icon, onBack, actions, breadcrumbs, showBorder, compact, className, children, }: SharedPageHeaderProps): react_jsx_runtime.JSX.Element;
184
+
185
+ interface FooterService {
186
+ name: string;
187
+ slug: string;
188
+ /** Local route within the portal (e.g. /services/sqdcp) */
189
+ localRoute: string;
190
+ /** External URL for the subdomain (e.g. https://sqdcp.oplytics.digital) */
191
+ externalUrl?: string;
192
+ }
193
+ /** Standard Oplytics service list */
194
+ declare const FOOTER_SERVICES: FooterService[];
195
+ interface SharedFooterProps {
196
+ /** Override the default service list */
197
+ services?: FooterService[];
198
+ /** Show pricing link (default: true) */
199
+ showPricing?: boolean;
200
+ /** Show sign-in link (default: true) */
201
+ showSignIn?: boolean;
202
+ /** Handler for service link clicks — receives the service */
203
+ onServiceClick?: (service: FooterService, e: React.MouseEvent) => void;
204
+ /** Handler for pricing link click */
205
+ onPricingClick?: (e: React.MouseEvent) => void;
206
+ /** Handler for sign-in link click */
207
+ onSignInClick?: (e: React.MouseEvent) => void;
208
+ /** Additional content to render in the footer */
209
+ children?: ReactNode;
210
+ /** CSS class for the footer container */
211
+ className?: string;
212
+ /** Whether to use external URLs (subdomain links) or local routes */
213
+ useExternalUrls?: boolean;
214
+ /** Current year override (default: auto) */
215
+ year?: number;
216
+ }
217
+ declare function SharedFooter({ services, showPricing, showSignIn, onServiceClick, onPricingClick, onSignInClick, children, className, useExternalUrls, year, }: SharedFooterProps): react_jsx_runtime.JSX.Element;
218
+
219
+ /**
220
+ * createServiceLayout — Factory function that generates a complete DashboardLayout
221
+ * for any Oplytics subdomain from a SharedSidebarConfig.
222
+ *
223
+ * This is the primary API for subdomains. Instead of each subdomain maintaining
224
+ * its own 200-350 line DashboardLayout, they call:
225
+ *
226
+ * const SQDCPLayout = createServiceLayout({ serviceName: "SQDCP", ... });
227
+ *
228
+ * And get a fully-featured layout with:
229
+ * - Resizable sidebar with all standard features
230
+ * - SharedPageHeader integration
231
+ * - ReportingToolbar integration (optional)
232
+ * - HierarchyProvider wrapping (optional)
233
+ * - Auth gate with loading skeleton + sign-in fallback (optional)
234
+ * - User footer with logout dropdown
235
+ *
236
+ * @module shared/components/createServiceLayout
237
+ */
238
+
239
+ interface ServiceLayoutConfig extends SharedSidebarConfig {
240
+ /** Show SharedPageHeader above content (default: true) */
241
+ showPageHeader?: boolean;
242
+ /** SharedPageHeader props — passed through when showPageHeader is true */
243
+ pageHeaderProps?: {
244
+ showHierarchy?: boolean;
245
+ hierarchyMaxDepth?: number;
246
+ showSidebarTrigger?: boolean;
247
+ };
248
+ /** Show ReportingToolbar in the header (default: false) */
249
+ showReportingToolbar?: boolean;
250
+ /** Module name passed to ReportingToolbar */
251
+ reportingModuleName?: string;
252
+ /** Wrap layout in HierarchyProvider (default: false) */
253
+ wrapHierarchyProvider?: boolean;
254
+ /** Show auth gate — loading skeleton + sign-in fallback (default: true) */
255
+ showAuthGate?: boolean;
256
+ /** Report submit handler */
257
+ onReportSubmit?: (report: any) => Promise<void> | void;
258
+ /** Feedback submit handler */
259
+ onFeedbackSubmit?: (feedback: any) => Promise<void> | void;
260
+ }
261
+ /**
262
+ * Creates a service-specific layout component from configuration.
263
+ *
264
+ * NOTE: This is a blueprint/factory specification. The actual React component
265
+ * is assembled by each subdomain using their local UI primitives (Sidebar,
266
+ * SidebarProvider, etc.) since these are not shared across subdomains yet.
267
+ *
268
+ * When subdomain consolidation (#311) is complete, this factory will directly
269
+ * render the shared UI primitives. Until then, subdomains use this config
270
+ * to standardise their DashboardLayout implementations.
271
+ *
272
+ * @example
273
+ * ```tsx
274
+ * // In SQDCP's DashboardLayout.tsx:
275
+ * import { sqdcpLayoutConfig } from "./sqdcp-config";
276
+ * // Use sqdcpLayoutConfig.menuSections, .backLink, etc. to build the layout
277
+ * ```
278
+ */
279
+ declare function defineServiceLayout(config: ServiceLayoutConfig): ServiceLayoutConfig;
280
+
281
+ /**
282
+ * Canonical Service Layout Configurations for all Oplytics subdomains.
283
+ *
284
+ * Each subdomain imports its config from here and uses it to build its
285
+ * DashboardLayout. This ensures consistency across all services while
286
+ * allowing per-service customisation (menu items, features, etc.).
287
+ *
288
+ * When subdomain consolidation (#311) is complete, these configs will
289
+ * drive the shared createServiceLayout factory directly.
290
+ *
291
+ * @module shared/components/serviceConfigs
292
+ */
293
+ interface MenuItemConfig {
294
+ iconName: string;
295
+ label: string;
296
+ path: string;
297
+ }
298
+ interface MenuSectionConfig {
299
+ label?: string;
300
+ items: MenuItemConfig[];
301
+ adminOnly?: boolean;
302
+ }
303
+ interface ServiceConfig {
304
+ serviceName: string;
305
+ serviceAbbreviation: string;
306
+ serviceIconName: string;
307
+ menuSections: MenuSectionConfig[];
308
+ backLink?: {
309
+ label: string;
310
+ path: string;
311
+ };
312
+ showPageHeader: boolean;
313
+ showReportingToolbar: boolean;
314
+ reportingModuleName?: string;
315
+ wrapHierarchyProvider: boolean;
316
+ showAuthGate: boolean;
317
+ storageKeyPrefix: string;
318
+ defaultWidth: number;
319
+ minWidth: number;
320
+ maxWidth: number;
321
+ }
322
+ declare const sqdcpConfig: ServiceConfig;
323
+ declare const oeeManagerConfig: ServiceConfig;
324
+ declare const actionManagerConfig: ServiceConfig;
325
+ declare const businessHubConfig: ServiceConfig;
326
+ declare const policyDeploymentConfig: ServiceConfig;
327
+ declare const SERVICE_CONFIGS: {
328
+ readonly sqdcp: ServiceConfig;
329
+ readonly oeeManager: ServiceConfig;
330
+ readonly actionManager: ServiceConfig;
331
+ readonly businessHub: ServiceConfig;
332
+ readonly policyDeployment: ServiceConfig;
333
+ };
334
+ type ServiceKey = keyof typeof SERVICE_CONFIGS;
335
+
336
+ export { DashboardLayout, type DashboardLayoutProps, DashboardLayoutSkeleton, type DashboardLayoutSkeletonProps, type DashboardLayoutUser, FOOTER_SERVICES, type FooterService, type MenuItem, type MenuItemConfig, type MenuSection, type MenuSectionConfig, SERVICE_CONFIGS, type ServiceConfig, type ServiceKey, type ServiceLayoutConfig, SharedFooter, type SharedFooterProps, SharedPageHeader, type SharedPageHeaderProps, type SharedSidebarConfig, type SharedSidebarProps, type SharedSidebarConfig as SidebarConfig, actionManagerConfig, businessHubConfig, defineServiceLayout, getInitials, isAdminRole, isMenuItemActive, oeeManagerConfig, policyDeploymentConfig, sqdcpConfig, useSidebarResize };