@igstack/app-catalog-frontend-core 0.6.0 → 0.6.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.
Files changed (24) hide show
  1. package/dist/esm/__tests__/integration/notFoundRoute.integration.test.d.ts +0 -0
  2. package/dist/esm/__tests__/modules/appCatalog/utils/mergeFrontendBuildId.test.d.ts +1 -0
  3. package/dist/esm/__tests__/ui/components/header/VersionDisplay.test.d.ts +0 -0
  4. package/dist/esm/modules/appCatalog/context/AppCatalogContext.js +5 -8
  5. package/dist/esm/modules/appCatalog/context/AppCatalogContext.js.map +1 -1
  6. package/dist/esm/modules/appCatalog/context/mergeFrontendBuildId.d.ts +15 -0
  7. package/dist/esm/modules/appCatalog/context/mergeFrontendBuildId.js +15 -0
  8. package/dist/esm/modules/appCatalog/context/mergeFrontendBuildId.js.map +1 -0
  9. package/dist/esm/modules/auth/AuthContext.js +1 -1
  10. package/dist/esm/modules/auth/AuthModalContext.js +1 -1
  11. package/dist/esm/routes/__root.js +8 -2
  12. package/dist/esm/routes/__root.js.map +1 -1
  13. package/dist/esm/ui/components/header/Header.d.ts +4 -0
  14. package/dist/esm/ui/components/header/Header.js +40 -17
  15. package/dist/esm/ui/components/header/Header.js.map +1 -1
  16. package/package.json +4 -3
  17. package/src/__tests__/integration/harness/given.tsx +8 -1
  18. package/src/__tests__/integration/notFoundRoute.integration.test.tsx +34 -0
  19. package/src/__tests__/modules/appCatalog/utils/mergeFrontendBuildId.test.ts +42 -0
  20. package/src/__tests__/ui/components/header/VersionDisplay.test.tsx +38 -0
  21. package/src/modules/appCatalog/context/AppCatalogContext.tsx +5 -11
  22. package/src/modules/appCatalog/context/mergeFrontendBuildId.ts +31 -0
  23. package/src/routes/__root.tsx +16 -2
  24. package/src/ui/components/header/Header.tsx +39 -16
@@ -15,6 +15,7 @@ import "@radix-ui/react-tooltip";
15
15
  import "../../../ui/empty.js";
16
16
  import "@tanstack/react-router";
17
17
  import "../../../ui/button.js";
18
+ import { mergeFrontendBuildId } from "./mergeFrontendBuildId.js";
18
19
  const AppCatalogContext = createContext(void 0);
19
20
  function AppCatalogProvider({ children }) {
20
21
  var _a, _b;
@@ -30,14 +31,10 @@ function AppCatalogProvider({ children }) {
30
31
  approvalMethods: (data == null ? void 0 : data.approvalMethods) ?? [],
31
32
  persons: (data == null ? void 0 : data.persons) ?? [],
32
33
  groups: (data == null ? void 0 : data.groups) ?? [],
33
- versions: {
34
- ...data == null ? void 0 : data.versions,
35
- ...uiSettings.frontendBuildId && {
36
- frontend: {
37
- displayName: uiSettings.frontendBuildId === "local" ? "local" : `#${uiSettings.frontendBuildId}`
38
- }
39
- }
40
- }
34
+ versions: mergeFrontendBuildId(
35
+ (data == null ? void 0 : data.versions) ?? {},
36
+ uiSettings.frontendBuildId
37
+ )
41
38
  }),
42
39
  [
43
40
  data == null ? void 0 : data.approvalMethods,
@@ -1 +1 @@
1
- {"version":3,"file":"AppCatalogContext.js","sources":["../../../../../src/modules/appCatalog/context/AppCatalogContext.tsx"],"sourcesContent":["import type {\n AppApprovalMethod,\n AppVersionInfo,\n Group,\n GroupingTagDefinition,\n Person,\n Resource,\n} from '@igstack/app-catalog-backend-core'\nimport { useQuery } from '@tanstack/react-query'\nimport type { ReactNode } from 'react'\nimport { createContext, use, useEffect, useMemo } from 'react'\nimport { ApiQueryMagazineAppCatalog } from '~/modules/appCatalog'\nimport { useUiSettings } from '~/context/UiSettingsContext'\n\nexport interface AppCatalogContextIface {\n resources: Resource[]\n isLoadingApps: boolean\n tagsDefinitions: GroupingTagDefinition[]\n approvalMethods: AppApprovalMethod[]\n persons: Person[]\n groups: Group[]\n versions?: AppVersionInfo\n}\n\nexport const AppCatalogContext = createContext<\n AppCatalogContextIface | undefined\n>(undefined)\n\ninterface AppCatalogProviderProps {\n children: ReactNode\n}\n\nexport function AppCatalogProvider({ children }: AppCatalogProviderProps) {\n const { data, isLoading: isLoadingApps } = useQuery(\n ApiQueryMagazineAppCatalog.getAppCatalog(),\n )\n const uiSettings = useUiSettings()\n\n const contextValue = useMemo<AppCatalogContextIface>(\n () => ({\n resources: data?.resources ?? [],\n isLoadingApps,\n tagsDefinitions: data?.tagsDefinitions ?? [],\n approvalMethods: data?.approvalMethods ?? [],\n persons: data?.persons ?? [],\n groups: data?.groups ?? [],\n versions: {\n ...data?.versions,\n ...(uiSettings.frontendBuildId && {\n frontend: {\n displayName:\n uiSettings.frontendBuildId === 'local'\n ? 'local'\n : `#${uiSettings.frontendBuildId}`,\n },\n }),\n },\n }),\n [\n data?.approvalMethods,\n data?.resources,\n data?.tagsDefinitions,\n data?.persons,\n data?.groups,\n data?.versions,\n uiSettings.frontendBuildId,\n isLoadingApps,\n ],\n )\n\n // Update document title based on backend version\n useEffect(() => {\n if (data?.versions?.backend?.displayName === 'local') {\n document.title = 'Local'\n } else {\n document.title = 'App Catalog'\n }\n }, [data?.versions?.backend?.displayName])\n\n return <AppCatalogContext value={contextValue}>{children}</AppCatalogContext>\n}\n\nexport function useAppCatalogContext(): AppCatalogContextIface {\n const context = use(AppCatalogContext)\n if (!context) {\n throw new Error(\n 'useAppCatalogContext must be used within AppCatalogProvider',\n )\n }\n return context\n}\n"],"names":["_b","_a"],"mappings":";;;;;;;;;;;;;;;;;AAwBO,MAAM,oBAAoB,cAE/B,MAAS;AAMJ,SAAS,mBAAmB,EAAE,YAAqC;;AACxE,QAAM,EAAE,MAAM,WAAW,cAAA,IAAkB;AAAA,IACzC,2BAA2B,cAAA;AAAA,EAAc;AAE3C,QAAM,aAAa,cAAA;AAEnB,QAAM,eAAe;AAAA,IACnB,OAAO;AAAA,MACL,YAAW,6BAAM,cAAa,CAAA;AAAA,MAC9B;AAAA,MACA,kBAAiB,6BAAM,oBAAmB,CAAA;AAAA,MAC1C,kBAAiB,6BAAM,oBAAmB,CAAA;AAAA,MAC1C,UAAS,6BAAM,YAAW,CAAA;AAAA,MAC1B,SAAQ,6BAAM,WAAU,CAAA;AAAA,MACxB,UAAU;AAAA,QACR,GAAG,6BAAM;AAAA,QACT,GAAI,WAAW,mBAAmB;AAAA,UAChC,UAAU;AAAA,YACR,aACE,WAAW,oBAAoB,UAC3B,UACA,IAAI,WAAW,eAAe;AAAA,UAAA;AAAA,QACtC;AAAA,MACF;AAAA,IACF;AAAA,IAEF;AAAA,MACE,6BAAM;AAAA,MACN,6BAAM;AAAA,MACN,6BAAM;AAAA,MACN,6BAAM;AAAA,MACN,6BAAM;AAAA,MACN,6BAAM;AAAA,MACN,WAAW;AAAA,MACX;AAAA,IAAA;AAAA,EACF;AAIF,YAAU,MAAM;;AACd,UAAIA,OAAAC,MAAA,6BAAM,aAAN,gBAAAA,IAAgB,YAAhB,gBAAAD,IAAyB,iBAAgB,SAAS;AACpD,eAAS,QAAQ;AAAA,IACnB,OAAO;AACL,eAAS,QAAQ;AAAA,IACnB;AAAA,EACF,GAAG,EAAC,wCAAM,aAAN,mBAAgB,YAAhB,mBAAyB,WAAW,CAAC;AAEzC,SAAO,oBAAC,mBAAA,EAAkB,OAAO,cAAe,SAAA,CAAS;AAC3D;AAEO,SAAS,uBAA+C;AAC7D,QAAM,UAAU,IAAI,iBAAiB;AACrC,MAAI,CAAC,SAAS;AACZ,UAAM,IAAI;AAAA,MACR;AAAA,IAAA;AAAA,EAEJ;AACA,SAAO;AACT;"}
1
+ {"version":3,"file":"AppCatalogContext.js","sources":["../../../../../src/modules/appCatalog/context/AppCatalogContext.tsx"],"sourcesContent":["import type {\n AppApprovalMethod,\n AppVersionInfo,\n Group,\n GroupingTagDefinition,\n Person,\n Resource,\n} from '@igstack/app-catalog-backend-core'\nimport { useQuery } from '@tanstack/react-query'\nimport type { ReactNode } from 'react'\nimport { createContext, use, useEffect, useMemo } from 'react'\nimport { ApiQueryMagazineAppCatalog } from '~/modules/appCatalog'\nimport { useUiSettings } from '~/context/UiSettingsContext'\nimport { mergeFrontendBuildId } from '~/modules/appCatalog/context/mergeFrontendBuildId'\n\nexport interface AppCatalogContextIface {\n resources: Resource[]\n isLoadingApps: boolean\n tagsDefinitions: GroupingTagDefinition[]\n approvalMethods: AppApprovalMethod[]\n persons: Person[]\n groups: Group[]\n versions?: AppVersionInfo\n}\n\nexport const AppCatalogContext = createContext<\n AppCatalogContextIface | undefined\n>(undefined)\n\ninterface AppCatalogProviderProps {\n children: ReactNode\n}\n\nexport function AppCatalogProvider({ children }: AppCatalogProviderProps) {\n const { data, isLoading: isLoadingApps } = useQuery(\n ApiQueryMagazineAppCatalog.getAppCatalog(),\n )\n const uiSettings = useUiSettings()\n\n const contextValue = useMemo<AppCatalogContextIface>(\n () => ({\n resources: data?.resources ?? [],\n isLoadingApps,\n tagsDefinitions: data?.tagsDefinitions ?? [],\n approvalMethods: data?.approvalMethods ?? [],\n persons: data?.persons ?? [],\n groups: data?.groups ?? [],\n versions: mergeFrontendBuildId(\n data?.versions ?? {},\n uiSettings.frontendBuildId,\n ),\n }),\n [\n data?.approvalMethods,\n data?.resources,\n data?.tagsDefinitions,\n data?.persons,\n data?.groups,\n data?.versions,\n uiSettings.frontendBuildId,\n isLoadingApps,\n ],\n )\n\n // Update document title based on backend version\n useEffect(() => {\n if (data?.versions?.backend?.displayName === 'local') {\n document.title = 'Local'\n } else {\n document.title = 'App Catalog'\n }\n }, [data?.versions?.backend?.displayName])\n\n return <AppCatalogContext value={contextValue}>{children}</AppCatalogContext>\n}\n\nexport function useAppCatalogContext(): AppCatalogContextIface {\n const context = use(AppCatalogContext)\n if (!context) {\n throw new Error(\n 'useAppCatalogContext must be used within AppCatalogProvider',\n )\n }\n return context\n}\n"],"names":["_b","_a"],"mappings":";;;;;;;;;;;;;;;;;;AAyBO,MAAM,oBAAoB,cAE/B,MAAS;AAMJ,SAAS,mBAAmB,EAAE,YAAqC;;AACxE,QAAM,EAAE,MAAM,WAAW,cAAA,IAAkB;AAAA,IACzC,2BAA2B,cAAA;AAAA,EAAc;AAE3C,QAAM,aAAa,cAAA;AAEnB,QAAM,eAAe;AAAA,IACnB,OAAO;AAAA,MACL,YAAW,6BAAM,cAAa,CAAA;AAAA,MAC9B;AAAA,MACA,kBAAiB,6BAAM,oBAAmB,CAAA;AAAA,MAC1C,kBAAiB,6BAAM,oBAAmB,CAAA;AAAA,MAC1C,UAAS,6BAAM,YAAW,CAAA;AAAA,MAC1B,SAAQ,6BAAM,WAAU,CAAA;AAAA,MACxB,UAAU;AAAA,SACR,6BAAM,aAAY,CAAA;AAAA,QAClB,WAAW;AAAA,MAAA;AAAA,IACb;AAAA,IAEF;AAAA,MACE,6BAAM;AAAA,MACN,6BAAM;AAAA,MACN,6BAAM;AAAA,MACN,6BAAM;AAAA,MACN,6BAAM;AAAA,MACN,6BAAM;AAAA,MACN,WAAW;AAAA,MACX;AAAA,IAAA;AAAA,EACF;AAIF,YAAU,MAAM;;AACd,UAAIA,OAAAC,MAAA,6BAAM,aAAN,gBAAAA,IAAgB,YAAhB,gBAAAD,IAAyB,iBAAgB,SAAS;AACpD,eAAS,QAAQ;AAAA,IACnB,OAAO;AACL,eAAS,QAAQ;AAAA,IACnB;AAAA,EACF,GAAG,EAAC,wCAAM,aAAN,mBAAgB,YAAhB,mBAAyB,WAAW,CAAC;AAEzC,SAAO,oBAAC,mBAAA,EAAkB,OAAO,cAAe,SAAA,CAAS;AAC3D;AAEO,SAAS,uBAA+C;AAC7D,QAAM,UAAU,IAAI,iBAAiB;AACrC,MAAI,CAAC,SAAS;AACZ,UAAM,IAAI;AAAA,MACR;AAAA,IAAA;AAAA,EAEJ;AACA,SAAO;AACT;"}
@@ -0,0 +1,15 @@
1
+ import { AppVersionInfo } from '@igstack/app-catalog-backend-core';
2
+ /**
3
+ * Merge the (natera-agnostic) frontend build id into the backend-provided
4
+ * version info's `frontend` slot.
5
+ *
6
+ * The build id is baked into the deployed JS bundle at build time (e.g. a CI
7
+ * pipeline id via a Vite env var). We PREPEND it to the resolved frontend-core
8
+ * npm version instead of replacing it, so the footer surfaces both:
9
+ * `#<buildId> · <frontend-core version> (<sha>)`.
10
+ *
11
+ * - No build id → return versions unchanged.
12
+ * - build id === 'local' → show `local` (local dev, no meaningful SHA/version).
13
+ * - otherwise → prepend `#<buildId> · ` and preserve the version + sha + shaUrl.
14
+ */
15
+ export declare function mergeFrontendBuildId(versions: AppVersionInfo, frontendBuildId: string | undefined): AppVersionInfo;
@@ -0,0 +1,15 @@
1
+ function mergeFrontendBuildId(versions, frontendBuildId) {
2
+ if (!frontendBuildId) return versions;
3
+ if (frontendBuildId === "local") {
4
+ return { ...versions, frontend: { displayName: "local" } };
5
+ }
6
+ const be = versions.frontend;
7
+ return {
8
+ ...versions,
9
+ frontend: be ? { ...be, displayName: `#${frontendBuildId} · ${be.displayName}` } : { displayName: `#${frontendBuildId}` }
10
+ };
11
+ }
12
+ export {
13
+ mergeFrontendBuildId
14
+ };
15
+ //# sourceMappingURL=mergeFrontendBuildId.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mergeFrontendBuildId.js","sources":["../../../../../src/modules/appCatalog/context/mergeFrontendBuildId.ts"],"sourcesContent":["import type { AppVersionInfo } from '@igstack/app-catalog-backend-core'\n\n/**\n * Merge the (natera-agnostic) frontend build id into the backend-provided\n * version info's `frontend` slot.\n *\n * The build id is baked into the deployed JS bundle at build time (e.g. a CI\n * pipeline id via a Vite env var). We PREPEND it to the resolved frontend-core\n * npm version instead of replacing it, so the footer surfaces both:\n * `#<buildId> · <frontend-core version> (<sha>)`.\n *\n * - No build id → return versions unchanged.\n * - build id === 'local' → show `local` (local dev, no meaningful SHA/version).\n * - otherwise → prepend `#<buildId> · ` and preserve the version + sha + shaUrl.\n */\nexport function mergeFrontendBuildId(\n versions: AppVersionInfo,\n frontendBuildId: string | undefined,\n): AppVersionInfo {\n if (!frontendBuildId) return versions\n if (frontendBuildId === 'local') {\n return { ...versions, frontend: { displayName: 'local' } }\n }\n const be = versions.frontend\n return {\n ...versions,\n frontend: be\n ? { ...be, displayName: `#${frontendBuildId} · ${be.displayName}` }\n : { displayName: `#${frontendBuildId}` },\n }\n}\n"],"names":[],"mappings":"AAeO,SAAS,qBACd,UACA,iBACgB;AAChB,MAAI,CAAC,gBAAiB,QAAO;AAC7B,MAAI,oBAAoB,SAAS;AAC/B,WAAO,EAAE,GAAG,UAAU,UAAU,EAAE,aAAa,UAAQ;AAAA,EACzD;AACA,QAAM,KAAK,SAAS;AACpB,SAAO;AAAA,IACL,GAAG;AAAA,IACH,UAAU,KACN,EAAE,GAAG,IAAI,aAAa,IAAI,eAAe,MAAM,GAAG,WAAW,OAC7D,EAAE,aAAa,IAAI,eAAe,GAAA;AAAA,EAAG;AAE7C;"}
@@ -1,5 +1,5 @@
1
1
  import { jsx } from "react/jsx-runtime";
2
- import { use, createContext, useState, useEffect, useMemo } from "react";
2
+ import { useState, useEffect, useMemo, createContext, use } from "react";
3
3
  const AuthContext = createContext(void 0);
4
4
  const STORAGE_KEY = "ac_auth_user";
5
5
  function AuthProvider({ children }) {
@@ -1,5 +1,5 @@
1
1
  import { jsx } from "react/jsx-runtime";
2
- import { use, createContext, useState, useMemo } from "react";
2
+ import { useState, useMemo, createContext, use } from "react";
3
3
  const AuthModalContext = createContext(
4
4
  void 0
5
5
  );
@@ -3,11 +3,17 @@ import { createRootRouteWithContext, Outlet } from "@tanstack/react-router";
3
3
  import { RootErrorPage } from "../ui/components/error/RootErrorPage.js";
4
4
  import { NotFoundError } from "../ui/error/NotFoundError.js";
5
5
  import { LoadingScreen } from "../ui/layout/LoadingScreen.js";
6
+ import { TopLevelProvidersForErrors } from "../ui/layout/TopLevelProvidersForErrors.js";
6
7
  const Route = createRootRouteWithContext()({
7
8
  component: RootRoute,
8
9
  errorComponent: RootErrorPage,
9
- pendingComponent: () => /* @__PURE__ */ jsx(LoadingScreen, { label: "root pending" }),
10
- notFoundComponent: () => /* @__PURE__ */ jsx(NotFoundError, {}),
10
+ // These fallbacks render MainLayout Header → useAuth(), so they must be
11
+ // wrapped in the providers (Auth + AuthModal + Theme + Tooltip). The router
12
+ // renders pending/notFound OUTSIDE the route-component subtree, i.e. outside
13
+ // the app's TopLevelProviders — without this wrap they throw
14
+ // "useAuth must be used within AuthProvider" (see #6/#7).
15
+ pendingComponent: () => /* @__PURE__ */ jsx(TopLevelProvidersForErrors, { children: /* @__PURE__ */ jsx(LoadingScreen, { label: "root pending" }) }),
16
+ notFoundComponent: () => /* @__PURE__ */ jsx(TopLevelProvidersForErrors, { children: /* @__PURE__ */ jsx(NotFoundError, {}) }),
11
17
  wrapInSuspense: true
12
18
  });
13
19
  function RootRoute() {
@@ -1 +1 @@
1
- {"version":3,"file":"__root.js","sources":["../../../src/routes/__root.tsx"],"sourcesContent":["import { Outlet, createRootRouteWithContext } from '@tanstack/react-router'\nimport { TanStackRouterDevtools } from '@tanstack/react-router-devtools'\nimport type { AcRouterContext } from '~/types/types'\nimport { RootErrorPage } from '~/ui/components/error/RootErrorPage'\nimport { NotFoundError } from '~/ui/error/NotFoundError'\nimport { LoadingScreen } from '~/ui/layout/LoadingScreen'\n\nexport const Route = createRootRouteWithContext<AcRouterContext>()({\n component: RootRoute,\n errorComponent: RootErrorPage,\n pendingComponent: () => <LoadingScreen label=\"root pending\" />,\n notFoundComponent: () => <NotFoundError />,\n wrapInSuspense: true,\n})\n\nfunction RootRoute() {\n return (\n <div className=\"min-h-screen bg-base-200\">\n <Outlet />\n {import.meta.env.MODE === 'dev' ? <TanStackRouterDevtools /> : null}\n </div>\n )\n}\n"],"names":[],"mappings":";;;;;AAOO,MAAM,QAAQ,6BAA8C;AAAA,EACjE,WAAW;AAAA,EACX,gBAAgB;AAAA,EAChB,kBAAkB,MAAM,oBAAC,eAAA,EAAc,OAAM,gBAAe;AAAA,EAC5D,mBAAmB,MAAM,oBAAC,eAAA,EAAc;AAAA,EACxC,gBAAgB;AAClB,CAAC;AAED,SAAS,YAAY;AACnB,SACE,qBAAC,OAAA,EAAI,WAAU,4BACb,UAAA;AAAA,IAAA,oBAAC,QAAA,EAAO;AAAA,IACuD;AAAA,EAAA,GACjE;AAEJ;"}
1
+ {"version":3,"file":"__root.js","sources":["../../../src/routes/__root.tsx"],"sourcesContent":["import { Outlet, createRootRouteWithContext } from '@tanstack/react-router'\nimport { TanStackRouterDevtools } from '@tanstack/react-router-devtools'\nimport type { AcRouterContext } from '~/types/types'\nimport { RootErrorPage } from '~/ui/components/error/RootErrorPage'\nimport { NotFoundError } from '~/ui/error/NotFoundError'\nimport { LoadingScreen } from '~/ui/layout/LoadingScreen'\nimport { TopLevelProvidersForErrors } from '~/ui/layout/TopLevelProvidersForErrors'\n\nexport const Route = createRootRouteWithContext<AcRouterContext>()({\n component: RootRoute,\n errorComponent: RootErrorPage,\n // These fallbacks render MainLayout → Header → useAuth(), so they must be\n // wrapped in the providers (Auth + AuthModal + Theme + Tooltip). The router\n // renders pending/notFound OUTSIDE the route-component subtree, i.e. outside\n // the app's TopLevelProviders — without this wrap they throw\n // \"useAuth must be used within AuthProvider\" (see #6/#7).\n pendingComponent: () => (\n <TopLevelProvidersForErrors>\n <LoadingScreen label=\"root pending\" />\n </TopLevelProvidersForErrors>\n ),\n notFoundComponent: () => (\n <TopLevelProvidersForErrors>\n <NotFoundError />\n </TopLevelProvidersForErrors>\n ),\n wrapInSuspense: true,\n})\n\nfunction RootRoute() {\n return (\n <div className=\"min-h-screen bg-base-200\">\n <Outlet />\n {import.meta.env.MODE === 'dev' ? <TanStackRouterDevtools /> : null}\n </div>\n )\n}\n"],"names":[],"mappings":";;;;;;AAQO,MAAM,QAAQ,6BAA8C;AAAA,EACjE,WAAW;AAAA,EACX,gBAAgB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAMhB,kBAAkB,MAChB,oBAAC,4BAAA,EACC,8BAAC,eAAA,EAAc,OAAM,eAAA,CAAe,GACtC;AAAA,EAEF,mBAAmB,MACjB,oBAAC,4BAAA,EACC,UAAA,oBAAC,iBAAc,GACjB;AAAA,EAEF,gBAAgB;AAClB,CAAC;AAED,SAAS,YAAY;AACnB,SACE,qBAAC,OAAA,EAAI,WAAU,4BACb,UAAA;AAAA,IAAA,oBAAC,QAAA,EAAO;AAAA,IACuD;AAAA,EAAA,GACjE;AAEJ;"}
@@ -1,4 +1,8 @@
1
+ import { AppVersionInfo } from '@igstack/app-catalog-backend-core';
1
2
  import { default as React } from 'react';
3
+ export declare function VersionDisplay({ versions }: {
4
+ versions: AppVersionInfo;
5
+ }): import("react/jsx-runtime").JSX.Element;
2
6
  export interface HeaderProps {
3
7
  middle?: React.ReactNode;
4
8
  }
@@ -1,4 +1,4 @@
1
- import { jsxs, jsx } from "react/jsx-runtime";
1
+ import { jsxs, jsx, Fragment } from "react/jsx-runtime";
2
2
  import { Link } from "@tanstack/react-router";
3
3
  import { useQuery } from "@tanstack/react-query";
4
4
  import { LogOut } from "lucide-react";
@@ -13,32 +13,54 @@ import { useAuthModal } from "../../../modules/auth/AuthModalContext.js";
13
13
  import { Button } from "../../button.js";
14
14
  import { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuSeparator, DropdownMenuItem } from "../../dropdown-menu.js";
15
15
  import { Skeleton } from "../../skeleton.js";
16
+ function ShaSuffix({ version }) {
17
+ if (!version.sha) return null;
18
+ if (version.shaUrl) {
19
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
20
+ " ",
21
+ /* @__PURE__ */ jsxs(
22
+ "a",
23
+ {
24
+ href: version.shaUrl,
25
+ target: "_blank",
26
+ rel: "noopener noreferrer",
27
+ className: "text-muted-foreground hover:text-primary",
28
+ onClick: (e) => e.stopPropagation(),
29
+ children: [
30
+ "(",
31
+ version.sha,
32
+ ")"
33
+ ]
34
+ }
35
+ )
36
+ ] });
37
+ }
38
+ return /* @__PURE__ */ jsxs("span", { className: "text-muted-foreground", children: [
39
+ " (",
40
+ version.sha,
41
+ ")"
42
+ ] });
43
+ }
16
44
  function VersionItem({
17
45
  label,
18
46
  version
19
47
  }) {
20
- if (version.url) {
21
- return /* @__PURE__ */ jsxs(
48
+ return /* @__PURE__ */ jsxs("span", { className: "text-xs text-muted-foreground", title: label, children: [
49
+ label,
50
+ ":",
51
+ " ",
52
+ version.url ? /* @__PURE__ */ jsx(
22
53
  "a",
23
54
  {
24
55
  href: version.url,
25
56
  target: "_blank",
26
57
  rel: "noopener noreferrer",
27
- className: "text-xs text-primary hover:text-primary/80 transition-colors font-medium",
58
+ className: "text-primary hover:text-primary/80 transition-colors font-medium",
28
59
  onClick: (e) => e.stopPropagation(),
29
- title: label,
30
- children: [
31
- label,
32
- ": ",
33
- version.displayName
34
- ]
60
+ children: version.displayName
35
61
  }
36
- );
37
- }
38
- return /* @__PURE__ */ jsxs("span", { className: "text-xs text-muted-foreground", title: label, children: [
39
- label,
40
- ": ",
41
- version.displayName
62
+ ) : version.displayName,
63
+ /* @__PURE__ */ jsx(ShaSuffix, { version })
42
64
  ] });
43
65
  }
44
66
  function VersionDisplay({ versions }) {
@@ -154,6 +176,7 @@ function Header({ middle }) {
154
176
  ] });
155
177
  }
156
178
  export {
157
- Header
179
+ Header,
180
+ VersionDisplay
158
181
  };
159
182
  //# sourceMappingURL=Header.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"Header.js","sources":["../../../../../src/ui/components/header/Header.tsx"],"sourcesContent":["import type {\n AppVersionInfo,\n VersionInfo,\n} from '@igstack/app-catalog-backend-core'\nimport { Link } from '@tanstack/react-router'\nimport { useQuery } from '@tanstack/react-query'\nimport { LogOut } from 'lucide-react'\nimport React, { use } from 'react'\nimport AppCatalogLogo from '~/assets/app-catalog.svg?react'\nimport { useTRPC } from '~/api/infra/trpc'\nimport { ThemeSwitcher } from '~/components/ThemeSwitcher'\nimport { AppCatalogContext } from '~/modules/appCatalog/context/AppCatalogContext'\nimport {\n useAuth,\n useAuthActions,\n useIsAuthenticated,\n useUser,\n} from '~/modules/auth'\nimport { useAuthModal } from '~/modules/auth/AuthModalContext'\nimport { Button } from '~/ui/button'\nimport {\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuSeparator,\n DropdownMenuTrigger,\n} from '~/ui/dropdown-menu'\nimport { Skeleton } from '~/ui/skeleton'\n\nfunction VersionItem({\n label,\n version,\n}: {\n label: string\n version: VersionInfo\n}) {\n if (version.url) {\n return (\n <a\n href={version.url}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"text-xs text-primary hover:text-primary/80 transition-colors font-medium\"\n onClick={(e) => e.stopPropagation()}\n title={label}\n >\n {label}: {version.displayName}\n </a>\n )\n }\n return (\n <span className=\"text-xs text-muted-foreground\" title={label}>\n {label}: {version.displayName}\n </span>\n )\n}\n\nfunction VersionDisplay({ versions }: { versions: AppVersionInfo }) {\n const isLocal =\n versions.backend?.displayName === 'local' && !versions.coreVersion\n\n if (isLocal) {\n return (\n <span className=\"text-xs text-muted-foreground\" title=\"Local\">\n local\n </span>\n )\n }\n\n return (\n <div className=\"flex flex-col gap-0.5\">\n {versions.backend && (\n <VersionItem label=\"Pipeline\" version={versions.backend} />\n )}\n {versions.coreVersion && (\n <VersionItem label=\"Core\" version={versions.coreVersion} />\n )}\n {versions.frontend && (\n <VersionItem label=\"FE\" version={versions.frontend} />\n )}\n </div>\n )\n}\n\nexport interface HeaderProps {\n middle?: React.ReactNode\n}\n\nexport function Header({ middle }: HeaderProps) {\n const { isLoading } = useAuth()\n const isAuthenticated = useIsAuthenticated()\n const user = useUser()\n const { logout, devLogin } = useAuthActions()\n const { open: openLoginModal } = useAuthModal()\n const appCatalogContextMaybe = use(AppCatalogContext)\n const trpc = useTRPC()\n const { data: providersData } = useQuery(\n trpc.auth.getProviders.queryOptions(),\n )\n\n const handleLogout = async () => {\n try {\n await logout()\n } catch (error) {\n console.error('Logout failed:', error)\n }\n }\n\n const handleLoginClick = () => {\n // Preserve the current URL for redirect after login\n const currentUrl = window.location.pathname + window.location.search\n openLoginModal(currentUrl)\n }\n\n return (\n <div className=\"flex items-center mb-4 pb-4 gap-4 border-b\">\n <div className=\"flex items-center gap-4\">\n <Link to=\"/\">\n <div className=\"flex items-center gap-2\">\n <AppCatalogLogo className=\"h-16 w-16\" />\n <div className=\"flex flex-col\">\n <span className=\"text-lg font-bold\">App Catalog</span>\n {appCatalogContextMaybe?.versions && (\n <VersionDisplay versions={appCatalogContextMaybe.versions} />\n )}\n </div>\n </div>\n </Link>\n </div>\n {middle && <div className=\"flex-1\">{middle}</div>}\n <div className=\"flex items-center gap-3 ml-auto\">\n <ThemeSwitcher />\n {isLoading ? (\n <Skeleton className=\"w-8 h-8 rounded-full\" />\n ) : !isAuthenticated ? (\n <div className=\"flex items-center gap-2\">\n {providersData?.devLoginEnabled && (\n <button\n type=\"button\"\n onClick={devLogin}\n className=\"text-xs font-medium px-2 py-1 rounded bg-amber-500/20 text-amber-600 dark:text-amber-400 hover:bg-amber-500/30 border border-amber-500/30 cursor-pointer\"\n >\n DEV Login\n </button>\n )}\n <button\n type=\"button\"\n onClick={handleLoginClick}\n className=\"text-sm font-medium text-muted-foreground hover:text-foreground cursor-pointer\"\n >\n Login\n </button>\n </div>\n ) : (\n <DropdownMenu>\n <DropdownMenuTrigger asChild>\n <Button\n variant=\"ghost\"\n size=\"sm\"\n className=\"w-8 h-8 rounded-full p-0 hover:bg-accent hover:text-accent-foreground cursor-pointer\"\n aria-label={`User menu for ${user?.name || 'User'}`}\n data-testid=\"user-avatar-button\"\n >\n {user?.image ? (\n <img\n src={user.image}\n alt={user.name || 'User'}\n className=\"w-full h-full rounded-full object-cover\"\n />\n ) : (\n <div className=\"flex items-center justify-center w-full h-full rounded-full bg-primary text-primary-foreground text-xs font-semibold\">\n {(user?.name || user?.email || '?').charAt(0).toUpperCase()}\n </div>\n )}\n </Button>\n </DropdownMenuTrigger>\n <DropdownMenuContent align=\"end\" className=\"w-56\">\n <div className=\"px-2 py-3 flex items-center gap-3\">\n {user?.image ? (\n <img\n src={user.image}\n alt={user.name || 'User'}\n className=\"w-10 h-10 rounded-full object-cover shrink-0\"\n />\n ) : (\n <div className=\"flex items-center justify-center w-10 h-10 rounded-full bg-primary text-primary-foreground text-sm font-semibold shrink-0\">\n {(user?.name || user?.email || '?').charAt(0).toUpperCase()}\n </div>\n )}\n <div className=\"flex flex-col min-w-0\">\n <p className=\"text-xs text-muted-foreground truncate\">\n @{user?.email?.split('@')[0] || 'user'}\n </p>\n <p className=\"text-sm font-medium truncate\">\n {user?.name || 'User'}\n </p>\n </div>\n </div>\n <DropdownMenuSeparator />\n <DropdownMenuItem onClick={handleLogout}>\n <LogOut className=\"h-4 w-4 mr-2\" />\n <span>Sign out</span>\n </DropdownMenuItem>\n </DropdownMenuContent>\n </DropdownMenu>\n )}\n </div>\n </div>\n )\n}\n"],"names":["AppCatalogLogo"],"mappings":"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"}
1
+ {"version":3,"file":"Header.js","sources":["../../../../../src/ui/components/header/Header.tsx"],"sourcesContent":["import type {\n AppVersionInfo,\n VersionInfo,\n} from '@igstack/app-catalog-backend-core'\nimport { Link } from '@tanstack/react-router'\nimport { useQuery } from '@tanstack/react-query'\nimport { LogOut } from 'lucide-react'\nimport React, { use } from 'react'\nimport AppCatalogLogo from '~/assets/app-catalog.svg?react'\nimport { useTRPC } from '~/api/infra/trpc'\nimport { ThemeSwitcher } from '~/components/ThemeSwitcher'\nimport { AppCatalogContext } from '~/modules/appCatalog/context/AppCatalogContext'\nimport {\n useAuth,\n useAuthActions,\n useIsAuthenticated,\n useUser,\n} from '~/modules/auth'\nimport { useAuthModal } from '~/modules/auth/AuthModalContext'\nimport { Button } from '~/ui/button'\nimport {\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuSeparator,\n DropdownMenuTrigger,\n} from '~/ui/dropdown-menu'\nimport { Skeleton } from '~/ui/skeleton'\n\nfunction ShaSuffix({ version }: { version: VersionInfo }) {\n if (!version.sha) return null\n if (version.shaUrl) {\n return (\n <>\n {' '}\n <a\n href={version.shaUrl}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"text-muted-foreground hover:text-primary\"\n onClick={(e) => e.stopPropagation()}\n >\n ({version.sha})\n </a>\n </>\n )\n }\n return <span className=\"text-muted-foreground\"> ({version.sha})</span>\n}\n\nfunction VersionItem({\n label,\n version,\n}: {\n label: string\n version: VersionInfo\n}) {\n // Outer element is a <span> (not <a>) so the optional SHA link can be a\n // sibling anchor without producing invalid nested <a> markup.\n return (\n <span className=\"text-xs text-muted-foreground\" title={label}>\n {label}:{' '}\n {version.url ? (\n <a\n href={version.url}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"text-primary hover:text-primary/80 transition-colors font-medium\"\n onClick={(e) => e.stopPropagation()}\n >\n {version.displayName}\n </a>\n ) : (\n version.displayName\n )}\n <ShaSuffix version={version} />\n </span>\n )\n}\n\nexport function VersionDisplay({ versions }: { versions: AppVersionInfo }) {\n const isLocal =\n versions.backend?.displayName === 'local' && !versions.coreVersion\n\n if (isLocal) {\n return (\n <span className=\"text-xs text-muted-foreground\" title=\"Local\">\n local\n </span>\n )\n }\n\n return (\n <div className=\"flex flex-col gap-0.5\">\n {versions.backend && (\n <VersionItem label=\"Pipeline\" version={versions.backend} />\n )}\n {versions.coreVersion && (\n <VersionItem label=\"Core\" version={versions.coreVersion} />\n )}\n {versions.frontend && (\n <VersionItem label=\"FE\" version={versions.frontend} />\n )}\n </div>\n )\n}\n\nexport interface HeaderProps {\n middle?: React.ReactNode\n}\n\nexport function Header({ middle }: HeaderProps) {\n const { isLoading } = useAuth()\n const isAuthenticated = useIsAuthenticated()\n const user = useUser()\n const { logout, devLogin } = useAuthActions()\n const { open: openLoginModal } = useAuthModal()\n const appCatalogContextMaybe = use(AppCatalogContext)\n const trpc = useTRPC()\n const { data: providersData } = useQuery(\n trpc.auth.getProviders.queryOptions(),\n )\n\n const handleLogout = async () => {\n try {\n await logout()\n } catch (error) {\n console.error('Logout failed:', error)\n }\n }\n\n const handleLoginClick = () => {\n // Preserve the current URL for redirect after login\n const currentUrl = window.location.pathname + window.location.search\n openLoginModal(currentUrl)\n }\n\n return (\n <div className=\"flex items-center mb-4 pb-4 gap-4 border-b\">\n <div className=\"flex items-center gap-4\">\n <Link to=\"/\">\n <div className=\"flex items-center gap-2\">\n <AppCatalogLogo className=\"h-16 w-16\" />\n <div className=\"flex flex-col\">\n <span className=\"text-lg font-bold\">App Catalog</span>\n {appCatalogContextMaybe?.versions && (\n <VersionDisplay versions={appCatalogContextMaybe.versions} />\n )}\n </div>\n </div>\n </Link>\n </div>\n {middle && <div className=\"flex-1\">{middle}</div>}\n <div className=\"flex items-center gap-3 ml-auto\">\n <ThemeSwitcher />\n {isLoading ? (\n <Skeleton className=\"w-8 h-8 rounded-full\" />\n ) : !isAuthenticated ? (\n <div className=\"flex items-center gap-2\">\n {providersData?.devLoginEnabled && (\n <button\n type=\"button\"\n onClick={devLogin}\n className=\"text-xs font-medium px-2 py-1 rounded bg-amber-500/20 text-amber-600 dark:text-amber-400 hover:bg-amber-500/30 border border-amber-500/30 cursor-pointer\"\n >\n DEV Login\n </button>\n )}\n <button\n type=\"button\"\n onClick={handleLoginClick}\n className=\"text-sm font-medium text-muted-foreground hover:text-foreground cursor-pointer\"\n >\n Login\n </button>\n </div>\n ) : (\n <DropdownMenu>\n <DropdownMenuTrigger asChild>\n <Button\n variant=\"ghost\"\n size=\"sm\"\n className=\"w-8 h-8 rounded-full p-0 hover:bg-accent hover:text-accent-foreground cursor-pointer\"\n aria-label={`User menu for ${user?.name || 'User'}`}\n data-testid=\"user-avatar-button\"\n >\n {user?.image ? (\n <img\n src={user.image}\n alt={user.name || 'User'}\n className=\"w-full h-full rounded-full object-cover\"\n />\n ) : (\n <div className=\"flex items-center justify-center w-full h-full rounded-full bg-primary text-primary-foreground text-xs font-semibold\">\n {(user?.name || user?.email || '?').charAt(0).toUpperCase()}\n </div>\n )}\n </Button>\n </DropdownMenuTrigger>\n <DropdownMenuContent align=\"end\" className=\"w-56\">\n <div className=\"px-2 py-3 flex items-center gap-3\">\n {user?.image ? (\n <img\n src={user.image}\n alt={user.name || 'User'}\n className=\"w-10 h-10 rounded-full object-cover shrink-0\"\n />\n ) : (\n <div className=\"flex items-center justify-center w-10 h-10 rounded-full bg-primary text-primary-foreground text-sm font-semibold shrink-0\">\n {(user?.name || user?.email || '?').charAt(0).toUpperCase()}\n </div>\n )}\n <div className=\"flex flex-col min-w-0\">\n <p className=\"text-xs text-muted-foreground truncate\">\n @{user?.email?.split('@')[0] || 'user'}\n </p>\n <p className=\"text-sm font-medium truncate\">\n {user?.name || 'User'}\n </p>\n </div>\n </div>\n <DropdownMenuSeparator />\n <DropdownMenuItem onClick={handleLogout}>\n <LogOut className=\"h-4 w-4 mr-2\" />\n <span>Sign out</span>\n </DropdownMenuItem>\n </DropdownMenuContent>\n </DropdownMenu>\n )}\n </div>\n </div>\n )\n}\n"],"names":["AppCatalogLogo"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@igstack/app-catalog-frontend-core",
3
- "version": "0.6.0",
3
+ "version": "0.6.2",
4
4
  "description": "Frontend core library for App Catalog",
5
5
  "homepage": "https://github.com/lislon/app-catalog",
6
6
  "repository": {
@@ -134,8 +134,8 @@
134
134
  "vite-plugin-static-copy": "^3.1.4",
135
135
  "vite-plugin-svgr": "^4.2.0",
136
136
  "vitest": "^4.1.2",
137
- "@igstack/app-catalog-backend-core": "0.6.0",
138
- "@igstack/app-catalog-shared-core": "0.6.0"
137
+ "@igstack/app-catalog-backend-core": "0.6.2",
138
+ "@igstack/app-catalog-shared-core": "0.6.2"
139
139
  },
140
140
  "peerDependencies": {
141
141
  "react": "19.1.2",
@@ -144,6 +144,7 @@
144
144
  "vite": "^6.3.5",
145
145
  "vite-plugin-svgr": "^4.2.0"
146
146
  },
147
+ "gitHead": "dcc2b0acf435e115ec42861ec79064c6394b1ada",
147
148
  "scripts": {
148
149
  "build": "vite build",
149
150
  "build:lenient": "vite build --mode lenient",
@@ -174,7 +174,14 @@ export async function given(
174
174
  const hasTanstackError = document.body.textContent.includes(
175
175
  'Something went wrong',
176
176
  )
177
- if (!hasAlert && !hasTable && !hasSearchbox && !hasTanstackError) {
177
+ const hasNotFound = document.body.textContent.includes('404 Not Found')
178
+ if (
179
+ !hasAlert &&
180
+ !hasTable &&
181
+ !hasSearchbox &&
182
+ !hasTanstackError &&
183
+ !hasNotFound
184
+ ) {
178
185
  throw new Error('Still loading...')
179
186
  }
180
187
  },
@@ -0,0 +1,34 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { screen, waitFor } from '@testing-library/react'
3
+ import '@testing-library/jest-dom/vitest'
4
+
5
+ import { given } from './harness/given'
6
+ import { magazine } from './mock-backend/magazines'
7
+
8
+ /**
9
+ * Regression for #7 (root cause in #6): the root route's `notFoundComponent`
10
+ * (`NotFoundError`) renders `MainLayout → Header → useAuth()`. Before the fix
11
+ * the fallback rendered OUTSIDE `AuthProvider`, so any unknown URL threw
12
+ * `useAuth must be used within AuthProvider` and the app showed its "Ooops!"
13
+ * error page instead of a clean 404.
14
+ */
15
+ describe('Unknown URL renders NotFound without a useAuth error', () => {
16
+ it('shows the 404 page (not the useAuth error) for a bogus URL', async () => {
17
+ const { ui } = await given(magazine.full(), {
18
+ initialRoute: '/nonexistent-xyz',
19
+ })
20
+
21
+ // The clean NotFound UI is rendered.
22
+ await waitFor(() => {
23
+ expect(screen.getByText('404 Not Found')).toBeInTheDocument()
24
+ })
25
+
26
+ // And crucially: NO useAuth error surfaced. The header (which calls
27
+ // useAuth) must render inside a provider, so the global error page must
28
+ // not appear at all.
29
+ expect(
30
+ screen.queryByText(/useAuth must be used within AuthProvider/),
31
+ ).not.toBeInTheDocument()
32
+ expect(() => ui.globalError()).toThrow(/No global error found/)
33
+ })
34
+ })
@@ -0,0 +1,42 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { mergeFrontendBuildId } from '~/modules/appCatalog/context/mergeFrontendBuildId'
3
+
4
+ describe('mergeFrontendBuildId', () => {
5
+ it('returns versions unchanged when no build id', () => {
6
+ const versions = { frontend: { displayName: '0.4.0', sha: 'abc1234' } }
7
+ expect(mergeFrontendBuildId(versions, undefined)).toEqual(versions)
8
+ })
9
+
10
+ it('shows local (dropping SHA) when build id is local', () => {
11
+ const versions = { frontend: { displayName: '0.4.0', sha: 'abc1234' } }
12
+ const out = mergeFrontendBuildId(versions, 'local')
13
+ expect(out.frontend).toEqual({ displayName: 'local' })
14
+ })
15
+
16
+ it('prepends pipeline id while preserving fe-core version + sha + url', () => {
17
+ const versions = {
18
+ frontend: {
19
+ displayName: '0.4.0-alpha-x',
20
+ sha: 'abc1234',
21
+ shaUrl: 'https://github.com/lislon/app-catalog/commit/abc1234',
22
+ },
23
+ }
24
+ const out = mergeFrontendBuildId(versions, '12345')
25
+ expect(out.frontend?.displayName).toBe('#12345 · 0.4.0-alpha-x')
26
+ expect(out.frontend?.sha).toBe('abc1234')
27
+ expect(out.frontend?.shaUrl).toBe(
28
+ 'https://github.com/lislon/app-catalog/commit/abc1234',
29
+ )
30
+ })
31
+
32
+ it('shows only pipeline id when backend gave no frontend slot', () => {
33
+ const out = mergeFrontendBuildId({}, '12345')
34
+ expect(out.frontend).toEqual({ displayName: '#12345' })
35
+ })
36
+
37
+ it('does not mutate the input', () => {
38
+ const versions = { frontend: { displayName: '0.4.0' } }
39
+ mergeFrontendBuildId(versions, '99')
40
+ expect(versions.frontend.displayName).toBe('0.4.0')
41
+ })
42
+ })
@@ -0,0 +1,38 @@
1
+ import { render, screen } from '@testing-library/react'
2
+ import { describe, expect, it } from 'vitest'
3
+ import '@testing-library/jest-dom/vitest'
4
+
5
+ import { VersionDisplay } from '~/ui/components/header/Header'
6
+
7
+ describe('VersionDisplay footer', () => {
8
+ it('renders the frontend-core version and its short SHA as a commit link', () => {
9
+ render(
10
+ <VersionDisplay
11
+ versions={{
12
+ backend: { displayName: '#12345 (deadbee)' },
13
+ coreVersion: { displayName: 'be 0.4.0 / fe 0.3.1' },
14
+ frontend: {
15
+ displayName: '#12345 · 0.3.1-alpha-x',
16
+ sha: 'abc1234',
17
+ shaUrl: 'https://github.com/lislon/app-catalog/commit/abc1234',
18
+ },
19
+ }}
20
+ />,
21
+ )
22
+ // FE line shows pipeline id + fe-core version
23
+ expect(screen.getByText(/0\.3\.1-alpha-x/)).toBeInTheDocument()
24
+ // SHA rendered and linked to the commit
25
+ const shaLink = screen.getByText('(abc1234)').closest('a')
26
+ expect(shaLink).toHaveAttribute(
27
+ 'href',
28
+ 'https://github.com/lislon/app-catalog/commit/abc1234',
29
+ )
30
+ // Backend line carries the natera SHA suffix
31
+ expect(screen.getByText(/#12345 \(deadbee\)/)).toBeInTheDocument()
32
+ })
33
+
34
+ it('shows plain "local" for local dev', () => {
35
+ render(<VersionDisplay versions={{ backend: { displayName: 'local' } }} />)
36
+ expect(screen.getByText('local')).toBeInTheDocument()
37
+ })
38
+ })
@@ -11,6 +11,7 @@ import type { ReactNode } from 'react'
11
11
  import { createContext, use, useEffect, useMemo } from 'react'
12
12
  import { ApiQueryMagazineAppCatalog } from '~/modules/appCatalog'
13
13
  import { useUiSettings } from '~/context/UiSettingsContext'
14
+ import { mergeFrontendBuildId } from '~/modules/appCatalog/context/mergeFrontendBuildId'
14
15
 
15
16
  export interface AppCatalogContextIface {
16
17
  resources: Resource[]
@@ -44,17 +45,10 @@ export function AppCatalogProvider({ children }: AppCatalogProviderProps) {
44
45
  approvalMethods: data?.approvalMethods ?? [],
45
46
  persons: data?.persons ?? [],
46
47
  groups: data?.groups ?? [],
47
- versions: {
48
- ...data?.versions,
49
- ...(uiSettings.frontendBuildId && {
50
- frontend: {
51
- displayName:
52
- uiSettings.frontendBuildId === 'local'
53
- ? 'local'
54
- : `#${uiSettings.frontendBuildId}`,
55
- },
56
- }),
57
- },
48
+ versions: mergeFrontendBuildId(
49
+ data?.versions ?? {},
50
+ uiSettings.frontendBuildId,
51
+ ),
58
52
  }),
59
53
  [
60
54
  data?.approvalMethods,
@@ -0,0 +1,31 @@
1
+ import type { AppVersionInfo } from '@igstack/app-catalog-backend-core'
2
+
3
+ /**
4
+ * Merge the (natera-agnostic) frontend build id into the backend-provided
5
+ * version info's `frontend` slot.
6
+ *
7
+ * The build id is baked into the deployed JS bundle at build time (e.g. a CI
8
+ * pipeline id via a Vite env var). We PREPEND it to the resolved frontend-core
9
+ * npm version instead of replacing it, so the footer surfaces both:
10
+ * `#<buildId> · <frontend-core version> (<sha>)`.
11
+ *
12
+ * - No build id → return versions unchanged.
13
+ * - build id === 'local' → show `local` (local dev, no meaningful SHA/version).
14
+ * - otherwise → prepend `#<buildId> · ` and preserve the version + sha + shaUrl.
15
+ */
16
+ export function mergeFrontendBuildId(
17
+ versions: AppVersionInfo,
18
+ frontendBuildId: string | undefined,
19
+ ): AppVersionInfo {
20
+ if (!frontendBuildId) return versions
21
+ if (frontendBuildId === 'local') {
22
+ return { ...versions, frontend: { displayName: 'local' } }
23
+ }
24
+ const be = versions.frontend
25
+ return {
26
+ ...versions,
27
+ frontend: be
28
+ ? { ...be, displayName: `#${frontendBuildId} · ${be.displayName}` }
29
+ : { displayName: `#${frontendBuildId}` },
30
+ }
31
+ }
@@ -4,12 +4,26 @@ import type { AcRouterContext } from '~/types/types'
4
4
  import { RootErrorPage } from '~/ui/components/error/RootErrorPage'
5
5
  import { NotFoundError } from '~/ui/error/NotFoundError'
6
6
  import { LoadingScreen } from '~/ui/layout/LoadingScreen'
7
+ import { TopLevelProvidersForErrors } from '~/ui/layout/TopLevelProvidersForErrors'
7
8
 
8
9
  export const Route = createRootRouteWithContext<AcRouterContext>()({
9
10
  component: RootRoute,
10
11
  errorComponent: RootErrorPage,
11
- pendingComponent: () => <LoadingScreen label="root pending" />,
12
- notFoundComponent: () => <NotFoundError />,
12
+ // These fallbacks render MainLayout → Header → useAuth(), so they must be
13
+ // wrapped in the providers (Auth + AuthModal + Theme + Tooltip). The router
14
+ // renders pending/notFound OUTSIDE the route-component subtree, i.e. outside
15
+ // the app's TopLevelProviders — without this wrap they throw
16
+ // "useAuth must be used within AuthProvider" (see #6/#7).
17
+ pendingComponent: () => (
18
+ <TopLevelProvidersForErrors>
19
+ <LoadingScreen label="root pending" />
20
+ </TopLevelProvidersForErrors>
21
+ ),
22
+ notFoundComponent: () => (
23
+ <TopLevelProvidersForErrors>
24
+ <NotFoundError />
25
+ </TopLevelProvidersForErrors>
26
+ ),
13
27
  wrapInSuspense: true,
14
28
  })
15
29
 
@@ -27,6 +27,27 @@ import {
27
27
  } from '~/ui/dropdown-menu'
28
28
  import { Skeleton } from '~/ui/skeleton'
29
29
 
30
+ function ShaSuffix({ version }: { version: VersionInfo }) {
31
+ if (!version.sha) return null
32
+ if (version.shaUrl) {
33
+ return (
34
+ <>
35
+ {' '}
36
+ <a
37
+ href={version.shaUrl}
38
+ target="_blank"
39
+ rel="noopener noreferrer"
40
+ className="text-muted-foreground hover:text-primary"
41
+ onClick={(e) => e.stopPropagation()}
42
+ >
43
+ ({version.sha})
44
+ </a>
45
+ </>
46
+ )
47
+ }
48
+ return <span className="text-muted-foreground"> ({version.sha})</span>
49
+ }
50
+
30
51
  function VersionItem({
31
52
  label,
32
53
  version,
@@ -34,28 +55,30 @@ function VersionItem({
34
55
  label: string
35
56
  version: VersionInfo
36
57
  }) {
37
- if (version.url) {
38
- return (
39
- <a
40
- href={version.url}
41
- target="_blank"
42
- rel="noopener noreferrer"
43
- className="text-xs text-primary hover:text-primary/80 transition-colors font-medium"
44
- onClick={(e) => e.stopPropagation()}
45
- title={label}
46
- >
47
- {label}: {version.displayName}
48
- </a>
49
- )
50
- }
58
+ // Outer element is a <span> (not <a>) so the optional SHA link can be a
59
+ // sibling anchor without producing invalid nested <a> markup.
51
60
  return (
52
61
  <span className="text-xs text-muted-foreground" title={label}>
53
- {label}: {version.displayName}
62
+ {label}:{' '}
63
+ {version.url ? (
64
+ <a
65
+ href={version.url}
66
+ target="_blank"
67
+ rel="noopener noreferrer"
68
+ className="text-primary hover:text-primary/80 transition-colors font-medium"
69
+ onClick={(e) => e.stopPropagation()}
70
+ >
71
+ {version.displayName}
72
+ </a>
73
+ ) : (
74
+ version.displayName
75
+ )}
76
+ <ShaSuffix version={version} />
54
77
  </span>
55
78
  )
56
79
  }
57
80
 
58
- function VersionDisplay({ versions }: { versions: AppVersionInfo }) {
81
+ export function VersionDisplay({ versions }: { versions: AppVersionInfo }) {
59
82
  const isLocal =
60
83
  versions.backend?.displayName === 'local' && !versions.coreVersion
61
84