@eintrek/erp-shell 0.1.19 → 0.1.21
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/dist/hooks/use-org-side-effects.d.ts +14 -3
- package/dist/index.esm.js +54 -10
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +54 -10
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -386,18 +386,32 @@ function useInvalidateQueriesOnOrgChange(organizationId) {
|
|
|
386
386
|
}, [organizationId, queryClient]);
|
|
387
387
|
}
|
|
388
388
|
/**
|
|
389
|
-
*
|
|
390
|
-
*
|
|
391
|
-
*
|
|
389
|
+
* Clear the selected organization when the signed-in user actually changes.
|
|
390
|
+
*
|
|
391
|
+
* Only a transition between two KNOWN users counts. An empty or absent id
|
|
392
|
+
* means "not resolved yet" — it is not a different person, and treating it as
|
|
393
|
+
* one is what made this fire on every cold load: hosts derive the id as
|
|
394
|
+
* `session?.user?.id ?? ""`, so the first effect run saw `""`, and the moment
|
|
395
|
+
* NextAuth resolved, `"" !== "abc"` looked like a user switch and wiped the
|
|
396
|
+
* org. Downstream that nulls organizationId, which re-keys every org-scoped
|
|
397
|
+
* query and remounts the page — visible as settings snapping back to their
|
|
398
|
+
* defaults.
|
|
399
|
+
*
|
|
400
|
+
* Remembering the last known id (rather than the last value seen) keeps the
|
|
401
|
+
* real case working: user A signs out — id goes empty, ignored — user B signs
|
|
402
|
+
* in, A !== B, and the org is cleared.
|
|
392
403
|
*/
|
|
393
404
|
function useClearOrgOnUserChange(userId, clearOrg) {
|
|
394
|
-
const
|
|
405
|
+
const lastKnownUserIdRef = React.useRef(null);
|
|
395
406
|
React.useEffect(() => {
|
|
396
|
-
|
|
397
|
-
|
|
407
|
+
const next = userId ?? "";
|
|
408
|
+
if (next === "")
|
|
409
|
+
return;
|
|
410
|
+
const previous = lastKnownUserIdRef.current;
|
|
411
|
+
lastKnownUserIdRef.current = next;
|
|
412
|
+
if (previous !== null && previous !== next) {
|
|
398
413
|
Promise.resolve(clearOrg(null)).catch(console.error);
|
|
399
414
|
}
|
|
400
|
-
previousUserIdRef.current = userId ?? null;
|
|
401
415
|
}, [userId, clearOrg]);
|
|
402
416
|
}
|
|
403
417
|
|
|
@@ -1046,6 +1060,25 @@ function AppLayoutShell({ children, sidebar, loadingComponent, breadcrumb, permi
|
|
|
1046
1060
|
code,
|
|
1047
1061
|
organizations.length,
|
|
1048
1062
|
]);
|
|
1063
|
+
// Key for the content subtree. It exists so switching organizations throws
|
|
1064
|
+
// away the previous org's component state — but it must react ONLY to a real
|
|
1065
|
+
// switch. organizationId legitimately passes through null while the context
|
|
1066
|
+
// re-settles (session resolving, cookie write in flight), and keying on that
|
|
1067
|
+
// unmounted and remounted the entire page: forms lost their loaded values,
|
|
1068
|
+
// in-flight edits vanished, and settings pickers snapped back to defaults.
|
|
1069
|
+
//
|
|
1070
|
+
// Holding the last known id makes a transient null a no-op while a genuine
|
|
1071
|
+
// org-to-org switch still changes the key.
|
|
1072
|
+
const lastOrgKeyRef = React__namespace.useRef("no-org");
|
|
1073
|
+
const currentOrgKey = organizationId === null ||
|
|
1074
|
+
organizationId === undefined ||
|
|
1075
|
+
String(organizationId).trim() === ""
|
|
1076
|
+
? null
|
|
1077
|
+
: String(organizationId);
|
|
1078
|
+
if (currentOrgKey !== null) {
|
|
1079
|
+
lastOrgKeyRef.current = currentOrgKey;
|
|
1080
|
+
}
|
|
1081
|
+
const contentOrgKey = lastOrgKeyRef.current;
|
|
1049
1082
|
// ≤1024px: sidebar renders as a modal overlay (backdrop dims content). >1024px:
|
|
1050
1083
|
// sidebar is docked and the content gets a left margin to clear it.
|
|
1051
1084
|
const [isSidebarModalLayout, setIsSidebarModalLayout] = React__namespace.useState(false);
|
|
@@ -1092,7 +1125,7 @@ function AppLayoutShell({ children, sidebar, loadingComponent, breadcrumb, permi
|
|
|
1092
1125
|
const contentMargin = open && !isSidebarModalLayout ? "ml-[21.5rem]" : "";
|
|
1093
1126
|
const needsLeftGutter = !(open && !isSidebarModalLayout);
|
|
1094
1127
|
const wrap = permissionProvider ?? ((c) => c);
|
|
1095
|
-
return (jsxRuntime.jsxs("div", { className: "flex min-h-screen w-full bg-background", children: [open && (jsxRuntime.jsx("div", { className: "fixed left-0 top-0 z-50 h-screen", children: sidebar })), open && isSidebarModalLayout && (jsxRuntime.jsx("button", { type: "button", "aria-label": "\u0E1B\u0E34\u0E14\u0E40\u0E21\u0E19\u0E39", className: "fixed inset-0 z-40 bg-black/50", onClick: () => setOpen(false) })), jsxRuntime.jsx("div", { className: cx("relative flex min-w-0 flex-1 flex-col transition-all", contentMargin), children: jsxRuntime.jsxs("main", { className: "flex flex-1 flex-col min-h-screen gap-4", children: [jsxRuntime.jsxs("div", { className: "sticky top-0 z-30 shrink-0 w-full", children: [jsxRuntime.jsx(HeaderBackground, {}), jsxRuntime.jsxs("header", { className: "relative shrink-0 gap-2 py-4 h-auto min-h-[123.5px] w-full z-10", children: [jsxRuntime.jsxs("div", { className: "flex w-full items-center gap-2 px-3 sm:px-4 lg:px-6", children: [jsxRuntime.jsx(SidebarTrigger, { className: "shrink-0 text-white hover:bg-white/10" }), jsxRuntime.jsx("div", { className: "flex-1 min-w-0", children: breadcrumb })] }), jsxRuntime.jsx("div", { id: "header-title" })] })] }), jsxRuntime.jsx(React.Suspense, { fallback: jsxRuntime.jsx("div", { className: "relative z-10 min-h-[calc(100vh-8rem)]", children: jsxRuntime.jsx("div", { className: "flex h-full items-center justify-center", children: jsxRuntime.jsxs("div", { className: "space-y-4 text-center", children: [jsxRuntime.jsx("div", { className: "mx-auto h-8 w-8 animate-spin rounded-full border-b-2 border-primary" }), jsxRuntime.jsx("p", { className: "text-sm text-muted-foreground", children: "\u0E01\u0E33\u0E25\u0E31\u0E07\u0E42\u0E2B\u0E25\u0E14..." })] }) }) }), children: jsxRuntime.jsx("div", { className: cx("relative z-10 pr-6", needsLeftGutter && "pl-6"), children: wrap(children) }, `org-${
|
|
1128
|
+
return (jsxRuntime.jsxs("div", { className: "flex min-h-screen w-full bg-background", children: [open && (jsxRuntime.jsx("div", { className: "fixed left-0 top-0 z-50 h-screen", children: sidebar })), open && isSidebarModalLayout && (jsxRuntime.jsx("button", { type: "button", "aria-label": "\u0E1B\u0E34\u0E14\u0E40\u0E21\u0E19\u0E39", className: "fixed inset-0 z-40 bg-black/50", onClick: () => setOpen(false) })), jsxRuntime.jsx("div", { className: cx("relative flex min-w-0 flex-1 flex-col transition-all", contentMargin), children: jsxRuntime.jsxs("main", { className: "flex flex-1 flex-col min-h-screen gap-4", children: [jsxRuntime.jsxs("div", { className: "sticky top-0 z-30 shrink-0 w-full", children: [jsxRuntime.jsx(HeaderBackground, {}), jsxRuntime.jsxs("header", { className: "relative shrink-0 gap-2 py-4 h-auto min-h-[123.5px] w-full z-10", children: [jsxRuntime.jsxs("div", { className: "flex w-full items-center gap-2 px-3 sm:px-4 lg:px-6", children: [jsxRuntime.jsx(SidebarTrigger, { className: "shrink-0 text-white hover:bg-white/10" }), jsxRuntime.jsx("div", { className: "flex-1 min-w-0", children: breadcrumb })] }), jsxRuntime.jsx("div", { id: "header-title" })] })] }), jsxRuntime.jsx(React.Suspense, { fallback: jsxRuntime.jsx("div", { className: "relative z-10 min-h-[calc(100vh-8rem)]", children: jsxRuntime.jsx("div", { className: "flex h-full items-center justify-center", children: jsxRuntime.jsxs("div", { className: "space-y-4 text-center", children: [jsxRuntime.jsx("div", { className: "mx-auto h-8 w-8 animate-spin rounded-full border-b-2 border-primary" }), jsxRuntime.jsx("p", { className: "text-sm text-muted-foreground", children: "\u0E01\u0E33\u0E25\u0E31\u0E07\u0E42\u0E2B\u0E25\u0E14..." })] }) }) }), children: jsxRuntime.jsx("div", { className: cx("relative z-10 pr-6", needsLeftGutter && "pl-6"), children: wrap(children) }, `org-${contentOrgKey}`) })] }) })] }));
|
|
1096
1129
|
}
|
|
1097
1130
|
/** Local tiny classnames helper — avoids pulling clsx/tailwind-merge into
|
|
1098
1131
|
* the shell just for the layout file. Falsy values are skipped. */
|
|
@@ -1361,8 +1394,19 @@ const getQueryClientConfig = () => ({
|
|
|
1361
1394
|
refetchOnWindowFocus: true,
|
|
1362
1395
|
},
|
|
1363
1396
|
mutations: {
|
|
1364
|
-
retry
|
|
1365
|
-
|
|
1397
|
+
// NEVER auto-retry a mutation. Unlike a query, a mutation is a side
|
|
1398
|
+
// effect on a non-idempotent endpoint (submit, create, approve), and
|
|
1399
|
+
// React Query cannot tell "the network dropped" from "the server
|
|
1400
|
+
// rejected this on business grounds".
|
|
1401
|
+
//
|
|
1402
|
+
// With retry: 1 every failed mutation fired twice. Two visible
|
|
1403
|
+
// consequences: a rejected submit showed up as two failed POSTs in the
|
|
1404
|
+
// network tab, and — far worse — a create whose response was lost in
|
|
1405
|
+
// flight would be re-sent and duplicate the record server-side.
|
|
1406
|
+
//
|
|
1407
|
+
// Retrying is the caller's decision, not a global default: the user
|
|
1408
|
+
// sees the error and presses the button again.
|
|
1409
|
+
retry: 0,
|
|
1366
1410
|
},
|
|
1367
1411
|
},
|
|
1368
1412
|
queryCache: new reactQuery.QueryCache({
|