@igstack/app-catalog-frontend-core 0.17.0 → 0.17.1-alpha-20260821043201

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 (52) hide show
  1. package/dist/esm/__tests__/integration/mock-backend/magazines.d.ts +1 -1
  2. package/dist/esm/__tests__/integration/tools/AppDetailTools.d.ts +16 -0
  3. package/dist/esm/__tests__/integration/tools/CatalogTools.d.ts +12 -0
  4. package/dist/esm/api/infra/trpc.d.ts +24 -0
  5. package/dist/esm/modules/appCatalog/hooks/useUpdateApp.d.ts +1 -0
  6. package/dist/esm/modules/appCatalog/ui/components/AccessPrerequisiteChain.d.ts +1 -1
  7. package/dist/esm/modules/appCatalog/ui/components/AccessPrerequisiteChain.js.map +1 -1
  8. package/dist/esm/modules/appCatalog/ui/components/SubResourceDetailPanel.d.ts +9 -0
  9. package/dist/esm/modules/appCatalog/ui/components/SubResourceDetailPanel.js +90 -0
  10. package/dist/esm/modules/appCatalog/ui/components/SubResourceDetailPanel.js.map +1 -0
  11. package/dist/esm/modules/appCatalog/ui/components/SubResourcesSection.d.ts +1 -1
  12. package/dist/esm/modules/appCatalog/ui/components/SubResourcesSection.js.map +1 -1
  13. package/dist/esm/modules/appCatalog/ui/grid/AppCatalogGrid.d.ts +7 -1
  14. package/dist/esm/modules/appCatalog/ui/grid/AppCatalogGrid.js +108 -31
  15. package/dist/esm/modules/appCatalog/ui/grid/AppCatalogGrid.js.map +1 -1
  16. package/dist/esm/modules/appCatalog/ui/pages/AppCatalogPage.js +31 -4
  17. package/dist/esm/modules/appCatalog/ui/pages/AppCatalogPage.js.map +1 -1
  18. package/dist/esm/modules/appCatalog/ui/pages/ServiceDesksPage.js +41 -20
  19. package/dist/esm/modules/appCatalog/ui/pages/ServiceDesksPage.js.map +1 -1
  20. package/dist/esm/modules/appCatalog/utils/markdownToPlainText.d.ts +1 -1
  21. package/dist/esm/modules/appCatalog/utils/markdownToPlainText.js.map +1 -1
  22. package/dist/esm/routeTree.gen.d.ts +2 -0
  23. package/dist/esm/routes/_layout/app.$slug.d.ts +1 -0
  24. package/dist/esm/routes/_layout/app._slug.js +2 -1
  25. package/dist/esm/routes/_layout/app._slug.js.map +1 -1
  26. package/dist/esm/routes/_layout/catalog.apps.index.d.ts +1 -0
  27. package/dist/esm/routes/_layout/catalog.apps.index.js +2 -1
  28. package/dist/esm/routes/_layout/catalog.apps.index.js.map +1 -1
  29. package/package.json +5 -7
  30. package/src/__tests__/integration/appCatalog.integration.test.ts +264 -5
  31. package/src/__tests__/integration/appDeepLink.integration.test.ts +46 -19
  32. package/src/__tests__/integration/crossRefLink.integration.test.ts +8 -8
  33. package/src/__tests__/integration/crossRefLinkListView.integration.test.ts +8 -8
  34. package/src/__tests__/integration/mock-backend/MockBackendConfigurer.ts +2 -0
  35. package/src/__tests__/integration/mock-backend/magazines.ts +24 -24
  36. package/src/__tests__/integration/replacementLink.integration.test.ts +2 -2
  37. package/src/__tests__/integration/searchDeprecatedFallback.integration.test.ts +2 -2
  38. package/src/__tests__/integration/serviceDesks.integration.test.ts +31 -24
  39. package/src/__tests__/integration/tools/AppDetailTools.ts +57 -0
  40. package/src/__tests__/integration/tools/CatalogTools.ts +77 -4
  41. package/src/__tests__/modules/appCatalog/utils/markdownToPlainText.test.ts +6 -6
  42. package/src/__tests__/modules/appCatalog/utils/resolveHelpers.test.ts +1 -1
  43. package/src/__tests__/modules/appCatalog/utils/searchApps.test.ts +11 -7
  44. package/src/modules/appCatalog/ui/components/AccessPrerequisiteChain.tsx +1 -1
  45. package/src/modules/appCatalog/ui/components/SubResourceDetailPanel.tsx +124 -0
  46. package/src/modules/appCatalog/ui/components/SubResourcesSection.tsx +1 -1
  47. package/src/modules/appCatalog/ui/grid/AppCatalogGrid.tsx +128 -34
  48. package/src/modules/appCatalog/ui/pages/AppCatalogPage.tsx +49 -8
  49. package/src/modules/appCatalog/ui/pages/ServiceDesksPage.tsx +37 -9
  50. package/src/modules/appCatalog/utils/markdownToPlainText.ts +1 -1
  51. package/src/routes/_layout/app.$slug.tsx +1 -0
  52. package/src/routes/_layout/catalog.apps.index.tsx +1 -0
@@ -2,7 +2,7 @@
2
2
  * Reduce a short markdown string to its visible plain text.
3
3
  *
4
4
  * Descriptions may contain markdown — notably cross-reference links like
5
- * `[Signatera Portal](/app/signatera-portal)` (#25). The detail panel renders
5
+ * `[Team Chat](/app/team-chat)` (#25). The detail panel renders
6
6
  * those as interactive links via {@link MarkdownText}, but in compact / clamped
7
7
  * surfaces (grid list preview, table row, filter combobox, sub-resources) an
8
8
  * interactive link would break `line-clamp` and can't be reliably highlighted
@@ -1 +1 @@
1
- {"version":3,"file":"markdownToPlainText.js","sources":["../../../../../src/modules/appCatalog/utils/markdownToPlainText.ts"],"sourcesContent":["/**\n * Reduce a short markdown string to its visible plain text.\n *\n * Descriptions may contain markdown — notably cross-reference links like\n * `[Signatera Portal](/app/signatera-portal)` (#25). The detail panel renders\n * those as interactive links via {@link MarkdownText}, but in compact / clamped\n * surfaces (grid list preview, table row, filter combobox, sub-resources) an\n * interactive link would break `line-clamp` and can't be reliably highlighted\n * for search. There we show the *text* only — so `[Name](/app/slug)` shows\n * \"Name\", never the raw bracket/paren syntax.\n *\n * This is deliberately a small, allocation-cheap regex pass (not a full\n * markdown parser): it covers the inline constructs that actually appear in\n * catalog descriptions — links, images, emphasis, and inline code.\n */\nexport function markdownToPlainText(input: string): string {\n if (!input) return ''\n\n return (\n input\n // Images: ![alt](url) → alt (must run before links)\n .replace(/!\\[([^\\]]*)\\]\\([^)]*\\)/g, '$1')\n // Links: [text](url) → text\n .replace(/\\[([^\\]]*)\\]\\([^)]*\\)/g, '$1')\n // Bold / italic markers: **x**, __x__, *x*, _x_ → x\n .replace(/(\\*\\*|__)(.*?)\\1/g, '$2')\n .replace(/(\\*|_)(.*?)\\1/g, '$2')\n // Inline code: `x` → x\n .replace(/`([^`]*)`/g, '$1')\n )\n}\n"],"names":[],"mappings":"AAeO,SAAS,oBAAoB,OAAuB;AACzD,MAAI,CAAC,MAAO,QAAO;AAEnB,SACE,MAEG,QAAQ,2BAA2B,IAAI,EAEvC,QAAQ,0BAA0B,IAAI,EAEtC,QAAQ,qBAAqB,IAAI,EACjC,QAAQ,kBAAkB,IAAI,EAE9B,QAAQ,cAAc,IAAI;AAEjC;"}
1
+ {"version":3,"file":"markdownToPlainText.js","sources":["../../../../../src/modules/appCatalog/utils/markdownToPlainText.ts"],"sourcesContent":["/**\n * Reduce a short markdown string to its visible plain text.\n *\n * Descriptions may contain markdown — notably cross-reference links like\n * `[Team Chat](/app/team-chat)` (#25). The detail panel renders\n * those as interactive links via {@link MarkdownText}, but in compact / clamped\n * surfaces (grid list preview, table row, filter combobox, sub-resources) an\n * interactive link would break `line-clamp` and can't be reliably highlighted\n * for search. There we show the *text* only — so `[Name](/app/slug)` shows\n * \"Name\", never the raw bracket/paren syntax.\n *\n * This is deliberately a small, allocation-cheap regex pass (not a full\n * markdown parser): it covers the inline constructs that actually appear in\n * catalog descriptions — links, images, emphasis, and inline code.\n */\nexport function markdownToPlainText(input: string): string {\n if (!input) return ''\n\n return (\n input\n // Images: ![alt](url) → alt (must run before links)\n .replace(/!\\[([^\\]]*)\\]\\([^)]*\\)/g, '$1')\n // Links: [text](url) → text\n .replace(/\\[([^\\]]*)\\]\\([^)]*\\)/g, '$1')\n // Bold / italic markers: **x**, __x__, *x*, _x_ → x\n .replace(/(\\*\\*|__)(.*?)\\1/g, '$2')\n .replace(/(\\*|_)(.*?)\\1/g, '$2')\n // Inline code: `x` → x\n .replace(/`([^`]*)`/g, '$1')\n )\n}\n"],"names":[],"mappings":"AAeO,SAAS,oBAAoB,OAAuB;AACzD,MAAI,CAAC,MAAO,QAAO;AAEnB,SACE,MAEG,QAAQ,2BAA2B,IAAI,EAEvC,QAAQ,0BAA0B,IAAI,EAEtC,QAAQ,qBAAqB,IAAI,EACjC,QAAQ,kBAAkB,IAAI,EAE9B,QAAQ,cAAc,IAAI;AAEjC;"}
@@ -29,12 +29,14 @@ declare const LayoutAppSlugRoute: import('@tanstack/router-core').Route<import('
29
29
  filters: import('zod').ZodOptional<import('zod').ZodString>;
30
30
  deprecated: import('zod').ZodOptional<import('zod').ZodString>;
31
31
  q: import('zod').ZodOptional<import('zod').ZodString>;
32
+ sub: import('zod').ZodOptional<import('zod').ZodString>;
32
33
  }, import('zod/v4/core').$strip>, import('@tanstack/router-core').ResolveParams<"/app/$slug">, import('@tanstack/router-core').AnyContext, import('@tanstack/router-core').AnyContext, import('@tanstack/router-core').AnyContext, {}, () => Promise<{
33
34
  appCatalogLoader: import('./modules/appCatalog/routeLoader.js').AppCatalogLoaderReturn;
34
35
  }>, unknown, unknown, unknown, unknown, undefined>;
35
36
  declare const LayoutCatalogAppsIndexRoute: import('@tanstack/router-core').Route<import('@tanstack/react-router').Register, import('@tanstack/router-core').Route<import('@tanstack/react-router').Register, import('@tanstack/react-router').RootRoute<import('@tanstack/react-router').Register, undefined, import('./types/types.js').AcRouterContext, import('@tanstack/router-core').AnyContext, import('@tanstack/router-core').AnyContext, {}, undefined, unknown, unknown, unknown, unknown, undefined>, "", "/", "/_layout", "/_layout", undefined, import('@tanstack/router-core').ResolveParams<"">, import('@tanstack/router-core').AnyContext, import('@tanstack/router-core').AnyContext, import('@tanstack/router-core').AnyContext, {}, undefined, unknown, unknown, unknown, unknown, undefined>, "/catalog/apps", "/catalog/apps/", "/_layout/catalog/apps/", "/_layout/catalog/apps/", import('zod').ZodObject<{
36
37
  app: import('zod').ZodOptional<import('zod').ZodString>;
37
38
  q: import('zod').ZodOptional<import('zod').ZodString>;
39
+ sub: import('zod').ZodOptional<import('zod').ZodString>;
38
40
  }, import('zod/v4/core').$strip>, import('@tanstack/router-core').ResolveParams<"/catalog/apps">, import('@tanstack/router-core').AnyContext, import('@tanstack/router-core').AnyContext, import('@tanstack/router-core').AnyContext, {}, (ctx: import('@tanstack/router-core').LoaderFnContext<import('@tanstack/react-router').Register, import('@tanstack/router-core').Route<import('@tanstack/react-router').Register, import('@tanstack/react-router').RootRoute<import('@tanstack/react-router').Register, undefined, import('./types/types.js').AcRouterContext, import('@tanstack/router-core').AnyContext, import('@tanstack/router-core').AnyContext, {}, undefined, unknown, unknown, unknown, unknown, undefined>, "", "/", "/_layout", "/_layout", undefined, import('@tanstack/router-core').ResolveParams<"">, import('@tanstack/router-core').AnyContext, import('@tanstack/router-core').AnyContext, import('@tanstack/router-core').AnyContext, {}, undefined, unknown, unknown, unknown, unknown, undefined>, "/_layout/catalog/apps/", import('@tanstack/router-core').ResolveParams<"/catalog/apps">, {}, import('@tanstack/router-core').AnyContext, import('@tanstack/router-core').AnyContext, import('@tanstack/router-core').AnyContext, unknown, undefined>) => Promise<void>, unknown, unknown, unknown, unknown, undefined>;
39
41
  export interface FileRoutesByFullPath {
40
42
  '/': typeof LayoutIndexRoute;
@@ -5,6 +5,7 @@ export declare const Route: import('@tanstack/router-core').Route<import('@tanst
5
5
  filters: z.ZodOptional<z.ZodString>;
6
6
  deprecated: z.ZodOptional<z.ZodString>;
7
7
  q: z.ZodOptional<z.ZodString>;
8
+ sub: z.ZodOptional<z.ZodString>;
8
9
  }, z.core.$strip>, import('@tanstack/router-core').ResolveParams<"/app/$slug">, import('@tanstack/router-core').AnyContext, import('@tanstack/router-core').AnyContext, import('@tanstack/router-core').AnyContext, {}, () => Promise<{
9
10
  appCatalogLoader: import('~/modules/appCatalog/routeLoader').AppCatalogLoaderReturn;
10
11
  }>, unknown, unknown, unknown, unknown, undefined>;
@@ -9,7 +9,8 @@ const searchSchema = object({
9
9
  recent: string().optional(),
10
10
  filters: string().optional(),
11
11
  deprecated: string().optional(),
12
- q: string().optional()
12
+ q: string().optional(),
13
+ sub: string().optional()
13
14
  });
14
15
  const Route = createFileRoute("/_layout/app/$slug")({
15
16
  component: RouteComponent,
@@ -1 +1 @@
1
- {"version":3,"file":"app._slug.js","sources":["../../../../src/routes/_layout/app.$slug.tsx"],"sourcesContent":["import { createFileRoute, stripSearchParams } from '@tanstack/react-router'\nimport { z } from 'zod'\nimport { appCatalogRouteLoader } from '~/modules/appCatalog/routeLoader'\nimport { AppCatalogLayout } from '~/modules/appCatalog/ui/layout/AppCatalogLayout'\nimport { AppCatalogPage } from '~/modules/appCatalog/ui/pages/AppCatalogPage'\n\n// Preserve the URL-synced filter params (see AppCatalogFiltersContext) on this\n// route so they aren't stripped on navigation. `q` (the search text) is the\n// exception: as of #27 it lives in sessionStorage, not the URL. It stays\n// declared here so an incoming legacy `?q=` still validates, but the\n// `stripSearchParams` middleware removes it so shared app links stay clean.\nconst searchSchema = z.object({\n filterTag: z.string().optional(),\n recent: z.string().optional(),\n filters: z.string().optional(),\n deprecated: z.string().optional(),\n q: z.string().optional(),\n})\n\nexport const Route = createFileRoute('/_layout/app/$slug')({\n component: RouteComponent,\n validateSearch: searchSchema,\n search: { middlewares: [stripSearchParams(['q'])] },\n async loader() {\n const appCatalogLoader = await appCatalogRouteLoader()\n return { appCatalogLoader }\n },\n})\n\nfunction RouteComponent() {\n const { queryClient, trpcClient } = Route.useRouteContext()\n const { slug } = Route.useParams()\n\n return (\n <AppCatalogLayout queryClient={queryClient} trpcClient={trpcClient}>\n <AppCatalogPage selectedSlug={slug} />\n </AppCatalogLayout>\n )\n}\n"],"names":["z.object","z.string"],"mappings":";;;;;;AAWA,MAAM,eAAeA,OAAS;AAAA,EAC5B,WAAWC,OAAE,EAAS,SAAA;AAAA,EACtB,QAAQA,OAAE,EAAS,SAAA;AAAA,EACnB,SAASA,OAAE,EAAS,SAAA;AAAA,EACpB,YAAYA,OAAE,EAAS,SAAA;AAAA,EACvB,GAAGA,OAAE,EAAS,SAAA;AAChB,CAAC;AAEM,MAAM,QAAQ,gBAAgB,oBAAoB,EAAE;AAAA,EACzD,WAAW;AAAA,EACX,gBAAgB;AAAA,EAChB,QAAQ,EAAE,aAAa,CAAC,kBAAkB,CAAC,GAAG,CAAC,CAAC,EAAA;AAAA,EAChD,MAAM,SAAS;AACb,UAAM,mBAAmB,MAAM,sBAAA;AAC/B,WAAO,EAAE,iBAAA;AAAA,EACX;AACF,CAAC;AAED,SAAS,iBAAiB;AACxB,QAAM,EAAE,aAAa,eAAe,MAAM,gBAAA;AAC1C,QAAM,EAAE,KAAA,IAAS,MAAM,UAAA;AAEvB,SACE,oBAAC,oBAAiB,aAA0B,YAC1C,8BAAC,gBAAA,EAAe,cAAc,MAAM,EAAA,CACtC;AAEJ;"}
1
+ {"version":3,"file":"app._slug.js","sources":["../../../../src/routes/_layout/app.$slug.tsx"],"sourcesContent":["import { createFileRoute, stripSearchParams } from '@tanstack/react-router'\nimport { z } from 'zod'\nimport { appCatalogRouteLoader } from '~/modules/appCatalog/routeLoader'\nimport { AppCatalogLayout } from '~/modules/appCatalog/ui/layout/AppCatalogLayout'\nimport { AppCatalogPage } from '~/modules/appCatalog/ui/pages/AppCatalogPage'\n\n// Preserve the URL-synced filter params (see AppCatalogFiltersContext) on this\n// route so they aren't stripped on navigation. `q` (the search text) is the\n// exception: as of #27 it lives in sessionStorage, not the URL. It stays\n// declared here so an incoming legacy `?q=` still validates, but the\n// `stripSearchParams` middleware removes it so shared app links stay clean.\nconst searchSchema = z.object({\n filterTag: z.string().optional(),\n recent: z.string().optional(),\n filters: z.string().optional(),\n deprecated: z.string().optional(),\n q: z.string().optional(),\n sub: z.string().optional(),\n})\n\nexport const Route = createFileRoute('/_layout/app/$slug')({\n component: RouteComponent,\n validateSearch: searchSchema,\n search: { middlewares: [stripSearchParams(['q'])] },\n async loader() {\n const appCatalogLoader = await appCatalogRouteLoader()\n return { appCatalogLoader }\n },\n})\n\nfunction RouteComponent() {\n const { queryClient, trpcClient } = Route.useRouteContext()\n const { slug } = Route.useParams()\n\n return (\n <AppCatalogLayout queryClient={queryClient} trpcClient={trpcClient}>\n <AppCatalogPage selectedSlug={slug} />\n </AppCatalogLayout>\n )\n}\n"],"names":["z.object","z.string"],"mappings":";;;;;;AAWA,MAAM,eAAeA,OAAS;AAAA,EAC5B,WAAWC,OAAE,EAAS,SAAA;AAAA,EACtB,QAAQA,OAAE,EAAS,SAAA;AAAA,EACnB,SAASA,OAAE,EAAS,SAAA;AAAA,EACpB,YAAYA,OAAE,EAAS,SAAA;AAAA,EACvB,GAAGA,OAAE,EAAS,SAAA;AAAA,EACd,KAAKA,OAAE,EAAS,SAAA;AAClB,CAAC;AAEM,MAAM,QAAQ,gBAAgB,oBAAoB,EAAE;AAAA,EACzD,WAAW;AAAA,EACX,gBAAgB;AAAA,EAChB,QAAQ,EAAE,aAAa,CAAC,kBAAkB,CAAC,GAAG,CAAC,CAAC,EAAA;AAAA,EAChD,MAAM,SAAS;AACb,UAAM,mBAAmB,MAAM,sBAAA;AAC/B,WAAO,EAAE,iBAAA;AAAA,EACX;AACF,CAAC;AAED,SAAS,iBAAiB;AACxB,QAAM,EAAE,aAAa,eAAe,MAAM,gBAAA;AAC1C,QAAM,EAAE,KAAA,IAAS,MAAM,UAAA;AAEvB,SACE,oBAAC,oBAAiB,aAA0B,YAC1C,8BAAC,gBAAA,EAAe,cAAc,MAAM,EAAA,CACtC;AAEJ;"}
@@ -2,4 +2,5 @@ import { z } from 'zod';
2
2
  export declare const Route: import('@tanstack/router-core').Route<import('@tanstack/react-router').Register, import('@tanstack/router-core').Route<import('@tanstack/react-router').Register, import('@tanstack/react-router').RootRoute<import('@tanstack/react-router').Register, undefined, import('../../types/types.js').AcRouterContext, import('@tanstack/router-core').AnyContext, import('@tanstack/router-core').AnyContext, {}, undefined, unknown, unknown, unknown, unknown, undefined>, "", "/", "/_layout", "/_layout", undefined, import('@tanstack/router-core').ResolveParams<"">, import('@tanstack/router-core').AnyContext, import('@tanstack/router-core').AnyContext, import('@tanstack/router-core').AnyContext, {}, undefined, unknown, unknown, unknown, unknown, undefined>, "/catalog/apps", "/catalog/apps/", "/_layout/catalog/apps/", "/_layout/catalog/apps/", z.ZodObject<{
3
3
  app: z.ZodOptional<z.ZodString>;
4
4
  q: z.ZodOptional<z.ZodString>;
5
+ sub: z.ZodOptional<z.ZodString>;
5
6
  }, z.core.$strip>, import('@tanstack/router-core').ResolveParams<"/catalog/apps">, import('@tanstack/router-core').AnyContext, import('@tanstack/router-core').AnyContext, import('@tanstack/router-core').AnyContext, {}, (ctx: import('@tanstack/router-core').LoaderFnContext<import('@tanstack/react-router').Register, import('@tanstack/router-core').Route<import('@tanstack/react-router').Register, import('@tanstack/react-router').RootRoute<import('@tanstack/react-router').Register, undefined, import('../../types/types.js').AcRouterContext, import('@tanstack/router-core').AnyContext, import('@tanstack/router-core').AnyContext, {}, undefined, unknown, unknown, unknown, unknown, undefined>, "", "/", "/_layout", "/_layout", undefined, import('@tanstack/router-core').ResolveParams<"">, import('@tanstack/router-core').AnyContext, import('@tanstack/router-core').AnyContext, import('@tanstack/router-core').AnyContext, {}, undefined, unknown, unknown, unknown, unknown, undefined>, "/_layout/catalog/apps/", import('@tanstack/router-core').ResolveParams<"/catalog/apps">, {}, import('@tanstack/router-core').AnyContext, import('@tanstack/router-core').AnyContext, import('@tanstack/router-core').AnyContext, unknown, undefined>) => Promise<void>, unknown, unknown, unknown, unknown, undefined>;
@@ -6,7 +6,8 @@ import { AppCatalogPage } from "../../modules/appCatalog/ui/pages/AppCatalogPage
6
6
  import { object, string } from "../../node_modules/.pnpm/zod@4.3.5/node_modules/zod/v4/classic/schemas.js";
7
7
  const searchSchema = object({
8
8
  app: string().optional(),
9
- q: string().optional()
9
+ q: string().optional(),
10
+ sub: string().optional()
10
11
  });
11
12
  const Route = createFileRoute("/_layout/catalog/apps/")({
12
13
  component: RouteComponent,
@@ -1 +1 @@
1
- {"version":3,"file":"catalog.apps.index.js","sources":["../../../../src/routes/_layout/catalog.apps.index.tsx"],"sourcesContent":["import { createFileRoute, stripSearchParams } from '@tanstack/react-router'\nimport { z } from 'zod'\nimport { catalogRouteLoader } from '~/modules/appCatalog/catalogRouteLoader'\nimport { AppCatalogLayout } from '~/modules/appCatalog/ui/layout/AppCatalogLayout'\nimport { AppCatalogPage } from '~/modules/appCatalog/ui/pages/AppCatalogPage'\n\n// #27: `q` kept declared for legacy links but stripped from the URL (search\n// text lives in sessionStorage now).\nconst searchSchema = z.object({\n app: z.string().optional(),\n q: z.string().optional(),\n})\n\nexport const Route = createFileRoute('/_layout/catalog/apps/')({\n component: RouteComponent,\n validateSearch: searchSchema,\n search: { middlewares: [stripSearchParams(['q'])] },\n async loader(ctx) {\n await catalogRouteLoader(ctx)\n },\n})\n\nfunction RouteComponent() {\n const { queryClient, trpcClient } = Route.useRouteContext()\n\n return (\n <AppCatalogLayout queryClient={queryClient} trpcClient={trpcClient}>\n <AppCatalogPage />\n </AppCatalogLayout>\n )\n}\n"],"names":["z.object","z.string"],"mappings":";;;;;;AAQA,MAAM,eAAeA,OAAS;AAAA,EAC5B,KAAKC,OAAE,EAAS,SAAA;AAAA,EAChB,GAAGA,OAAE,EAAS,SAAA;AAChB,CAAC;AAEM,MAAM,QAAQ,gBAAgB,wBAAwB,EAAE;AAAA,EAC7D,WAAW;AAAA,EACX,gBAAgB;AAAA,EAChB,QAAQ,EAAE,aAAa,CAAC,kBAAkB,CAAC,GAAG,CAAC,CAAC,EAAA;AAAA,EAChD,MAAM,OAAO,KAAK;AAChB,UAAM,mBAAmB,GAAG;AAAA,EAC9B;AACF,CAAC;AAED,SAAS,iBAAiB;AACxB,QAAM,EAAE,aAAa,eAAe,MAAM,gBAAA;AAE1C,6BACG,kBAAA,EAAiB,aAA0B,YAC1C,UAAA,oBAAC,kBAAe,GAClB;AAEJ;"}
1
+ {"version":3,"file":"catalog.apps.index.js","sources":["../../../../src/routes/_layout/catalog.apps.index.tsx"],"sourcesContent":["import { createFileRoute, stripSearchParams } from '@tanstack/react-router'\nimport { z } from 'zod'\nimport { catalogRouteLoader } from '~/modules/appCatalog/catalogRouteLoader'\nimport { AppCatalogLayout } from '~/modules/appCatalog/ui/layout/AppCatalogLayout'\nimport { AppCatalogPage } from '~/modules/appCatalog/ui/pages/AppCatalogPage'\n\n// #27: `q` kept declared for legacy links but stripped from the URL (search\n// text lives in sessionStorage now).\nconst searchSchema = z.object({\n app: z.string().optional(),\n q: z.string().optional(),\n sub: z.string().optional(),\n})\n\nexport const Route = createFileRoute('/_layout/catalog/apps/')({\n component: RouteComponent,\n validateSearch: searchSchema,\n search: { middlewares: [stripSearchParams(['q'])] },\n async loader(ctx) {\n await catalogRouteLoader(ctx)\n },\n})\n\nfunction RouteComponent() {\n const { queryClient, trpcClient } = Route.useRouteContext()\n\n return (\n <AppCatalogLayout queryClient={queryClient} trpcClient={trpcClient}>\n <AppCatalogPage />\n </AppCatalogLayout>\n )\n}\n"],"names":["z.object","z.string"],"mappings":";;;;;;AAQA,MAAM,eAAeA,OAAS;AAAA,EAC5B,KAAKC,OAAE,EAAS,SAAA;AAAA,EAChB,GAAGA,OAAE,EAAS,SAAA;AAAA,EACd,KAAKA,OAAE,EAAS,SAAA;AAClB,CAAC;AAEM,MAAM,QAAQ,gBAAgB,wBAAwB,EAAE;AAAA,EAC7D,WAAW;AAAA,EACX,gBAAgB;AAAA,EAChB,QAAQ,EAAE,aAAa,CAAC,kBAAkB,CAAC,GAAG,CAAC,CAAC,EAAA;AAAA,EAChD,MAAM,OAAO,KAAK;AAChB,UAAM,mBAAmB,GAAG;AAAA,EAC9B;AACF,CAAC;AAED,SAAS,iBAAiB;AACxB,QAAM,EAAE,aAAa,eAAe,MAAM,gBAAA;AAE1C,6BACG,kBAAA,EAAiB,aAA0B,YAC1C,UAAA,oBAAC,kBAAe,GAClB;AAEJ;"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@igstack/app-catalog-frontend-core",
3
- "version": "0.17.0",
3
+ "version": "0.17.1-alpha-20260821043201",
4
4
  "description": "Frontend core library for App Catalog",
5
5
  "homepage": "https://github.com/lislon/app-catalog",
6
6
  "repository": {
@@ -39,7 +39,6 @@
39
39
  "public"
40
40
  ],
41
41
  "dependencies": {
42
- "@ai-sdk/react": "^3.0.39",
43
42
  "@base-ui/react": "^1.0.0",
44
43
  "@radix-ui/react-alert-dialog": "^1.1.14",
45
44
  "@radix-ui/react-checkbox": "^1.3.3",
@@ -70,10 +69,9 @@
70
69
  "@trpc/client": "^11.8.1",
71
70
  "@trpc/server": "^11.4.2",
72
71
  "@trpc/tanstack-react-query": "^11.8.1",
73
- "ai": "^6.0.37",
74
72
  "autoprefixer": "^10.4.21",
75
73
  "autosuggest-highlight": "^3.3.4",
76
- "better-auth": "^1.4.18",
74
+ "better-auth": ">=1.4.18 <1.7.0",
77
75
  "class-variance-authority": "^0.7.1",
78
76
  "classnames": "^2.5.1",
79
77
  "clsx": "^2.1.1",
@@ -134,8 +132,8 @@
134
132
  "vite-plugin-static-copy": "^3.1.4",
135
133
  "vite-plugin-svgr": "^4.2.0",
136
134
  "vitest": "^4.1.2",
137
- "@igstack/app-catalog-shared-core": "0.17.0",
138
- "@igstack/app-catalog-backend-core": "0.17.0"
135
+ "@igstack/app-catalog-backend-core": "0.17.1-alpha-20260821043201",
136
+ "@igstack/app-catalog-shared-core": "0.17.1-alpha-20260821043201"
139
137
  },
140
138
  "peerDependencies": {
141
139
  "react": "19.1.2",
@@ -144,7 +142,7 @@
144
142
  "vite": "^6.3.5",
145
143
  "vite-plugin-svgr": "^4.2.0"
146
144
  },
147
- "gitHead": "c7c8e9105727375dce326320db3259b9ef2f9ceb",
145
+ "gitHead": "304d9c364de826d155069ac01370baffb0620c78",
148
146
  "scripts": {
149
147
  "build": "vite build",
150
148
  "build:lenient": "vite build --mode lenient",
@@ -1,5 +1,5 @@
1
1
  import { describe, expect, it } from 'vitest'
2
- import { fireEvent, waitFor } from '@testing-library/react'
2
+ import { fireEvent, screen, waitFor } from '@testing-library/react'
3
3
  import '@testing-library/jest-dom/vitest'
4
4
 
5
5
  import { given } from './harness/given'
@@ -18,7 +18,7 @@ describe('App Catalog Integration', () => {
18
18
  it('navigate screenshots then escape back to list view', async () => {
19
19
  const { ui } = await given(magazine.full())
20
20
 
21
- await ui.catalog.openApp('Jira')
21
+ await ui.catalog.openApp('TaskFlow')
22
22
  await ui.app.screenshots.open()
23
23
 
24
24
  await waitFor(() => {
@@ -45,7 +45,7 @@ describe('App Catalog Integration', () => {
45
45
  const approvalMethod = backendCfg.withApprovalMethod({
46
46
  type: 'service',
47
47
  displayName: 'Help Desk',
48
- config: { url: 'https://helpdesk.example.com' },
48
+ config: { url: 'https://support.example.com' },
49
49
  })
50
50
  backendCfg.withApp({
51
51
  displayName: 'My Custom App',
@@ -148,6 +148,36 @@ describe('App Catalog Integration', () => {
148
148
  expect(ui.catalog.isDetailPanelOpen()).toBe(false)
149
149
  })
150
150
 
151
+ // Detail card UX (#45): the detail panel shows a prominent primary "Open" button
152
+ // with the destination URL visible alongside it.
153
+ it('shows a prominent primary Open button with URL in the app detail card', async () => {
154
+ const { ui } = await given(
155
+ magazine.custom(({ backendCfg }) => {
156
+ backendCfg.withApp({
157
+ displayName: 'Website App',
158
+ description: 'Has a website',
159
+ appUrl: 'https://tools.example.com/launch',
160
+ })
161
+ backendCfg.withApp({
162
+ displayName: 'No URL App',
163
+ description: 'No url',
164
+ })
165
+ }),
166
+ )
167
+
168
+ await ui.catalog.openApp('Website App')
169
+ const btn = ui.app.getOpenButton()
170
+ expect(btn).not.toBeNull()
171
+ expect(btn!.getAttribute('href')).toBe('https://tools.example.com/launch')
172
+ expect(btn!.getAttribute('target')).toBe('_blank')
173
+ // The URL is shown in the button (stripped of protocol).
174
+ expect(btn!.textContent).toContain('tools.example.com/launch')
175
+
176
+ // An app without appUrl shows no Open button.
177
+ await ui.catalog.openApp('No URL App')
178
+ expect(ui.app.getOpenButton()).toBeNull()
179
+ })
180
+
151
181
  // Test 5: Returning user — cached data + no onboarding + backend down
152
182
  it('returning user sees cached apps even when backend is unavailable', async () => {
153
183
  suppressConsole([/TRPC Error/, /Failed to fetch/])
@@ -164,8 +194,8 @@ describe('App Catalog Integration', () => {
164
194
 
165
195
  const tableData = ui.catalog.getTableData()
166
196
  expect(tableData.length).toBe(5)
167
- expect(tableData.map((r) => r.name)).toContain('Jira')
168
- expect(tableData.map((r) => r.name)).toContain('Slack')
197
+ expect(tableData.map((r) => r.name)).toContain('TaskFlow')
198
+ expect(tableData.map((r) => r.name)).toContain('TeamChat')
169
199
  })
170
200
 
171
201
  // Test 3: Network Error — Connection Reset
@@ -298,4 +328,233 @@ describe('App Catalog Integration', () => {
298
328
  /(?<![\d])0(?![\d]).*clear filters/i,
299
329
  )
300
330
  })
331
+
332
+ it('highlights matched query text in search result app names (#57)', async () => {
333
+ const { ui } = await given(
334
+ magazine.custom(({ backendCfg }) => {
335
+ backendCfg.withApp({ displayName: 'Kubernetes Platform' })
336
+ backendCfg.withApp({ displayName: 'Other Tool' })
337
+ }),
338
+ )
339
+
340
+ await ui.catalog.search('kube')
341
+
342
+ await waitFor(() => {
343
+ const marks = document.querySelectorAll('mark')
344
+ const markTexts = [...marks].map((m) => String(m.textContent))
345
+ expect(markTexts.some((t) => t.toLowerCase().includes('kube'))).toBe(true)
346
+ })
347
+ })
348
+
349
+ it('clear (×) button appears when search has text and clears on click (#54)', async () => {
350
+ const { ui } = await given(magazine.full())
351
+
352
+ // Before typing: no clear button
353
+ expect(
354
+ document.querySelector('[aria-label="Clear search"]'),
355
+ ).not.toBeInTheDocument()
356
+
357
+ // After typing: clear button appears
358
+ await ui.catalog.search('taskflow')
359
+ await waitFor(() => {
360
+ expect(
361
+ document.querySelector('[aria-label="Clear search"]'),
362
+ ).toBeInTheDocument()
363
+ })
364
+
365
+ // Click × — input clears, button disappears
366
+ fireEvent.click(document.querySelector('[aria-label="Clear search"]')!)
367
+ await waitFor(() => {
368
+ expect(ui.catalog.getSearchInput().value).toBe('')
369
+ expect(
370
+ document.querySelector('[aria-label="Clear search"]'),
371
+ ).not.toBeInTheDocument()
372
+ })
373
+ })
374
+
375
+ it('shows expandable subresource rows under parent in search results', async () => {
376
+ const { ui } = await given(
377
+ magazine.custom(({ backendCfg }) => {
378
+ const app = backendCfg.withApp({
379
+ slug: 'aws-console',
380
+ displayName: 'AWS Console',
381
+ description: 'Cloud management console',
382
+ })
383
+ for (let i = 1; i <= 7; i++) {
384
+ backendCfg.withSubResource({
385
+ appSlug: app.slug,
386
+ slug: `acct-data-${i}`,
387
+ displayName: `Data Account ${i}`,
388
+ })
389
+ }
390
+ }),
391
+ )
392
+ await ui.catalog.search('Data Account')
393
+ await waitFor(() => {
394
+ const subRows = ui.catalog.getSubResourceRows()
395
+ expect(subRows).not.toBeNull()
396
+ expect(subRows!.visible).toBe(5)
397
+ expect(subRows!.total).toBe(7)
398
+ expect(subRows!.hasExpandRow).toBe(true)
399
+ })
400
+ })
401
+
402
+ it('expand "...N more" row reveals all subresources', async () => {
403
+ const { ui } = await given(
404
+ magazine.custom(({ backendCfg }) => {
405
+ const app = backendCfg.withApp({
406
+ slug: 'aws-console',
407
+ displayName: 'AWS Console',
408
+ })
409
+ for (let i = 1; i <= 7; i++) {
410
+ backendCfg.withSubResource({
411
+ appSlug: app.slug,
412
+ slug: `acct-data-${i}`,
413
+ displayName: `Data Account ${i}`,
414
+ })
415
+ }
416
+ }),
417
+ )
418
+ await ui.catalog.search('Data Account')
419
+ await waitFor(() => {
420
+ expect(ui.catalog.getSubResourceRows()).not.toBeNull()
421
+ })
422
+ await ui.catalog.expandSubResources()
423
+ const subRows = ui.catalog.getSubResourceRows()
424
+ expect(subRows!.visible).toBe(7)
425
+ expect(subRows!.hasExpandRow).toBe(false)
426
+ })
427
+
428
+ it('clicking subresource row shows its detail with two-step access', async () => {
429
+ const { ui } = await given(
430
+ magazine.custom(({ backendCfg }) => {
431
+ const parentMethod = backendCfg.withApprovalMethod({
432
+ type: 'service',
433
+ displayName: 'Support Portal',
434
+ config: { url: 'https://support.example.com' },
435
+ })
436
+ const subMethod = backendCfg.withApprovalMethod({
437
+ type: 'custom',
438
+ displayName: 'Account Owner',
439
+ })
440
+ const app = backendCfg.withApp({
441
+ slug: 'aws-console',
442
+ displayName: 'AWS Console',
443
+ accessRequest: {
444
+ approvalMethodSlug: parentMethod.slug,
445
+ comments: 'Submit a support ticket to request access',
446
+ },
447
+ })
448
+ backendCfg.withSubResource({
449
+ appSlug: app.slug,
450
+ slug: 'acct-data-prod',
451
+ displayName: 'Data Account Prod',
452
+ accessRequest: {
453
+ approvalMethodSlug: subMethod.slug,
454
+ comments: 'Contact account owner for account-level permissions',
455
+ },
456
+ })
457
+ }),
458
+ )
459
+ await ui.catalog.search('Data Account')
460
+ await waitFor(() => {
461
+ expect(ui.catalog.getSubResourceRows()).not.toBeNull()
462
+ })
463
+ await ui.catalog.clickSubResource('Data Account Prod')
464
+ await waitFor(() => {
465
+ expect(ui.app.getSubResourceDetail()).not.toBeNull()
466
+ })
467
+ const detail = ui.app.getSubResourceDetail()
468
+ expect(detail).not.toBeNull()
469
+ expect(detail!.subResourceName).toBe('Data Account Prod')
470
+ expect(detail!.hasStep1).toBe(true)
471
+ expect(detail!.hasStep2).toBe(true)
472
+ expect(detail!.backButtonLabel).toBe('Back to AWS Console')
473
+ })
474
+
475
+ it('back button from subresource detail returns to parent view', async () => {
476
+ const { ui } = await given(
477
+ magazine.custom(({ backendCfg }) => {
478
+ const method = backendCfg.withApprovalMethod({
479
+ type: 'service',
480
+ displayName: 'Support Portal',
481
+ config: { url: 'https://support.example.com' },
482
+ })
483
+ const app = backendCfg.withApp({
484
+ slug: 'aws-console',
485
+ displayName: 'AWS Console',
486
+ accessRequest: { approvalMethodSlug: method.slug },
487
+ })
488
+ backendCfg.withSubResource({
489
+ appSlug: app.slug,
490
+ slug: 'acct-data-prod',
491
+ displayName: 'Data Account Prod',
492
+ })
493
+ }),
494
+ )
495
+ await ui.catalog.search('Data Account')
496
+ await waitFor(() => {
497
+ expect(ui.catalog.getSubResourceRows()).not.toBeNull()
498
+ })
499
+ await ui.catalog.clickSubResource('Data Account Prod')
500
+ await waitFor(() => {
501
+ expect(ui.app.getSubResourceDetail()).not.toBeNull()
502
+ })
503
+ await ui.app.clickBackToParent()
504
+ // After going back, the sub detail should be gone and parent detail shown
505
+ expect(ui.app.getSubResourceDetail()).toBeNull()
506
+ expect(ui.catalog.isDetailPanelOpen()).toBe(true)
507
+ })
508
+
509
+ it('shows Step 1 / Step 2 badges for two-step access apps (#56)', async () => {
510
+ const { ui } = await given(
511
+ magazine.custom(({ backendCfg }) => {
512
+ const method = backendCfg.withApprovalMethod({
513
+ type: 'service',
514
+ displayName: 'Natero Bot',
515
+ config: { url: 'https://natero.example.com' },
516
+ })
517
+ backendCfg.withApp({
518
+ displayName: 'Two-Step App',
519
+ accessRequest: {
520
+ approvalMethodSlug: method.slug,
521
+ requestPrompt: 'Give me access to Two-Step App',
522
+ postApprovalInstructions: 'Contact the owner to complete setup.',
523
+ },
524
+ })
525
+ backendCfg.withApp({ displayName: 'Other App' })
526
+ }),
527
+ )
528
+ await ui.catalog.openApp('Two-Step App')
529
+ await waitFor(() => expect(ui.catalog.isDetailPanelOpen()).toBe(true))
530
+ expect(screen.getByText('Step 1')).toBeInTheDocument()
531
+ expect(screen.getByText('Step 2')).toBeInTheDocument()
532
+ expect(
533
+ screen.getByText('Contact the owner to complete setup.'),
534
+ ).toBeInTheDocument()
535
+ })
536
+
537
+ it('hides Step labels for single-step apps (#56)', async () => {
538
+ const { ui } = await given(
539
+ magazine.custom(({ backendCfg }) => {
540
+ const method = backendCfg.withApprovalMethod({
541
+ type: 'service',
542
+ displayName: 'Natero Bot',
543
+ config: { url: 'https://natero.example.com' },
544
+ })
545
+ backendCfg.withApp({
546
+ displayName: 'Single-Step App',
547
+ accessRequest: {
548
+ approvalMethodSlug: method.slug,
549
+ requestPrompt: 'Give me access',
550
+ },
551
+ })
552
+ backendCfg.withApp({ displayName: 'Other App' })
553
+ }),
554
+ )
555
+ await ui.catalog.openApp('Single-Step App')
556
+ await waitFor(() => expect(ui.catalog.isDetailPanelOpen()).toBe(true))
557
+ expect(screen.queryByText('Step 1')).not.toBeInTheDocument()
558
+ expect(screen.queryByText('Step 2')).not.toBeInTheDocument()
559
+ })
301
560
  })
@@ -1,5 +1,5 @@
1
1
  import { describe, expect, it } from 'vitest'
2
- import { fireEvent, waitFor } from '@testing-library/react'
2
+ import { fireEvent, screen, waitFor } from '@testing-library/react'
3
3
  import '@testing-library/jest-dom/vitest'
4
4
 
5
5
  import { given } from './harness/given'
@@ -7,7 +7,9 @@ import { magazine } from './mock-backend/magazines'
7
7
 
8
8
  describe('App deep-link routing', () => {
9
9
  it('opens the app detail when navigating directly to /app/<slug>', async () => {
10
- const { ui } = await given(magazine.full(), { initialRoute: '/app/jira' })
10
+ const { ui } = await given(magazine.full(), {
11
+ initialRoute: '/app/taskflow',
12
+ })
11
13
  await waitFor(() => {
12
14
  expect(ui.catalog.isDetailPanelOpen()).toBe(true)
13
15
  })
@@ -16,9 +18,9 @@ describe('App deep-link routing', () => {
16
18
 
17
19
  it('navigates to /app/<slug> when selecting an app, and pushes history', async () => {
18
20
  const { ui, router } = await given(magazine.full(), { initialRoute: '/' })
19
- await ui.catalog.openApp('Jira')
21
+ await ui.catalog.openApp('TaskFlow')
20
22
  await waitFor(() => {
21
- expect(router.state.location.pathname).toBe('/app/jira')
23
+ expect(router.state.location.pathname).toBe('/app/taskflow')
22
24
  })
23
25
  // push (not replace): catalog '/' is still in history, Back returns to it
24
26
  expect(router.history.canGoBack()).toBe(true)
@@ -36,9 +38,32 @@ describe('App deep-link routing', () => {
36
38
 
37
39
  it('returns to / when the detail panel is closed', async () => {
38
40
  const { ui, router } = await given(magazine.full(), { initialRoute: '/' })
39
- await ui.catalog.openApp('Jira')
41
+ await ui.catalog.openApp('TaskFlow')
42
+ await waitFor(() => expect(ui.catalog.isDetailPanelOpen()).toBe(true))
43
+ fireEvent.keyDown(document, { key: 'Escape', code: 'Escape' })
44
+ await waitFor(() => {
45
+ expect(ui.catalog.isDetailPanelOpen()).toBe(false)
46
+ })
47
+ expect(router.state.location.pathname).toBe('/')
48
+ })
49
+
50
+ it('Esc closes card opened by mouse click (#53 bug 1)', async () => {
51
+ const { ui, router } = await given(magazine.full(), { initialRoute: '/' })
52
+ // Open via click (mouse, not keyboard nav)
53
+ await ui.catalog.openApp('TaskFlow')
40
54
  await waitFor(() => expect(ui.catalog.isDetailPanelOpen()).toBe(true))
55
+ // Esc should close — no "dead click" needed
41
56
  fireEvent.keyDown(document, { key: 'Escape', code: 'Escape' })
57
+ await waitFor(() => expect(ui.catalog.isDetailPanelOpen()).toBe(false))
58
+ expect(router.state.location.pathname).toBe('/')
59
+ })
60
+
61
+ it('closes the detail panel when the × close button is clicked (#59)', async () => {
62
+ const { ui, router } = await given(magazine.full(), { initialRoute: '/' })
63
+ await ui.catalog.openApp('TaskFlow')
64
+ await waitFor(() => expect(ui.catalog.isDetailPanelOpen()).toBe(true))
65
+ const closeBtn = screen.getByRole('button', { name: /^Close$/i })
66
+ fireEvent.click(closeBtn)
42
67
  await waitFor(() => {
43
68
  expect(ui.catalog.isDetailPanelOpen()).toBe(false)
44
69
  })
@@ -46,7 +71,9 @@ describe('App deep-link routing', () => {
46
71
  })
47
72
 
48
73
  it('ignores legacy ?app= query param (no detail opens)', async () => {
49
- const { ui } = await given(magazine.full(), { initialRoute: '/?app=jira' })
74
+ const { ui } = await given(magazine.full(), {
75
+ initialRoute: '/?app=taskflow',
76
+ })
50
77
  await waitFor(() => {
51
78
  expect(ui.catalog.getTableData().length).toBeGreaterThan(0)
52
79
  })
@@ -64,7 +91,7 @@ describe('App deep-link routing', () => {
64
91
  // jsdom userEvent.type + useDeferredValue drops characters, orthogonal here.
65
92
  const { ui, router } = await given(magazine.full(), {
66
93
  initialRoute: '/',
67
- seedSearch: 'jira',
94
+ seedSearch: 'taskflow',
68
95
  })
69
96
 
70
97
  // Stays on the launcher home — NO auto-navigation to the detail route.
@@ -72,33 +99,33 @@ describe('App deep-link routing', () => {
72
99
  expect(ui.catalog.getTableData().length).toBeGreaterThan(0)
73
100
  })
74
101
  expect(router.state.location.pathname).toBe('/')
75
- expect(ui.catalog.getTableData().map((r) => r.name)).toContain('Jira')
102
+ expect(ui.catalog.getTableData().map((r) => r.name)).toContain('TaskFlow')
76
103
  // Search text preserved (sessionStorage-backed), no ?q leak.
77
- expect(ui.catalog.getSearchInput().value).toBe('jira')
104
+ expect(ui.catalog.getSearchInput().value).toBe('taskflow')
78
105
  expect((router.state.location.search as { q?: string }).q).toBeUndefined()
79
106
 
80
107
  // Opting in (click) opens the detail.
81
- await ui.catalog.openApp('Jira')
108
+ await ui.catalog.openApp('TaskFlow')
82
109
  await waitFor(() => {
83
- expect(router.state.location.pathname).toBe('/app/jira')
110
+ expect(router.state.location.pathname).toBe('/app/taskflow')
84
111
  })
85
112
  })
86
113
 
87
114
  it('strips a legacy ?q= from the URL on the app detail route (#27)', async () => {
88
- // Old shared links may still carry `?q=` (e.g. /app/jira?q=jira). `q` is
115
+ // Old shared links may still carry `?q=` (e.g. /app/taskflow?q=taskflow). `q` is
89
116
  // still declared in the route search schema (so it validates), but the
90
117
  // route's `stripSearchParams(['q'])` middleware rewrites the URL to a clean
91
- // /app/jira on load. We no longer read `q` from the URL, so a bare deep-link
118
+ // /app/taskflow on load. We no longer read `q` from the URL, so a bare deep-link
92
119
  // doesn't repopulate the input from the query string.
93
120
  const { ui, router } = await given(magazine.full(), {
94
- initialRoute: '/app/jira?q=jira',
121
+ initialRoute: '/app/taskflow?q=taskflow',
95
122
  })
96
123
 
97
124
  await waitFor(() => {
98
125
  expect(ui.catalog.isDetailPanelOpen()).toBe(true)
99
126
  })
100
- expect(router.state.location.pathname).toBe('/app/jira')
101
- expect(router.state.location.href).toBe('/app/jira')
127
+ expect(router.state.location.pathname).toBe('/app/taskflow')
128
+ expect(router.state.location.href).toBe('/app/taskflow')
102
129
  expect((router.state.location.search as { q?: string }).q).toBeUndefined()
103
130
  })
104
131
 
@@ -131,14 +158,14 @@ describe('App deep-link routing', () => {
131
158
  // store as a returning user would have (typed a search, then navigated), and
132
159
  // assert the detail route restores it with no `q` in the URL.
133
160
  const { ui, router } = await given(magazine.full(), {
134
- initialRoute: '/app/jira',
135
- seedSearch: 'jira',
161
+ initialRoute: '/app/taskflow',
162
+ seedSearch: 'taskflow',
136
163
  })
137
164
 
138
165
  await waitFor(() => {
139
166
  expect(ui.catalog.isDetailPanelOpen()).toBe(true)
140
167
  })
141
- expect(ui.catalog.getSearchInput().value).toBe('jira')
168
+ expect(ui.catalog.getSearchInput().value).toBe('taskflow')
142
169
  expect((router.state.location.search as { q?: string }).q).toBeUndefined()
143
170
  })
144
171
  })
@@ -16,7 +16,7 @@ describe('Cross-reference links between app entries (#25)', () => {
16
16
  slug: 'portals-hub',
17
17
  displayName: 'Portals Hub',
18
18
  description:
19
- 'See the [Jira](/app/jira) entry and the external [docs](https://example.com/docs). Also [Ghost](/app/no-such-app) which does not exist.',
19
+ 'See the [TaskFlow](/app/taskflow) entry and the external [docs](https://example.com/docs). Also [Ghost](/app/no-such-app) which does not exist.',
20
20
  })
21
21
  })
22
22
 
@@ -28,22 +28,22 @@ describe('Cross-reference links between app entries (#25)', () => {
28
28
  expect(ui.catalog.isDetailPanelOpen()).toBe(true)
29
29
  })
30
30
 
31
- const jiraLink = screen.getByRole('link', { name: 'Jira' })
31
+ const taskflowLink = screen.getByRole('link', { name: 'TaskFlow' })
32
32
  // Internal link must NOT open a new tab.
33
- expect(jiraLink).not.toHaveAttribute('target', '_blank')
33
+ expect(taskflowLink).not.toHaveAttribute('target', '_blank')
34
34
  // It points at the router path (rendered as an href).
35
- expect(jiraLink).toHaveAttribute('href', '/app/jira')
35
+ expect(taskflowLink).toHaveAttribute('href', '/app/taskflow')
36
36
 
37
37
  const user = userEvent.setup()
38
- await user.click(jiraLink)
38
+ await user.click(taskflowLink)
39
39
 
40
- // In-app navigation: the router path changes and Jira's detail opens,
40
+ // In-app navigation: the router path changes and TaskFlow's detail opens,
41
41
  // without a full page reload / new tab.
42
42
  await waitFor(() => {
43
- expect(router.state.location.pathname).toBe('/app/jira')
43
+ expect(router.state.location.pathname).toBe('/app/taskflow')
44
44
  })
45
45
  await waitFor(() => {
46
- expect(ui.app.getOpenTitle()).toContain('Jira')
46
+ expect(ui.app.getOpenTitle()).toContain('TaskFlow')
47
47
  })
48
48
  })
49
49