@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/dist/index.js
CHANGED
|
@@ -722,43 +722,79 @@ function filterNavigationModulesByPermission(modules, rulesMap, userChildSegment
|
|
|
722
722
|
})
|
|
723
723
|
.filter((m) => m !== null);
|
|
724
724
|
}
|
|
725
|
+
/** The path below the org code: "/O1/project/budgets" → "/project/budgets". */
|
|
726
|
+
function pathBelowCode(pathname) {
|
|
727
|
+
const segments = pathname.split("?")[0].split("/").filter(Boolean);
|
|
728
|
+
return segments.length > 1 ? "/" + segments.slice(1).join("/") : "/";
|
|
729
|
+
}
|
|
725
730
|
/**
|
|
726
|
-
*
|
|
727
|
-
*
|
|
731
|
+
* Whether `url` (relative, no org code) covers `path`: the same page, or a
|
|
732
|
+
* page below it on a segment boundary. "/" covers only the home itself and a
|
|
733
|
+
* query-only URL covers nothing.
|
|
728
734
|
*/
|
|
729
|
-
function
|
|
730
|
-
|
|
731
|
-
|
|
735
|
+
function coversPath(url, path) {
|
|
736
|
+
if (!url || url.includes("?") || /^https?:\/\//i.test(url))
|
|
737
|
+
return false;
|
|
738
|
+
if (url === "/")
|
|
739
|
+
return path === "/";
|
|
740
|
+
const clean = url.replace(/\/+$/, "");
|
|
741
|
+
return path === clean || path.startsWith(clean + "/");
|
|
742
|
+
}
|
|
743
|
+
/**
|
|
744
|
+
* The module and page whose URL is the longest one covering the path, over
|
|
745
|
+
* every module — a module's own URL, its pages and their sub-pages. Equal
|
|
746
|
+
* lengths go to the first in menu order.
|
|
747
|
+
*/
|
|
748
|
+
function longestMatch(path, modules) {
|
|
749
|
+
let best = null;
|
|
750
|
+
const consider = (module, sub, url) => {
|
|
751
|
+
if (!coversPath(url, path))
|
|
752
|
+
return;
|
|
753
|
+
if (!best || url.length > best.length) {
|
|
754
|
+
best = { module, sub, length: url.length };
|
|
755
|
+
}
|
|
756
|
+
};
|
|
732
757
|
for (const navModule of modules) {
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
for (const subModule of navModule.subModules) {
|
|
738
|
-
if (pathWithoutCode.startsWith(subModule.url) ||
|
|
739
|
-
pathname.includes(subModule.url)) {
|
|
740
|
-
return navModule.id;
|
|
741
|
-
}
|
|
742
|
-
}
|
|
758
|
+
for (const sub of navModule.subModules ?? []) {
|
|
759
|
+
consider(navModule, sub, sub.url);
|
|
760
|
+
for (const page of sub.subPages ?? [])
|
|
761
|
+
consider(navModule, sub, page.url);
|
|
743
762
|
}
|
|
763
|
+
// After its pages: on a tie the page is the more specific answer.
|
|
764
|
+
consider(navModule, null, navModule.url);
|
|
744
765
|
}
|
|
745
|
-
|
|
766
|
+
const found = best;
|
|
767
|
+
return found ? { module: found.module, sub: found.sub } : null;
|
|
768
|
+
}
|
|
769
|
+
/**
|
|
770
|
+
* Find the active top-level module id from `pathname`: the module whose URL
|
|
771
|
+
* (or one of whose pages' URLs) is the longest prefix of the path below the
|
|
772
|
+
* org code.
|
|
773
|
+
*
|
|
774
|
+
* Before 0.1.57 this took the first module with a page URL anywhere in the
|
|
775
|
+
* path (`includes`), so "/project" claimed /project/schedule and the sidebar,
|
|
776
|
+
* the trail and the tabs could disagree about where the user was.
|
|
777
|
+
*/
|
|
778
|
+
function getActiveModule(pathname, modules) {
|
|
779
|
+
return longestMatch(pathBelowCode(pathname), modules)?.module.id ?? null;
|
|
746
780
|
}
|
|
747
|
-
/**
|
|
781
|
+
/**
|
|
782
|
+
* Find the active sub-module id from `pathname`: the page whose own URL — or
|
|
783
|
+
* one of whose listed `subPages` — is the path itself (below the org code,
|
|
784
|
+
* ignoring a trailing slash). A record, a create or an edit page below a page
|
|
785
|
+
* is no page of the menu, so it has no active sub-module (0.1.58); the module
|
|
786
|
+
* (`getActiveModule`) still says where it belongs.
|
|
787
|
+
*
|
|
788
|
+
* Equal URLs in two modules go to the active module's page.
|
|
789
|
+
*/
|
|
748
790
|
function getActiveSubModule(pathname, modules) {
|
|
749
|
-
const
|
|
750
|
-
const
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
return subModule.id;
|
|
757
|
-
}
|
|
758
|
-
}
|
|
759
|
-
}
|
|
760
|
-
}
|
|
761
|
-
return null;
|
|
791
|
+
const path = strip(pathBelowCode(pathname));
|
|
792
|
+
const isPage = (url) => isMenuPath(url) && strip(url) === path;
|
|
793
|
+
const pagesOf = (m) => (m.subModules ?? []).filter(sub => isPage(sub.url) || (sub.subPages ?? []).some(p => isPage(p.url)));
|
|
794
|
+
const moduleId = getActiveModule(pathname, modules);
|
|
795
|
+
const own = modules.find(m => m.id === moduleId);
|
|
796
|
+
const hit = (own ? pagesOf(own)[0] : undefined) ?? modules.flatMap(pagesOf)[0];
|
|
797
|
+
return hit?.id ?? null;
|
|
762
798
|
}
|
|
763
799
|
/** Prepend `code` (org code) to a relative URL so `/sales` → `/O25.../sales`.
|
|
764
800
|
* Absolute `http(s)://` URLs are left intact; optional `{code}` placeholder is replaced.
|
|
@@ -780,6 +816,140 @@ function buildUrlWithCode(code, url) {
|
|
|
780
816
|
const base = cleanPath ? `/${code}/${cleanPath}` : `/${code}`;
|
|
781
817
|
return `${base}${query}${hash}`;
|
|
782
818
|
}
|
|
819
|
+
const strip = (p) => (p.length > 1 ? p.replace(/\/+$/, "") : p);
|
|
820
|
+
/** A URL the path can be compared with: not query-only, not another site. */
|
|
821
|
+
const isMenuPath = (url) => !!url && !url.includes("?") && !/^https?:\/\//i.test(url);
|
|
822
|
+
/** Same check as `isPathActive`: the page itself or one below it. */
|
|
823
|
+
const pathIsAtOrBelow = (pathname, url) => pathname === url || pathname.startsWith(url + "/");
|
|
824
|
+
/** "/O1/" and "/O1" are the same page; a query string never is. */
|
|
825
|
+
function samePage(pathname, url) {
|
|
826
|
+
if (url.includes("?"))
|
|
827
|
+
return false;
|
|
828
|
+
return strip(pathname.split("?")[0]) === strip(url);
|
|
829
|
+
}
|
|
830
|
+
/**
|
|
831
|
+
* How long `url` is when it is the page `pathname` shows (ignoring a trailing
|
|
832
|
+
* slash), or -1. Compared with and without the org code, as the menu URLs
|
|
833
|
+
* are written either way.
|
|
834
|
+
*/
|
|
835
|
+
function pageLength(pathname, url, code) {
|
|
836
|
+
if (!isMenuPath(url))
|
|
837
|
+
return -1;
|
|
838
|
+
const built = buildUrlWithCode(code, url);
|
|
839
|
+
if (samePage(pathname, built))
|
|
840
|
+
return strip(built).length;
|
|
841
|
+
if (samePage(pathname, url))
|
|
842
|
+
return strip(url).length;
|
|
843
|
+
return -1;
|
|
844
|
+
}
|
|
845
|
+
/**
|
|
846
|
+
* How long the URL that makes `url` cover `pathname` is — the page itself or
|
|
847
|
+
* one below it — or -1. The organisation's home ("/" → "/O1") covers only
|
|
848
|
+
* itself: every page sits below it, and a dashboard is not the parent of
|
|
849
|
+
* every page.
|
|
850
|
+
*/
|
|
851
|
+
function coverLength(pathname, url, code) {
|
|
852
|
+
if (!isMenuPath(url))
|
|
853
|
+
return -1;
|
|
854
|
+
const path = strip(pathname.split("?")[0]);
|
|
855
|
+
const built = strip(buildUrlWithCode(code, url));
|
|
856
|
+
const isRoot = url === "/" || (!!code && built === `/${code}`);
|
|
857
|
+
if (isRoot)
|
|
858
|
+
return pageLength(path, url, code);
|
|
859
|
+
if (pathIsAtOrBelow(path, built))
|
|
860
|
+
return built.length;
|
|
861
|
+
const raw = strip(url);
|
|
862
|
+
if (pathIsAtOrBelow(path, raw))
|
|
863
|
+
return raw.length;
|
|
864
|
+
return -1;
|
|
865
|
+
}
|
|
866
|
+
/**
|
|
867
|
+
* Where `pathname` sits in the sidebar: the entry to light and the group to
|
|
868
|
+
* open.
|
|
869
|
+
*
|
|
870
|
+
* Lit (0.1.58): only the entry whose own page this is — its URL or one of its
|
|
871
|
+
* `subPages`, exactly. A record, a create or an edit page below a menu page
|
|
872
|
+
* (Plannex /project/detail/<id>/schedule under "โครงการ" /project) lights
|
|
873
|
+
* nothing; an app lists such a page in `subPages` when it should keep its
|
|
874
|
+
* entry lit. At most one entry across every group (0.1.57).
|
|
875
|
+
*
|
|
876
|
+
* Open: the lit entry's group, or else the group with the deepest URL the
|
|
877
|
+
* path sits below.
|
|
878
|
+
*
|
|
879
|
+
* Equal lengths go to the group the app marks active (`item.isActive`, from
|
|
880
|
+
* its `activeModule`), then to a page over a group row, then to the first in
|
|
881
|
+
* menu order.
|
|
882
|
+
*/
|
|
883
|
+
function resolveSidebarNav(items, pathname, { code, highlightActiveModule = false } = {}) {
|
|
884
|
+
const path = (pathname ?? "").split("?")[0];
|
|
885
|
+
// The app named a one-line module as the current one: that is the answer.
|
|
886
|
+
if (highlightActiveModule) {
|
|
887
|
+
const leaf = items.findIndex(item => !item.subModules?.length && !!item.isActive);
|
|
888
|
+
if (leaf >= 0) {
|
|
889
|
+
return {
|
|
890
|
+
active: { item: leaf, sub: null, subPage: null },
|
|
891
|
+
open: { item: leaf, sub: null },
|
|
892
|
+
};
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
const better = (c, best) => {
|
|
896
|
+
if (!best)
|
|
897
|
+
return true;
|
|
898
|
+
if (c.length !== best.length)
|
|
899
|
+
return c.length > best.length;
|
|
900
|
+
const cActive = !!items[c.item].isActive;
|
|
901
|
+
const bActive = !!items[best.item].isActive;
|
|
902
|
+
if (cActive !== bActive)
|
|
903
|
+
return cActive;
|
|
904
|
+
// The same sub-item by its own URL and by a listed page: the page.
|
|
905
|
+
if (c.item === best.item && c.sub === best.sub) {
|
|
906
|
+
return c.subPage !== null && best.subPage === null;
|
|
907
|
+
}
|
|
908
|
+
return c.sub !== null && best.sub === null;
|
|
909
|
+
};
|
|
910
|
+
let exact = null;
|
|
911
|
+
let within = null;
|
|
912
|
+
const consider = (c, isExact) => {
|
|
913
|
+
if (c.length < 0)
|
|
914
|
+
return;
|
|
915
|
+
if (isExact) {
|
|
916
|
+
if (better(c, exact))
|
|
917
|
+
exact = c;
|
|
918
|
+
}
|
|
919
|
+
else if (better(c, within)) {
|
|
920
|
+
within = c;
|
|
921
|
+
}
|
|
922
|
+
};
|
|
923
|
+
items.forEach((item, i) => {
|
|
924
|
+
const subs = item.subModules ?? [];
|
|
925
|
+
const row = { item: i, sub: null, subPage: null };
|
|
926
|
+
// The row itself: only on its own URL. A one-line item under
|
|
927
|
+
// highlightActiveModule is lit by isActive alone (handled above).
|
|
928
|
+
if (!(highlightActiveModule && subs.length === 0)) {
|
|
929
|
+
consider({ ...row, length: pageLength(path, item.url, code) }, true);
|
|
930
|
+
}
|
|
931
|
+
consider({ ...row, length: coverLength(path, item.url, code) }, false);
|
|
932
|
+
subs.forEach((sub, j) => {
|
|
933
|
+
const entry = { item: i, sub: j };
|
|
934
|
+
consider({ ...entry, subPage: null, length: pageLength(path, sub.url, code) }, true);
|
|
935
|
+
consider({ ...entry, subPage: null, length: coverLength(path, sub.url, code) }, false);
|
|
936
|
+
(sub.subPages ?? []).forEach((page, k) => {
|
|
937
|
+
consider({ ...entry, subPage: k, length: pageLength(path, page.url, code) }, true);
|
|
938
|
+
consider({ ...entry, subPage: k, length: coverLength(path, page.url, code) }, false);
|
|
939
|
+
});
|
|
940
|
+
});
|
|
941
|
+
});
|
|
942
|
+
const lit = exact;
|
|
943
|
+
const holder = lit ?? within;
|
|
944
|
+
return {
|
|
945
|
+
active: lit ? { item: lit.item, sub: lit.sub, subPage: lit.subPage } : null,
|
|
946
|
+
open: holder ? { item: holder.item, sub: holder.sub } : null,
|
|
947
|
+
};
|
|
948
|
+
}
|
|
949
|
+
/** The entry lit for `pathname` — `resolveSidebarNav(...).active`. */
|
|
950
|
+
function resolveActiveNav(items, pathname, options = {}) {
|
|
951
|
+
return resolveSidebarNav(items, pathname, options).active;
|
|
952
|
+
}
|
|
783
953
|
|
|
784
954
|
function NavTools({ tools, closeOnNavigation = false }) {
|
|
785
955
|
const pathname = navigation.usePathname();
|
|
@@ -1265,7 +1435,7 @@ function platformApiBase(app) {
|
|
|
1265
1435
|
* This package's version, attached to help messages so the back office can
|
|
1266
1436
|
* tell which shell a report came from. Keep in step with package.json.
|
|
1267
1437
|
*/
|
|
1268
|
-
const SHELL_VERSION = "0.1.
|
|
1438
|
+
const SHELL_VERSION = "0.1.58";
|
|
1269
1439
|
|
|
1270
1440
|
const HELP_APP_LABEL = {
|
|
1271
1441
|
platform: "แพลตฟอร์ม",
|
|
@@ -1813,31 +1983,6 @@ function buildSystemLinks({ orgCode, activeApp, features, }) {
|
|
|
1813
1983
|
}));
|
|
1814
1984
|
}
|
|
1815
1985
|
|
|
1816
|
-
// Helper function to check if pathname matches a URL pattern
|
|
1817
|
-
// This prevents false matches like /reports/sales matching /sales
|
|
1818
|
-
// Handles paths with or without [code] prefix
|
|
1819
|
-
function isPathActive(pathname, url) {
|
|
1820
|
-
// Exact match
|
|
1821
|
-
if (pathname === url) {
|
|
1822
|
-
return true;
|
|
1823
|
-
}
|
|
1824
|
-
// Check if pathname starts with url + "/" to ensure exact segment matching
|
|
1825
|
-
// This prevents /reports/sales from matching /sales
|
|
1826
|
-
// Example: /org123/reports/sales should NOT match /org123/sales
|
|
1827
|
-
// But /org123/sales/overview SHOULD match /org123/sales
|
|
1828
|
-
if (pathname.startsWith(url + "/")) {
|
|
1829
|
-
return true;
|
|
1830
|
-
}
|
|
1831
|
-
return false;
|
|
1832
|
-
}
|
|
1833
|
-
|
|
1834
|
-
/** "/O1/" and "/O1" are the same page; a query string never is. */
|
|
1835
|
-
function samePage(pathname, url) {
|
|
1836
|
-
if (url.includes("?"))
|
|
1837
|
-
return false;
|
|
1838
|
-
const strip = (p) => (p.length > 1 ? p.replace(/\/+$/, "") : p);
|
|
1839
|
-
return strip(pathname) === strip(url);
|
|
1840
|
-
}
|
|
1841
1986
|
function NavMain({ items, orgCode, closeOnNavigation = false, approvalCounts, chrome = "brand", highlightActiveModule = false, }) {
|
|
1842
1987
|
const workspace = chrome === "workspace";
|
|
1843
1988
|
const params = navigation.useParams();
|
|
@@ -1848,63 +1993,46 @@ function NavMain({ items, orgCode, closeOnNavigation = false, approvalCounts, ch
|
|
|
1848
1993
|
setOpen(false);
|
|
1849
1994
|
};
|
|
1850
1995
|
const code = orgCode ?? params?.code;
|
|
1851
|
-
//
|
|
1852
|
-
|
|
1996
|
+
// The one entry lit for this page — only on that entry's own page, never
|
|
1997
|
+
// on a record or a form below it — and the group holding the page, which
|
|
1998
|
+
// opens either way.
|
|
1999
|
+
const { active, open } = resolveSidebarNav(items, pathname, {
|
|
2000
|
+
code,
|
|
2001
|
+
highlightActiveModule,
|
|
2002
|
+
});
|
|
2003
|
+
const states = items.map((item, i) => {
|
|
1853
2004
|
const itemUrl = buildUrlWithCode(code, item.url);
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
return best;
|
|
1863
|
-
const candidateUrl = subItemUrl;
|
|
1864
|
-
if (!best)
|
|
1865
|
-
return candidateUrl;
|
|
1866
|
-
return candidateUrl.length > best.length ? candidateUrl : best;
|
|
1867
|
-
}, null) ?? null;
|
|
1868
|
-
const hasActiveSubItem = !!activeSubItemUrl ||
|
|
1869
|
-
!!item.subModules?.some(subItem => subItem.subPages?.some(subPage => isPathActive(pathname, buildUrlWithCode(code, subPage.url)) ||
|
|
1870
|
-
isPathActive(pathname, subPage.url)));
|
|
1871
|
-
// Parent only active when path matches its URL exactly AND no child
|
|
1872
|
-
// is active (so parent+child don't both light up). When the app opts
|
|
1873
|
-
// into highlightActiveModule, leaf items trust `item.isActive` alone —
|
|
1874
|
-
// needed for query-only hub actions (`/?start=1`) that share a path
|
|
1875
|
-
// with another leaf (e.g. "แอปของฉัน" at `/`).
|
|
1876
|
-
const isItemActive = highlightActiveModule && !item.subModules?.length
|
|
1877
|
-
? !!item.isActive
|
|
1878
|
-
: (workspace ? samePage(pathname, itemUrl) : pathname === itemUrl) &&
|
|
1879
|
-
!hasActiveSubItem;
|
|
1880
|
-
return { itemUrl, activeSubItemUrl, hasActiveSubItem, isItemActive };
|
|
2005
|
+
const mine = active?.item === i ? active : null;
|
|
2006
|
+
return {
|
|
2007
|
+
itemUrl,
|
|
2008
|
+
activeSub: mine?.sub ?? null,
|
|
2009
|
+
activeSubPage: mine?.subPage ?? null,
|
|
2010
|
+
openSub: open?.item === i ? open.sub : null,
|
|
2011
|
+
isItemActive: !!mine && mine.sub === null,
|
|
2012
|
+
};
|
|
1881
2013
|
});
|
|
1882
|
-
// A group opens when
|
|
2014
|
+
// A group opens when the page moves into it and otherwise stays as the
|
|
1883
2015
|
// user left it. defaultOpen alone decided once, on the first render — and
|
|
1884
2016
|
// on a direct load the sub-items arrive after permissions do, so the group
|
|
1885
2017
|
// holding the current page stayed shut.
|
|
1886
|
-
const
|
|
1887
|
-
|
|
1888
|
-
.map(item => item.id)
|
|
1889
|
-
.join(",");
|
|
1890
|
-
const [openIds, setOpenIds] = React__namespace.useState(() => new Set(activeKey ? activeKey.split(",") : []));
|
|
2018
|
+
const openId = open ? (items[open.item]?.id ?? "") : "";
|
|
2019
|
+
const [openIds, setOpenIds] = React__namespace.useState(() => new Set(openId ? [openId] : []));
|
|
1891
2020
|
React__namespace.useEffect(() => {
|
|
1892
|
-
if (!
|
|
2021
|
+
if (!openId)
|
|
1893
2022
|
return;
|
|
1894
2023
|
setOpenIds(prev => {
|
|
1895
|
-
|
|
1896
|
-
if (ids.every(id => prev.has(id)))
|
|
2024
|
+
if (prev.has(openId))
|
|
1897
2025
|
return prev;
|
|
1898
2026
|
const next = new Set(prev);
|
|
1899
|
-
|
|
2027
|
+
next.add(openId);
|
|
1900
2028
|
return next;
|
|
1901
2029
|
});
|
|
1902
|
-
}, [
|
|
1903
|
-
const setItemOpen = (id,
|
|
1904
|
-
if (prev.has(id) ===
|
|
2030
|
+
}, [openId]);
|
|
2031
|
+
const setItemOpen = (id, isOpen) => setOpenIds(prev => {
|
|
2032
|
+
if (prev.has(id) === isOpen)
|
|
1905
2033
|
return prev;
|
|
1906
2034
|
const next = new Set(prev);
|
|
1907
|
-
if (
|
|
2035
|
+
if (isOpen)
|
|
1908
2036
|
next.add(id);
|
|
1909
2037
|
else
|
|
1910
2038
|
next.delete(id);
|
|
@@ -1915,7 +2043,7 @@ function NavMain({ items, orgCode, closeOnNavigation = false, approvalCounts, ch
|
|
|
1915
2043
|
item.group &&
|
|
1916
2044
|
item.group !== items[index - 1]?.group ? (jsxRuntime.jsx("li", { className: "list-none px-3 pb-2.5 text-xs text-muted-foreground" +
|
|
1917
2045
|
(index > 0 ? " pt-5" : ""), children: item.group }, `group-${item.id}`)) : null;
|
|
1918
|
-
const { itemUrl,
|
|
2046
|
+
const { itemUrl, activeSub, activeSubPage, openSub, isItemActive } = states[index];
|
|
1919
2047
|
const IconComp = item.icon;
|
|
1920
2048
|
const workspaceButton = workspace
|
|
1921
2049
|
? "h-auto gap-3 rounded-[10px] px-3 py-2.5 text-xs " +
|
|
@@ -1925,13 +2053,13 @@ function NavMain({ items, orgCode, closeOnNavigation = false, approvalCounts, ch
|
|
|
1925
2053
|
: "";
|
|
1926
2054
|
const icon = workspace && IconComp ? (jsxRuntime.jsx(IconComp, { className: "size-[18px] shrink-0" })) : null;
|
|
1927
2055
|
const badge = workspace && item.badge !== undefined && item.badge !== "" ? (jsxRuntime.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;
|
|
1928
|
-
return (jsxRuntime.jsxs(React__namespace.Fragment, { children: [groupHeading, jsxRuntime.jsx(erpTheme.Collapsible, { asChild: true, open: openIds.has(item.id), onOpenChange:
|
|
2056
|
+
return (jsxRuntime.jsxs(React__namespace.Fragment, { children: [groupHeading, jsxRuntime.jsx(erpTheme.Collapsible, { asChild: true, open: openIds.has(item.id), onOpenChange: isOpen => setItemOpen(item.id, isOpen), className: "group/collapsible", children: jsxRuntime.jsxs(SidebarMenuItem, { children: [jsxRuntime.jsx(erpTheme.CollapsibleTrigger, { asChild: true, children: jsxRuntime.jsx(SidebarMenuButton, { asChild: !item.subModules || item.subModules.length === 0, isActive: isItemActive, className: "data-[state=open]:text-sidebar-accent-foreground " +
|
|
1929
2057
|
workspaceButton, children: item.subModules && item.subModules.length > 0 ? (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [icon, jsxRuntime.jsx("span", { id: item.id, children: item.titleTh }), jsxRuntime.jsx(lucideReact.ChevronRight, { className: "ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90" })] })) : (jsxRuntime.jsxs(Link, { href: itemUrl, prefetch: false, className: "flex items-center w-full min-w-0 flex-1 " +
|
|
1930
2058
|
(workspace ? "gap-3" : "gap-2"), onClick: handleNavLinkClick, "aria-current": isItemActive ? "page" : undefined, children: [icon, jsxRuntime.jsx("span", { id: item.id, className: "flex-1 truncate", title: item.titleTh, children: item.titleTh }), badge] })) }) }), item.subModules && item.subModules.length > 0 && (jsxRuntime.jsx(erpTheme.CollapsibleContent, { children: jsxRuntime.jsx(SidebarMenuSub, { className: workspace
|
|
1931
2059
|
? "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]"
|
|
1932
|
-
: undefined, children: item.subModules.map(subItem => {
|
|
2060
|
+
: undefined, children: item.subModules.map((subItem, subIndex) => {
|
|
1933
2061
|
const subItemUrl = buildUrlWithCode(code, subItem.url);
|
|
1934
|
-
const isSubItemActive =
|
|
2062
|
+
const isSubItemActive = activeSub === subIndex;
|
|
1935
2063
|
const SubIcon = subItem.icon;
|
|
1936
2064
|
const approvalCount = approvalCounts && subItem.approvalCountKey
|
|
1937
2065
|
? (approvalCounts[subItem.approvalCountKey] ?? 0)
|
|
@@ -1939,10 +2067,10 @@ function NavMain({ items, orgCode, closeOnNavigation = false, approvalCounts, ch
|
|
|
1939
2067
|
if (subItem.isCollapsible &&
|
|
1940
2068
|
subItem.subPages &&
|
|
1941
2069
|
subItem.subPages.length > 0) {
|
|
1942
|
-
|
|
1943
|
-
return (jsxRuntime.jsx(erpTheme.Collapsible, { asChild: true, defaultOpen: isSubItemActive || hasActiveSubPage, className: "group/sub-collapsible", children: jsxRuntime.jsxs(SidebarMenuSubItem, { children: [jsxRuntime.jsx(erpTheme.CollapsibleTrigger, { asChild: true, children: jsxRuntime.jsxs(SidebarMenuSubButton, { className: "w-full", isActive: isSubItemActive || hasActiveSubPage, children: [SubIcon && (jsxRuntime.jsx(SubIcon, { className: "sidebar-submodule-icon flex-shrink-0" })), jsxRuntime.jsx("span", { className: "flex-1 truncate", title: subItem.titleTh, children: subItem.titleTh }), jsxRuntime.jsx(lucideReact.ChevronDown, { className: "ml-auto size-4 transition-transform duration-200 group-data-[state=open]/sub-collapsible:rotate-180" })] }) }), jsxRuntime.jsx(erpTheme.CollapsibleContent, { children: jsxRuntime.jsx(SidebarMenuSub, { className: "ml-4", children: subItem.subPages.map(subPage => {
|
|
2070
|
+
return (jsxRuntime.jsx(erpTheme.Collapsible, { asChild: true, defaultOpen: isSubItemActive || openSub === subIndex, className: "group/sub-collapsible", children: jsxRuntime.jsxs(SidebarMenuSubItem, { children: [jsxRuntime.jsx(erpTheme.CollapsibleTrigger, { asChild: true, children: jsxRuntime.jsxs(SidebarMenuSubButton, { className: "w-full", isActive: isSubItemActive, children: [SubIcon && (jsxRuntime.jsx(SubIcon, { className: "sidebar-submodule-icon flex-shrink-0" })), jsxRuntime.jsx("span", { className: "flex-1 truncate", title: subItem.titleTh, children: subItem.titleTh }), jsxRuntime.jsx(lucideReact.ChevronDown, { className: "ml-auto size-4 transition-transform duration-200 group-data-[state=open]/sub-collapsible:rotate-180" })] }) }), jsxRuntime.jsx(erpTheme.CollapsibleContent, { children: jsxRuntime.jsx(SidebarMenuSub, { className: "ml-4", children: subItem.subPages.map((subPage, subPageIndex) => {
|
|
1944
2071
|
const subPageUrl = buildUrlWithCode(code, subPage.url);
|
|
1945
|
-
const isSubPageActive =
|
|
2072
|
+
const isSubPageActive = isSubItemActive &&
|
|
2073
|
+
activeSubPage === subPageIndex;
|
|
1946
2074
|
const SubPageIcon = subPage.icon;
|
|
1947
2075
|
return (jsxRuntime.jsx(SidebarMenuSubItem, { children: jsxRuntime.jsx(SidebarMenuSubButton, { asChild: true, className: `font-thai w-full text-sm ${isSubPageActive
|
|
1948
2076
|
? "text-sidebar-accent-foreground font-medium"
|
|
@@ -3395,6 +3523,24 @@ function PermissionProvider({ children, usePermission, fallbackRulesMap, }) {
|
|
|
3395
3523
|
return jsxRuntime.jsx(jsxRuntime.Fragment, { children: children });
|
|
3396
3524
|
}
|
|
3397
3525
|
|
|
3526
|
+
// Helper function to check if pathname matches a URL pattern
|
|
3527
|
+
// This prevents false matches like /reports/sales matching /sales
|
|
3528
|
+
// Handles paths with or without [code] prefix
|
|
3529
|
+
function isPathActive(pathname, url) {
|
|
3530
|
+
// Exact match
|
|
3531
|
+
if (pathname === url) {
|
|
3532
|
+
return true;
|
|
3533
|
+
}
|
|
3534
|
+
// Check if pathname starts with url + "/" to ensure exact segment matching
|
|
3535
|
+
// This prevents /reports/sales from matching /sales
|
|
3536
|
+
// Example: /org123/reports/sales should NOT match /org123/sales
|
|
3537
|
+
// But /org123/sales/overview SHOULD match /org123/sales
|
|
3538
|
+
if (pathname.startsWith(url + "/")) {
|
|
3539
|
+
return true;
|
|
3540
|
+
}
|
|
3541
|
+
return false;
|
|
3542
|
+
}
|
|
3543
|
+
|
|
3398
3544
|
/** Cookie name for the cross-subdomain theme choice. */
|
|
3399
3545
|
const THEME_COOKIE = "erp-theme";
|
|
3400
3546
|
/**
|
|
@@ -3709,6 +3855,8 @@ exports.myRolesQueryKey = myRolesQueryKey;
|
|
|
3709
3855
|
exports.normalizePathnameForPermissionRules = normalizePathnameForPermissionRules;
|
|
3710
3856
|
exports.openHelp = openHelp;
|
|
3711
3857
|
exports.parseMyRoles = parseMyRoles;
|
|
3858
|
+
exports.resolveActiveNav = resolveActiveNav;
|
|
3859
|
+
exports.resolveSidebarNav = resolveSidebarNav;
|
|
3712
3860
|
exports.resolveUiRoutePermission = resolveUiRoutePermission;
|
|
3713
3861
|
exports.roleSummary = roleSummary;
|
|
3714
3862
|
exports.runGlobalRequestPrecheck = runGlobalRequestPrecheck;
|