@eintrek/erp-shell 0.1.56 → 0.1.58
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.
- package/README.md +10 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.esm.js +253 -107
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +254 -106
- package/dist/index.js.map +1 -1
- package/dist/navigation/navigation-data-helpers.d.ts +80 -3
- package/dist/navigation/navigation-types.d.ts +6 -0
- package/dist/version.d.ts +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -86,6 +86,16 @@ Shell now owns the **shapes + helpers** for navigation; apps own the data.
|
|
|
86
86
|
- Components: `NavTools` (reads Tool[] + buildUrlWithCode internally),
|
|
87
87
|
`AppBreadcrumb` (accepts `labels` prop so per-app Thai overrides stay local).
|
|
88
88
|
|
|
89
|
+
Sidebar highlight (0.1.58, `resolveSidebarNav`): an entry is lit only on
|
|
90
|
+
its own page — its URL or one of its `subPages`, exactly, after the org code,
|
|
91
|
+
ignoring a trailing slash — and at most one entry is lit. A record, a create
|
|
92
|
+
or an edit page below a menu page lights nothing; the group holding it (the
|
|
93
|
+
deepest URL the path sits below) opens. A page that should keep its entry lit
|
|
94
|
+
(a list's tab that is its own route) goes in that entry's `subPages`, with a
|
|
95
|
+
`permissionRouteKey` the user holds (usually the entry's own), since the
|
|
96
|
+
permission filter drops a sub-page without one. `getActiveModule` stays the
|
|
97
|
+
longest prefix; `getActiveSubModule` is exact like the sidebar.
|
|
98
|
+
|
|
89
99
|
Each app's `navigation-data.ts` now (1) imports types/helpers from shell,
|
|
90
100
|
(2) declares its own `navigationData` const + `breadcrumbLabels`, (3) wraps
|
|
91
101
|
`getActiveModule`/`getActiveSubModule` to keep the 1-arg signature, and
|
package/dist/index.d.ts
CHANGED
|
@@ -38,7 +38,7 @@ export { PermissionProvider, type PermissionProviderProps, type PermissionProbe,
|
|
|
38
38
|
export { getRequiredChildGroups, normalizePathnameForPermissionRules, resolveUiRoutePermission, type UiRoutePermissionLookup, } from "./auth/permission-utils";
|
|
39
39
|
export * from "./navigation/navigation-helpers";
|
|
40
40
|
export type { Module, SubPage, SubModule, Tool, Company, NavigationData, } from "./navigation/navigation-types";
|
|
41
|
-
export { canAccessNavPermissionRoute, filterNavigationToolsByPermission, filterNavigationModulesByPermission, getActiveModule, getActiveSubModule, buildUrlWithCode, } from "./navigation/navigation-data-helpers";
|
|
41
|
+
export { canAccessNavPermissionRoute, filterNavigationToolsByPermission, filterNavigationModulesByPermission, getActiveModule, getActiveSubModule, buildUrlWithCode, resolveActiveNav, resolveSidebarNav, type ActiveNavEntry, type SidebarNavState, type ResolveActiveNavOptions, } from "./navigation/navigation-data-helpers";
|
|
42
42
|
export { ThemeProvider } from "./providers/theme-provider";
|
|
43
43
|
export { FeatureFlagsProvider, useFeatureFlags, useFeatureFlag, type FeatureFlags, } from "./providers/feature-flags-provider";
|
|
44
44
|
export { ReactQueryProvider } from "./providers/react-query-provider";
|
package/dist/index.esm.js
CHANGED
|
@@ -703,43 +703,79 @@ function filterNavigationModulesByPermission(modules, rulesMap, userChildSegment
|
|
|
703
703
|
})
|
|
704
704
|
.filter((m) => m !== null);
|
|
705
705
|
}
|
|
706
|
+
/** The path below the org code: "/O1/project/budgets" → "/project/budgets". */
|
|
707
|
+
function pathBelowCode(pathname) {
|
|
708
|
+
const segments = pathname.split("?")[0].split("/").filter(Boolean);
|
|
709
|
+
return segments.length > 1 ? "/" + segments.slice(1).join("/") : "/";
|
|
710
|
+
}
|
|
706
711
|
/**
|
|
707
|
-
*
|
|
708
|
-
*
|
|
712
|
+
* Whether `url` (relative, no org code) covers `path`: the same page, or a
|
|
713
|
+
* page below it on a segment boundary. "/" covers only the home itself and a
|
|
714
|
+
* query-only URL covers nothing.
|
|
709
715
|
*/
|
|
710
|
-
function
|
|
711
|
-
|
|
712
|
-
|
|
716
|
+
function coversPath(url, path) {
|
|
717
|
+
if (!url || url.includes("?") || /^https?:\/\//i.test(url))
|
|
718
|
+
return false;
|
|
719
|
+
if (url === "/")
|
|
720
|
+
return path === "/";
|
|
721
|
+
const clean = url.replace(/\/+$/, "");
|
|
722
|
+
return path === clean || path.startsWith(clean + "/");
|
|
723
|
+
}
|
|
724
|
+
/**
|
|
725
|
+
* The module and page whose URL is the longest one covering the path, over
|
|
726
|
+
* every module — a module's own URL, its pages and their sub-pages. Equal
|
|
727
|
+
* lengths go to the first in menu order.
|
|
728
|
+
*/
|
|
729
|
+
function longestMatch(path, modules) {
|
|
730
|
+
let best = null;
|
|
731
|
+
const consider = (module, sub, url) => {
|
|
732
|
+
if (!coversPath(url, path))
|
|
733
|
+
return;
|
|
734
|
+
if (!best || url.length > best.length) {
|
|
735
|
+
best = { module, sub, length: url.length };
|
|
736
|
+
}
|
|
737
|
+
};
|
|
713
738
|
for (const navModule of modules) {
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
for (const subModule of navModule.subModules) {
|
|
719
|
-
if (pathWithoutCode.startsWith(subModule.url) ||
|
|
720
|
-
pathname.includes(subModule.url)) {
|
|
721
|
-
return navModule.id;
|
|
722
|
-
}
|
|
723
|
-
}
|
|
739
|
+
for (const sub of navModule.subModules ?? []) {
|
|
740
|
+
consider(navModule, sub, sub.url);
|
|
741
|
+
for (const page of sub.subPages ?? [])
|
|
742
|
+
consider(navModule, sub, page.url);
|
|
724
743
|
}
|
|
744
|
+
// After its pages: on a tie the page is the more specific answer.
|
|
745
|
+
consider(navModule, null, navModule.url);
|
|
725
746
|
}
|
|
726
|
-
|
|
747
|
+
const found = best;
|
|
748
|
+
return found ? { module: found.module, sub: found.sub } : null;
|
|
749
|
+
}
|
|
750
|
+
/**
|
|
751
|
+
* Find the active top-level module id from `pathname`: the module whose URL
|
|
752
|
+
* (or one of whose pages' URLs) is the longest prefix of the path below the
|
|
753
|
+
* org code.
|
|
754
|
+
*
|
|
755
|
+
* Before 0.1.57 this took the first module with a page URL anywhere in the
|
|
756
|
+
* path (`includes`), so "/project" claimed /project/schedule and the sidebar,
|
|
757
|
+
* the trail and the tabs could disagree about where the user was.
|
|
758
|
+
*/
|
|
759
|
+
function getActiveModule(pathname, modules) {
|
|
760
|
+
return longestMatch(pathBelowCode(pathname), modules)?.module.id ?? null;
|
|
727
761
|
}
|
|
728
|
-
/**
|
|
762
|
+
/**
|
|
763
|
+
* Find the active sub-module id from `pathname`: the page whose own URL — or
|
|
764
|
+
* one of whose listed `subPages` — is the path itself (below the org code,
|
|
765
|
+
* ignoring a trailing slash). A record, a create or an edit page below a page
|
|
766
|
+
* is no page of the menu, so it has no active sub-module (0.1.58); the module
|
|
767
|
+
* (`getActiveModule`) still says where it belongs.
|
|
768
|
+
*
|
|
769
|
+
* Equal URLs in two modules go to the active module's page.
|
|
770
|
+
*/
|
|
729
771
|
function getActiveSubModule(pathname, modules) {
|
|
730
|
-
const
|
|
731
|
-
const
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
return subModule.id;
|
|
738
|
-
}
|
|
739
|
-
}
|
|
740
|
-
}
|
|
741
|
-
}
|
|
742
|
-
return null;
|
|
772
|
+
const path = strip(pathBelowCode(pathname));
|
|
773
|
+
const isPage = (url) => isMenuPath(url) && strip(url) === path;
|
|
774
|
+
const pagesOf = (m) => (m.subModules ?? []).filter(sub => isPage(sub.url) || (sub.subPages ?? []).some(p => isPage(p.url)));
|
|
775
|
+
const moduleId = getActiveModule(pathname, modules);
|
|
776
|
+
const own = modules.find(m => m.id === moduleId);
|
|
777
|
+
const hit = (own ? pagesOf(own)[0] : undefined) ?? modules.flatMap(pagesOf)[0];
|
|
778
|
+
return hit?.id ?? null;
|
|
743
779
|
}
|
|
744
780
|
/** Prepend `code` (org code) to a relative URL so `/sales` → `/O25.../sales`.
|
|
745
781
|
* Absolute `http(s)://` URLs are left intact; optional `{code}` placeholder is replaced.
|
|
@@ -761,6 +797,140 @@ function buildUrlWithCode(code, url) {
|
|
|
761
797
|
const base = cleanPath ? `/${code}/${cleanPath}` : `/${code}`;
|
|
762
798
|
return `${base}${query}${hash}`;
|
|
763
799
|
}
|
|
800
|
+
const strip = (p) => (p.length > 1 ? p.replace(/\/+$/, "") : p);
|
|
801
|
+
/** A URL the path can be compared with: not query-only, not another site. */
|
|
802
|
+
const isMenuPath = (url) => !!url && !url.includes("?") && !/^https?:\/\//i.test(url);
|
|
803
|
+
/** Same check as `isPathActive`: the page itself or one below it. */
|
|
804
|
+
const pathIsAtOrBelow = (pathname, url) => pathname === url || pathname.startsWith(url + "/");
|
|
805
|
+
/** "/O1/" and "/O1" are the same page; a query string never is. */
|
|
806
|
+
function samePage(pathname, url) {
|
|
807
|
+
if (url.includes("?"))
|
|
808
|
+
return false;
|
|
809
|
+
return strip(pathname.split("?")[0]) === strip(url);
|
|
810
|
+
}
|
|
811
|
+
/**
|
|
812
|
+
* How long `url` is when it is the page `pathname` shows (ignoring a trailing
|
|
813
|
+
* slash), or -1. Compared with and without the org code, as the menu URLs
|
|
814
|
+
* are written either way.
|
|
815
|
+
*/
|
|
816
|
+
function pageLength(pathname, url, code) {
|
|
817
|
+
if (!isMenuPath(url))
|
|
818
|
+
return -1;
|
|
819
|
+
const built = buildUrlWithCode(code, url);
|
|
820
|
+
if (samePage(pathname, built))
|
|
821
|
+
return strip(built).length;
|
|
822
|
+
if (samePage(pathname, url))
|
|
823
|
+
return strip(url).length;
|
|
824
|
+
return -1;
|
|
825
|
+
}
|
|
826
|
+
/**
|
|
827
|
+
* How long the URL that makes `url` cover `pathname` is — the page itself or
|
|
828
|
+
* one below it — or -1. The organisation's home ("/" → "/O1") covers only
|
|
829
|
+
* itself: every page sits below it, and a dashboard is not the parent of
|
|
830
|
+
* every page.
|
|
831
|
+
*/
|
|
832
|
+
function coverLength(pathname, url, code) {
|
|
833
|
+
if (!isMenuPath(url))
|
|
834
|
+
return -1;
|
|
835
|
+
const path = strip(pathname.split("?")[0]);
|
|
836
|
+
const built = strip(buildUrlWithCode(code, url));
|
|
837
|
+
const isRoot = url === "/" || (!!code && built === `/${code}`);
|
|
838
|
+
if (isRoot)
|
|
839
|
+
return pageLength(path, url, code);
|
|
840
|
+
if (pathIsAtOrBelow(path, built))
|
|
841
|
+
return built.length;
|
|
842
|
+
const raw = strip(url);
|
|
843
|
+
if (pathIsAtOrBelow(path, raw))
|
|
844
|
+
return raw.length;
|
|
845
|
+
return -1;
|
|
846
|
+
}
|
|
847
|
+
/**
|
|
848
|
+
* Where `pathname` sits in the sidebar: the entry to light and the group to
|
|
849
|
+
* open.
|
|
850
|
+
*
|
|
851
|
+
* Lit (0.1.58): only the entry whose own page this is — its URL or one of its
|
|
852
|
+
* `subPages`, exactly. A record, a create or an edit page below a menu page
|
|
853
|
+
* (Plannex /project/detail/<id>/schedule under "โครงการ" /project) lights
|
|
854
|
+
* nothing; an app lists such a page in `subPages` when it should keep its
|
|
855
|
+
* entry lit. At most one entry across every group (0.1.57).
|
|
856
|
+
*
|
|
857
|
+
* Open: the lit entry's group, or else the group with the deepest URL the
|
|
858
|
+
* path sits below.
|
|
859
|
+
*
|
|
860
|
+
* Equal lengths go to the group the app marks active (`item.isActive`, from
|
|
861
|
+
* its `activeModule`), then to a page over a group row, then to the first in
|
|
862
|
+
* menu order.
|
|
863
|
+
*/
|
|
864
|
+
function resolveSidebarNav(items, pathname, { code, highlightActiveModule = false } = {}) {
|
|
865
|
+
const path = (pathname ?? "").split("?")[0];
|
|
866
|
+
// The app named a one-line module as the current one: that is the answer.
|
|
867
|
+
if (highlightActiveModule) {
|
|
868
|
+
const leaf = items.findIndex(item => !item.subModules?.length && !!item.isActive);
|
|
869
|
+
if (leaf >= 0) {
|
|
870
|
+
return {
|
|
871
|
+
active: { item: leaf, sub: null, subPage: null },
|
|
872
|
+
open: { item: leaf, sub: null },
|
|
873
|
+
};
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
const better = (c, best) => {
|
|
877
|
+
if (!best)
|
|
878
|
+
return true;
|
|
879
|
+
if (c.length !== best.length)
|
|
880
|
+
return c.length > best.length;
|
|
881
|
+
const cActive = !!items[c.item].isActive;
|
|
882
|
+
const bActive = !!items[best.item].isActive;
|
|
883
|
+
if (cActive !== bActive)
|
|
884
|
+
return cActive;
|
|
885
|
+
// The same sub-item by its own URL and by a listed page: the page.
|
|
886
|
+
if (c.item === best.item && c.sub === best.sub) {
|
|
887
|
+
return c.subPage !== null && best.subPage === null;
|
|
888
|
+
}
|
|
889
|
+
return c.sub !== null && best.sub === null;
|
|
890
|
+
};
|
|
891
|
+
let exact = null;
|
|
892
|
+
let within = null;
|
|
893
|
+
const consider = (c, isExact) => {
|
|
894
|
+
if (c.length < 0)
|
|
895
|
+
return;
|
|
896
|
+
if (isExact) {
|
|
897
|
+
if (better(c, exact))
|
|
898
|
+
exact = c;
|
|
899
|
+
}
|
|
900
|
+
else if (better(c, within)) {
|
|
901
|
+
within = c;
|
|
902
|
+
}
|
|
903
|
+
};
|
|
904
|
+
items.forEach((item, i) => {
|
|
905
|
+
const subs = item.subModules ?? [];
|
|
906
|
+
const row = { item: i, sub: null, subPage: null };
|
|
907
|
+
// The row itself: only on its own URL. A one-line item under
|
|
908
|
+
// highlightActiveModule is lit by isActive alone (handled above).
|
|
909
|
+
if (!(highlightActiveModule && subs.length === 0)) {
|
|
910
|
+
consider({ ...row, length: pageLength(path, item.url, code) }, true);
|
|
911
|
+
}
|
|
912
|
+
consider({ ...row, length: coverLength(path, item.url, code) }, false);
|
|
913
|
+
subs.forEach((sub, j) => {
|
|
914
|
+
const entry = { item: i, sub: j };
|
|
915
|
+
consider({ ...entry, subPage: null, length: pageLength(path, sub.url, code) }, true);
|
|
916
|
+
consider({ ...entry, subPage: null, length: coverLength(path, sub.url, code) }, false);
|
|
917
|
+
(sub.subPages ?? []).forEach((page, k) => {
|
|
918
|
+
consider({ ...entry, subPage: k, length: pageLength(path, page.url, code) }, true);
|
|
919
|
+
consider({ ...entry, subPage: k, length: coverLength(path, page.url, code) }, false);
|
|
920
|
+
});
|
|
921
|
+
});
|
|
922
|
+
});
|
|
923
|
+
const lit = exact;
|
|
924
|
+
const holder = lit ?? within;
|
|
925
|
+
return {
|
|
926
|
+
active: lit ? { item: lit.item, sub: lit.sub, subPage: lit.subPage } : null,
|
|
927
|
+
open: holder ? { item: holder.item, sub: holder.sub } : null,
|
|
928
|
+
};
|
|
929
|
+
}
|
|
930
|
+
/** The entry lit for `pathname` — `resolveSidebarNav(...).active`. */
|
|
931
|
+
function resolveActiveNav(items, pathname, options = {}) {
|
|
932
|
+
return resolveSidebarNav(items, pathname, options).active;
|
|
933
|
+
}
|
|
764
934
|
|
|
765
935
|
function NavTools({ tools, closeOnNavigation = false }) {
|
|
766
936
|
const pathname = usePathname();
|
|
@@ -1246,7 +1416,7 @@ function platformApiBase(app) {
|
|
|
1246
1416
|
* This package's version, attached to help messages so the back office can
|
|
1247
1417
|
* tell which shell a report came from. Keep in step with package.json.
|
|
1248
1418
|
*/
|
|
1249
|
-
const SHELL_VERSION = "0.1.
|
|
1419
|
+
const SHELL_VERSION = "0.1.58";
|
|
1250
1420
|
|
|
1251
1421
|
const HELP_APP_LABEL = {
|
|
1252
1422
|
platform: "แพลตฟอร์ม",
|
|
@@ -1794,31 +1964,6 @@ function buildSystemLinks({ orgCode, activeApp, features, }) {
|
|
|
1794
1964
|
}));
|
|
1795
1965
|
}
|
|
1796
1966
|
|
|
1797
|
-
// Helper function to check if pathname matches a URL pattern
|
|
1798
|
-
// This prevents false matches like /reports/sales matching /sales
|
|
1799
|
-
// Handles paths with or without [code] prefix
|
|
1800
|
-
function isPathActive(pathname, url) {
|
|
1801
|
-
// Exact match
|
|
1802
|
-
if (pathname === url) {
|
|
1803
|
-
return true;
|
|
1804
|
-
}
|
|
1805
|
-
// Check if pathname starts with url + "/" to ensure exact segment matching
|
|
1806
|
-
// This prevents /reports/sales from matching /sales
|
|
1807
|
-
// Example: /org123/reports/sales should NOT match /org123/sales
|
|
1808
|
-
// But /org123/sales/overview SHOULD match /org123/sales
|
|
1809
|
-
if (pathname.startsWith(url + "/")) {
|
|
1810
|
-
return true;
|
|
1811
|
-
}
|
|
1812
|
-
return false;
|
|
1813
|
-
}
|
|
1814
|
-
|
|
1815
|
-
/** "/O1/" and "/O1" are the same page; a query string never is. */
|
|
1816
|
-
function samePage(pathname, url) {
|
|
1817
|
-
if (url.includes("?"))
|
|
1818
|
-
return false;
|
|
1819
|
-
const strip = (p) => (p.length > 1 ? p.replace(/\/+$/, "") : p);
|
|
1820
|
-
return strip(pathname) === strip(url);
|
|
1821
|
-
}
|
|
1822
1967
|
function NavMain({ items, orgCode, closeOnNavigation = false, approvalCounts, chrome = "brand", highlightActiveModule = false, }) {
|
|
1823
1968
|
const workspace = chrome === "workspace";
|
|
1824
1969
|
const params = useParams();
|
|
@@ -1829,63 +1974,46 @@ function NavMain({ items, orgCode, closeOnNavigation = false, approvalCounts, ch
|
|
|
1829
1974
|
setOpen(false);
|
|
1830
1975
|
};
|
|
1831
1976
|
const code = orgCode ?? params?.code;
|
|
1832
|
-
//
|
|
1833
|
-
|
|
1977
|
+
// The one entry lit for this page — only on that entry's own page, never
|
|
1978
|
+
// on a record or a form below it — and the group holding the page, which
|
|
1979
|
+
// opens either way.
|
|
1980
|
+
const { active, open } = resolveSidebarNav(items, pathname, {
|
|
1981
|
+
code,
|
|
1982
|
+
highlightActiveModule,
|
|
1983
|
+
});
|
|
1984
|
+
const states = items.map((item, i) => {
|
|
1834
1985
|
const itemUrl = buildUrlWithCode(code, item.url);
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
return best;
|
|
1844
|
-
const candidateUrl = subItemUrl;
|
|
1845
|
-
if (!best)
|
|
1846
|
-
return candidateUrl;
|
|
1847
|
-
return candidateUrl.length > best.length ? candidateUrl : best;
|
|
1848
|
-
}, null) ?? null;
|
|
1849
|
-
const hasActiveSubItem = !!activeSubItemUrl ||
|
|
1850
|
-
!!item.subModules?.some(subItem => subItem.subPages?.some(subPage => isPathActive(pathname, buildUrlWithCode(code, subPage.url)) ||
|
|
1851
|
-
isPathActive(pathname, subPage.url)));
|
|
1852
|
-
// Parent only active when path matches its URL exactly AND no child
|
|
1853
|
-
// is active (so parent+child don't both light up). When the app opts
|
|
1854
|
-
// into highlightActiveModule, leaf items trust `item.isActive` alone —
|
|
1855
|
-
// needed for query-only hub actions (`/?start=1`) that share a path
|
|
1856
|
-
// with another leaf (e.g. "แอปของฉัน" at `/`).
|
|
1857
|
-
const isItemActive = highlightActiveModule && !item.subModules?.length
|
|
1858
|
-
? !!item.isActive
|
|
1859
|
-
: (workspace ? samePage(pathname, itemUrl) : pathname === itemUrl) &&
|
|
1860
|
-
!hasActiveSubItem;
|
|
1861
|
-
return { itemUrl, activeSubItemUrl, hasActiveSubItem, isItemActive };
|
|
1986
|
+
const mine = active?.item === i ? active : null;
|
|
1987
|
+
return {
|
|
1988
|
+
itemUrl,
|
|
1989
|
+
activeSub: mine?.sub ?? null,
|
|
1990
|
+
activeSubPage: mine?.subPage ?? null,
|
|
1991
|
+
openSub: open?.item === i ? open.sub : null,
|
|
1992
|
+
isItemActive: !!mine && mine.sub === null,
|
|
1993
|
+
};
|
|
1862
1994
|
});
|
|
1863
|
-
// A group opens when
|
|
1995
|
+
// A group opens when the page moves into it and otherwise stays as the
|
|
1864
1996
|
// user left it. defaultOpen alone decided once, on the first render — and
|
|
1865
1997
|
// on a direct load the sub-items arrive after permissions do, so the group
|
|
1866
1998
|
// holding the current page stayed shut.
|
|
1867
|
-
const
|
|
1868
|
-
|
|
1869
|
-
.map(item => item.id)
|
|
1870
|
-
.join(",");
|
|
1871
|
-
const [openIds, setOpenIds] = React.useState(() => new Set(activeKey ? activeKey.split(",") : []));
|
|
1999
|
+
const openId = open ? (items[open.item]?.id ?? "") : "";
|
|
2000
|
+
const [openIds, setOpenIds] = React.useState(() => new Set(openId ? [openId] : []));
|
|
1872
2001
|
React.useEffect(() => {
|
|
1873
|
-
if (!
|
|
2002
|
+
if (!openId)
|
|
1874
2003
|
return;
|
|
1875
2004
|
setOpenIds(prev => {
|
|
1876
|
-
|
|
1877
|
-
if (ids.every(id => prev.has(id)))
|
|
2005
|
+
if (prev.has(openId))
|
|
1878
2006
|
return prev;
|
|
1879
2007
|
const next = new Set(prev);
|
|
1880
|
-
|
|
2008
|
+
next.add(openId);
|
|
1881
2009
|
return next;
|
|
1882
2010
|
});
|
|
1883
|
-
}, [
|
|
1884
|
-
const setItemOpen = (id,
|
|
1885
|
-
if (prev.has(id) ===
|
|
2011
|
+
}, [openId]);
|
|
2012
|
+
const setItemOpen = (id, isOpen) => setOpenIds(prev => {
|
|
2013
|
+
if (prev.has(id) === isOpen)
|
|
1886
2014
|
return prev;
|
|
1887
2015
|
const next = new Set(prev);
|
|
1888
|
-
if (
|
|
2016
|
+
if (isOpen)
|
|
1889
2017
|
next.add(id);
|
|
1890
2018
|
else
|
|
1891
2019
|
next.delete(id);
|
|
@@ -1896,7 +2024,7 @@ function NavMain({ items, orgCode, closeOnNavigation = false, approvalCounts, ch
|
|
|
1896
2024
|
item.group &&
|
|
1897
2025
|
item.group !== items[index - 1]?.group ? (jsx("li", { className: "list-none px-3 pb-2.5 text-xs text-muted-foreground" +
|
|
1898
2026
|
(index > 0 ? " pt-5" : ""), children: item.group }, `group-${item.id}`)) : null;
|
|
1899
|
-
const { itemUrl,
|
|
2027
|
+
const { itemUrl, activeSub, activeSubPage, openSub, isItemActive } = states[index];
|
|
1900
2028
|
const IconComp = item.icon;
|
|
1901
2029
|
const workspaceButton = workspace
|
|
1902
2030
|
? "h-auto gap-3 rounded-[10px] px-3 py-2.5 text-xs " +
|
|
@@ -1906,13 +2034,13 @@ function NavMain({ items, orgCode, closeOnNavigation = false, approvalCounts, ch
|
|
|
1906
2034
|
: "";
|
|
1907
2035
|
const icon = workspace && IconComp ? (jsx(IconComp, { className: "size-[18px] shrink-0" })) : null;
|
|
1908
2036
|
const badge = workspace && item.badge !== undefined && item.badge !== "" ? (jsx("span", { className: "ml-auto min-w-5 rounded-[5px] bg-primary/15 px-1 text-center text-xs leading-5 font-bold tabular-nums text-primary", children: item.badge })) : null;
|
|
1909
|
-
return (jsxs(React.Fragment, { children: [groupHeading, jsx(Collapsible, { asChild: true, open: openIds.has(item.id), onOpenChange:
|
|
2037
|
+
return (jsxs(React.Fragment, { children: [groupHeading, jsx(Collapsible, { asChild: true, open: openIds.has(item.id), onOpenChange: isOpen => setItemOpen(item.id, isOpen), className: "group/collapsible", children: jsxs(SidebarMenuItem, { children: [jsx(CollapsibleTrigger, { asChild: true, children: jsx(SidebarMenuButton, { asChild: !item.subModules || item.subModules.length === 0, isActive: isItemActive, className: "data-[state=open]:text-sidebar-accent-foreground " +
|
|
1910
2038
|
workspaceButton, children: item.subModules && item.subModules.length > 0 ? (jsxs(Fragment$1, { children: [icon, jsx("span", { id: item.id, children: item.titleTh }), jsx(ChevronRight, { className: "ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90" })] })) : (jsxs(Link, { href: itemUrl, prefetch: false, className: "flex items-center w-full min-w-0 flex-1 " +
|
|
1911
2039
|
(workspace ? "gap-3" : "gap-2"), onClick: handleNavLinkClick, "aria-current": isItemActive ? "page" : undefined, children: [icon, jsx("span", { id: item.id, className: "flex-1 truncate", title: item.titleTh, children: item.titleTh }), badge] })) }) }), item.subModules && item.subModules.length > 0 && (jsx(CollapsibleContent, { children: jsx(SidebarMenuSub, { className: workspace
|
|
1912
2040
|
? "mx-0 mt-1 mb-3 ml-[21px] gap-0.5 border-l border-sidebar-border pl-2.5 [&_a]:h-auto [&_a]:gap-[11px] [&_a]:px-3 [&_a]:py-2 [&_a]:text-xs [&_svg]:!size-[17px]"
|
|
1913
|
-
: undefined, children: item.subModules.map(subItem => {
|
|
2041
|
+
: undefined, children: item.subModules.map((subItem, subIndex) => {
|
|
1914
2042
|
const subItemUrl = buildUrlWithCode(code, subItem.url);
|
|
1915
|
-
const isSubItemActive =
|
|
2043
|
+
const isSubItemActive = activeSub === subIndex;
|
|
1916
2044
|
const SubIcon = subItem.icon;
|
|
1917
2045
|
const approvalCount = approvalCounts && subItem.approvalCountKey
|
|
1918
2046
|
? (approvalCounts[subItem.approvalCountKey] ?? 0)
|
|
@@ -1920,10 +2048,10 @@ function NavMain({ items, orgCode, closeOnNavigation = false, approvalCounts, ch
|
|
|
1920
2048
|
if (subItem.isCollapsible &&
|
|
1921
2049
|
subItem.subPages &&
|
|
1922
2050
|
subItem.subPages.length > 0) {
|
|
1923
|
-
|
|
1924
|
-
return (jsx(Collapsible, { asChild: true, defaultOpen: isSubItemActive || hasActiveSubPage, className: "group/sub-collapsible", children: jsxs(SidebarMenuSubItem, { children: [jsx(CollapsibleTrigger, { asChild: true, children: jsxs(SidebarMenuSubButton, { className: "w-full", isActive: isSubItemActive || hasActiveSubPage, children: [SubIcon && (jsx(SubIcon, { className: "sidebar-submodule-icon flex-shrink-0" })), jsx("span", { className: "flex-1 truncate", title: subItem.titleTh, children: subItem.titleTh }), jsx(ChevronDown, { className: "ml-auto size-4 transition-transform duration-200 group-data-[state=open]/sub-collapsible:rotate-180" })] }) }), jsx(CollapsibleContent, { children: jsx(SidebarMenuSub, { className: "ml-4", children: subItem.subPages.map(subPage => {
|
|
2051
|
+
return (jsx(Collapsible, { asChild: true, defaultOpen: isSubItemActive || openSub === subIndex, className: "group/sub-collapsible", children: jsxs(SidebarMenuSubItem, { children: [jsx(CollapsibleTrigger, { asChild: true, children: jsxs(SidebarMenuSubButton, { className: "w-full", isActive: isSubItemActive, children: [SubIcon && (jsx(SubIcon, { className: "sidebar-submodule-icon flex-shrink-0" })), jsx("span", { className: "flex-1 truncate", title: subItem.titleTh, children: subItem.titleTh }), jsx(ChevronDown, { className: "ml-auto size-4 transition-transform duration-200 group-data-[state=open]/sub-collapsible:rotate-180" })] }) }), jsx(CollapsibleContent, { children: jsx(SidebarMenuSub, { className: "ml-4", children: subItem.subPages.map((subPage, subPageIndex) => {
|
|
1925
2052
|
const subPageUrl = buildUrlWithCode(code, subPage.url);
|
|
1926
|
-
const isSubPageActive =
|
|
2053
|
+
const isSubPageActive = isSubItemActive &&
|
|
2054
|
+
activeSubPage === subPageIndex;
|
|
1927
2055
|
const SubPageIcon = subPage.icon;
|
|
1928
2056
|
return (jsx(SidebarMenuSubItem, { children: jsx(SidebarMenuSubButton, { asChild: true, className: `font-thai w-full text-sm ${isSubPageActive
|
|
1929
2057
|
? "text-sidebar-accent-foreground font-medium"
|
|
@@ -3376,6 +3504,24 @@ function PermissionProvider({ children, usePermission, fallbackRulesMap, }) {
|
|
|
3376
3504
|
return jsx(Fragment$1, { children: children });
|
|
3377
3505
|
}
|
|
3378
3506
|
|
|
3507
|
+
// Helper function to check if pathname matches a URL pattern
|
|
3508
|
+
// This prevents false matches like /reports/sales matching /sales
|
|
3509
|
+
// Handles paths with or without [code] prefix
|
|
3510
|
+
function isPathActive(pathname, url) {
|
|
3511
|
+
// Exact match
|
|
3512
|
+
if (pathname === url) {
|
|
3513
|
+
return true;
|
|
3514
|
+
}
|
|
3515
|
+
// Check if pathname starts with url + "/" to ensure exact segment matching
|
|
3516
|
+
// This prevents /reports/sales from matching /sales
|
|
3517
|
+
// Example: /org123/reports/sales should NOT match /org123/sales
|
|
3518
|
+
// But /org123/sales/overview SHOULD match /org123/sales
|
|
3519
|
+
if (pathname.startsWith(url + "/")) {
|
|
3520
|
+
return true;
|
|
3521
|
+
}
|
|
3522
|
+
return false;
|
|
3523
|
+
}
|
|
3524
|
+
|
|
3379
3525
|
/** Cookie name for the cross-subdomain theme choice. */
|
|
3380
3526
|
const THEME_COOKIE = "erp-theme";
|
|
3381
3527
|
/**
|
|
@@ -3621,5 +3767,5 @@ const getAxiosErrorMessage = (error) => {
|
|
|
3621
3767
|
return String(error);
|
|
3622
3768
|
};
|
|
3623
3769
|
|
|
3624
|
-
export { AccessDenied, ApiError, AppBreadcrumb, AppLayoutShell, AppSidebar, EMPLOYEE_RECORD_OK_EVENT, EMPLOYEE_RECORD_REQUIRED, EMPLOYEE_RECORD_REQUIRED_EVENT, EmployeeRequiredGate, FeatureFlagsProvider, HeaderBackground, HelpButton, HelpDialogHost, NavMain, NavTools, NavUser, OWNER_CHIP, OrganizationSwitcher, PLAN_FEATURES, PermissionGateShell, PermissionProvider, PermissionRulesProvider, ReactQueryProvider, SHELL_VERSION, SessionBackedProfileCacheSeeder, SessionRefreshTrigger, SidebarContent, SidebarFooter, SidebarHeader, SidebarHelpCard, SidebarHelpLink, SidebarProvider, SidebarTrigger, TapMenu, ThemeProvider, WORKSPACE_HEADER_VARS, WORKSPACE_LIGHT_HEADER_VARS, WorkspaceBrand, instance as axiosInstance, buildSystemLinks, buildUrlWithCode, canAccessNavPermissionRoute, clearAccessTokenCache, clearOrganizationId, closeHelp, decodeJWT, describeAttached, describeBrowser, detectHelpApp, filterNavigationModulesByPermission, filterNavigationToolsByPermission, getAccessToken, getActiveModule, getActiveSubModule, getAxiosErrorMessage, getOrganizationId, getQueryClientConfig, getRequiredChildGroups, hasPlanFeature, isLikelyTransientNetworkError, isPathActive, myRolesQueryKey, normalizePathnameForPermissionRules, openHelp, parseMyRoles, resolveUiRoutePermission, roleSummary, runGlobalRequestPrecheck, runWithTransientRetry, setOrganizationId, toApiError, unwrapApiResponse, updateOrganizationId, useClearOrgOnUserChange, useFeatureFlag, useFeatureFlags, useInvalidateQueriesOnOrgChange, useKeycloakRoles, useMyRoles, useOrganizationIdState, usePermissionRules, useSessionRefresh, useSidebar, useSidebarToggle };
|
|
3770
|
+
export { AccessDenied, ApiError, AppBreadcrumb, AppLayoutShell, AppSidebar, EMPLOYEE_RECORD_OK_EVENT, EMPLOYEE_RECORD_REQUIRED, EMPLOYEE_RECORD_REQUIRED_EVENT, EmployeeRequiredGate, FeatureFlagsProvider, HeaderBackground, HelpButton, HelpDialogHost, NavMain, NavTools, NavUser, OWNER_CHIP, OrganizationSwitcher, PLAN_FEATURES, PermissionGateShell, PermissionProvider, PermissionRulesProvider, ReactQueryProvider, SHELL_VERSION, SessionBackedProfileCacheSeeder, SessionRefreshTrigger, SidebarContent, SidebarFooter, SidebarHeader, SidebarHelpCard, SidebarHelpLink, SidebarProvider, SidebarTrigger, TapMenu, ThemeProvider, WORKSPACE_HEADER_VARS, WORKSPACE_LIGHT_HEADER_VARS, WorkspaceBrand, instance as axiosInstance, buildSystemLinks, buildUrlWithCode, canAccessNavPermissionRoute, clearAccessTokenCache, clearOrganizationId, closeHelp, decodeJWT, describeAttached, describeBrowser, detectHelpApp, filterNavigationModulesByPermission, filterNavigationToolsByPermission, getAccessToken, getActiveModule, getActiveSubModule, getAxiosErrorMessage, getOrganizationId, getQueryClientConfig, getRequiredChildGroups, hasPlanFeature, isLikelyTransientNetworkError, isPathActive, myRolesQueryKey, normalizePathnameForPermissionRules, openHelp, parseMyRoles, resolveActiveNav, resolveSidebarNav, resolveUiRoutePermission, roleSummary, runGlobalRequestPrecheck, runWithTransientRetry, setOrganizationId, toApiError, unwrapApiResponse, updateOrganizationId, useClearOrgOnUserChange, useFeatureFlag, useFeatureFlags, useInvalidateQueriesOnOrgChange, useKeycloakRoles, useMyRoles, useOrganizationIdState, usePermissionRules, useSessionRefresh, useSidebar, useSidebarToggle };
|
|
3625
3771
|
//# sourceMappingURL=index.esm.js.map
|