@eintrek/erp-shell 0.1.20 → 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.
@@ -11,8 +11,19 @@
11
11
  */
12
12
  export declare function useInvalidateQueriesOnOrgChange(organizationId: number | string | null | undefined): void;
13
13
  /**
14
- * When `userId` changes (after the initial value), call `clearOrg`. Use
15
- * inside the ERP provider so logging out / switching user resets the
16
- * selected org cleanly.
14
+ * Clear the selected organization when the signed-in user actually changes.
15
+ *
16
+ * Only a transition between two KNOWN users counts. An empty or absent id
17
+ * means "not resolved yet" — it is not a different person, and treating it as
18
+ * one is what made this fire on every cold load: hosts derive the id as
19
+ * `session?.user?.id ?? ""`, so the first effect run saw `""`, and the moment
20
+ * NextAuth resolved, `"" !== "abc"` looked like a user switch and wiped the
21
+ * org. Downstream that nulls organizationId, which re-keys every org-scoped
22
+ * query and remounts the page — visible as settings snapping back to their
23
+ * defaults.
24
+ *
25
+ * Remembering the last known id (rather than the last value seen) keeps the
26
+ * real case working: user A signs out — id goes empty, ignored — user B signs
27
+ * in, A !== B, and the org is cleared.
17
28
  */
18
29
  export declare function useClearOrgOnUserChange(userId: string | null | undefined, clearOrg: (id: null) => Promise<void> | void): void;
package/dist/index.esm.js CHANGED
@@ -367,18 +367,32 @@ function useInvalidateQueriesOnOrgChange(organizationId) {
367
367
  }, [organizationId, queryClient]);
368
368
  }
369
369
  /**
370
- * When `userId` changes (after the initial value), call `clearOrg`. Use
371
- * inside the ERP provider so logging out / switching user resets the
372
- * selected org cleanly.
370
+ * Clear the selected organization when the signed-in user actually changes.
371
+ *
372
+ * Only a transition between two KNOWN users counts. An empty or absent id
373
+ * means "not resolved yet" — it is not a different person, and treating it as
374
+ * one is what made this fire on every cold load: hosts derive the id as
375
+ * `session?.user?.id ?? ""`, so the first effect run saw `""`, and the moment
376
+ * NextAuth resolved, `"" !== "abc"` looked like a user switch and wiped the
377
+ * org. Downstream that nulls organizationId, which re-keys every org-scoped
378
+ * query and remounts the page — visible as settings snapping back to their
379
+ * defaults.
380
+ *
381
+ * Remembering the last known id (rather than the last value seen) keeps the
382
+ * real case working: user A signs out — id goes empty, ignored — user B signs
383
+ * in, A !== B, and the org is cleared.
373
384
  */
374
385
  function useClearOrgOnUserChange(userId, clearOrg) {
375
- const previousUserIdRef = useRef(null);
386
+ const lastKnownUserIdRef = useRef(null);
376
387
  useEffect(() => {
377
- if (previousUserIdRef.current !== null &&
378
- previousUserIdRef.current !== userId) {
388
+ const next = userId ?? "";
389
+ if (next === "")
390
+ return;
391
+ const previous = lastKnownUserIdRef.current;
392
+ lastKnownUserIdRef.current = next;
393
+ if (previous !== null && previous !== next) {
379
394
  Promise.resolve(clearOrg(null)).catch(console.error);
380
395
  }
381
- previousUserIdRef.current = userId ?? null;
382
396
  }, [userId, clearOrg]);
383
397
  }
384
398
 
@@ -1027,6 +1041,25 @@ function AppLayoutShell({ children, sidebar, loadingComponent, breadcrumb, permi
1027
1041
  code,
1028
1042
  organizations.length,
1029
1043
  ]);
1044
+ // Key for the content subtree. It exists so switching organizations throws
1045
+ // away the previous org's component state — but it must react ONLY to a real
1046
+ // switch. organizationId legitimately passes through null while the context
1047
+ // re-settles (session resolving, cookie write in flight), and keying on that
1048
+ // unmounted and remounted the entire page: forms lost their loaded values,
1049
+ // in-flight edits vanished, and settings pickers snapped back to defaults.
1050
+ //
1051
+ // Holding the last known id makes a transient null a no-op while a genuine
1052
+ // org-to-org switch still changes the key.
1053
+ const lastOrgKeyRef = React.useRef("no-org");
1054
+ const currentOrgKey = organizationId === null ||
1055
+ organizationId === undefined ||
1056
+ String(organizationId).trim() === ""
1057
+ ? null
1058
+ : String(organizationId);
1059
+ if (currentOrgKey !== null) {
1060
+ lastOrgKeyRef.current = currentOrgKey;
1061
+ }
1062
+ const contentOrgKey = lastOrgKeyRef.current;
1030
1063
  // ≤1024px: sidebar renders as a modal overlay (backdrop dims content). >1024px:
1031
1064
  // sidebar is docked and the content gets a left margin to clear it.
1032
1065
  const [isSidebarModalLayout, setIsSidebarModalLayout] = React.useState(false);
@@ -1073,7 +1106,7 @@ function AppLayoutShell({ children, sidebar, loadingComponent, breadcrumb, permi
1073
1106
  const contentMargin = open && !isSidebarModalLayout ? "ml-[21.5rem]" : "";
1074
1107
  const needsLeftGutter = !(open && !isSidebarModalLayout);
1075
1108
  const wrap = permissionProvider ?? ((c) => c);
1076
- return (jsxs("div", { className: "flex min-h-screen w-full bg-background", children: [open && (jsx("div", { className: "fixed left-0 top-0 z-50 h-screen", children: sidebar })), open && isSidebarModalLayout && (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) })), jsx("div", { className: cx("relative flex min-w-0 flex-1 flex-col transition-all", contentMargin), children: jsxs("main", { className: "flex flex-1 flex-col min-h-screen gap-4", children: [jsxs("div", { className: "sticky top-0 z-30 shrink-0 w-full", children: [jsx(HeaderBackground, {}), jsxs("header", { className: "relative shrink-0 gap-2 py-4 h-auto min-h-[123.5px] w-full z-10", children: [jsxs("div", { className: "flex w-full items-center gap-2 px-3 sm:px-4 lg:px-6", children: [jsx(SidebarTrigger, { className: "shrink-0 text-white hover:bg-white/10" }), jsx("div", { className: "flex-1 min-w-0", children: breadcrumb })] }), jsx("div", { id: "header-title" })] })] }), jsx(Suspense, { fallback: jsx("div", { className: "relative z-10 min-h-[calc(100vh-8rem)]", children: jsx("div", { className: "flex h-full items-center justify-center", children: jsxs("div", { className: "space-y-4 text-center", children: [jsx("div", { className: "mx-auto h-8 w-8 animate-spin rounded-full border-b-2 border-primary" }), jsx("p", { className: "text-sm text-muted-foreground", children: "\u0E01\u0E33\u0E25\u0E31\u0E07\u0E42\u0E2B\u0E25\u0E14..." })] }) }) }), children: jsx("div", { className: cx("relative z-10 pr-6", needsLeftGutter && "pl-6"), children: wrap(children) }, `org-${organizationId ?? "no-org"}`) })] }) })] }));
1109
+ return (jsxs("div", { className: "flex min-h-screen w-full bg-background", children: [open && (jsx("div", { className: "fixed left-0 top-0 z-50 h-screen", children: sidebar })), open && isSidebarModalLayout && (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) })), jsx("div", { className: cx("relative flex min-w-0 flex-1 flex-col transition-all", contentMargin), children: jsxs("main", { className: "flex flex-1 flex-col min-h-screen gap-4", children: [jsxs("div", { className: "sticky top-0 z-30 shrink-0 w-full", children: [jsx(HeaderBackground, {}), jsxs("header", { className: "relative shrink-0 gap-2 py-4 h-auto min-h-[123.5px] w-full z-10", children: [jsxs("div", { className: "flex w-full items-center gap-2 px-3 sm:px-4 lg:px-6", children: [jsx(SidebarTrigger, { className: "shrink-0 text-white hover:bg-white/10" }), jsx("div", { className: "flex-1 min-w-0", children: breadcrumb })] }), jsx("div", { id: "header-title" })] })] }), jsx(Suspense, { fallback: jsx("div", { className: "relative z-10 min-h-[calc(100vh-8rem)]", children: jsx("div", { className: "flex h-full items-center justify-center", children: jsxs("div", { className: "space-y-4 text-center", children: [jsx("div", { className: "mx-auto h-8 w-8 animate-spin rounded-full border-b-2 border-primary" }), jsx("p", { className: "text-sm text-muted-foreground", children: "\u0E01\u0E33\u0E25\u0E31\u0E07\u0E42\u0E2B\u0E25\u0E14..." })] }) }) }), children: jsx("div", { className: cx("relative z-10 pr-6", needsLeftGutter && "pl-6"), children: wrap(children) }, `org-${contentOrgKey}`) })] }) })] }));
1077
1110
  }
1078
1111
  /** Local tiny classnames helper — avoids pulling clsx/tailwind-merge into
1079
1112
  * the shell just for the layout file. Falsy values are skipped. */