@igstack/app-catalog-frontend-core 0.10.1 → 0.11.0

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 (88) hide show
  1. package/dist/esm/__tests__/integration/harness/given.d.ts +1 -0
  2. package/dist/esm/__tests__/integration/skewWarning.integration.test.d.ts +0 -0
  3. package/dist/esm/__tests__/integration/tools/AppDetailTools.d.ts +5 -0
  4. package/dist/esm/__tests__/integration/tools/CatalogTools.d.ts +15 -1
  5. package/dist/esm/__tests__/modules/appCatalog/utils/resolveHelpers.test.d.ts +1 -0
  6. package/dist/esm/modules/appCatalog/hooks/useSessionSyncedState.d.ts +40 -0
  7. package/dist/esm/modules/appCatalog/hooks/useSessionSyncedState.js +40 -0
  8. package/dist/esm/modules/appCatalog/hooks/useSessionSyncedState.js.map +1 -0
  9. package/dist/esm/modules/appCatalog/ui/components/AccessPrerequisiteChain.d.ts +17 -0
  10. package/dist/esm/modules/appCatalog/ui/components/AccessPrerequisiteChain.js +61 -0
  11. package/dist/esm/modules/appCatalog/ui/components/AccessPrerequisiteChain.js.map +1 -0
  12. package/dist/esm/modules/appCatalog/ui/components/AccessRequestSection.js +9 -2
  13. package/dist/esm/modules/appCatalog/ui/components/AccessRequestSection.js.map +1 -1
  14. package/dist/esm/modules/appCatalog/ui/components/OnboardingCard.js +1 -1
  15. package/dist/esm/modules/appCatalog/ui/components/OnboardingCard.js.map +1 -1
  16. package/dist/esm/modules/appCatalog/ui/components/SubResourcesSection.d.ts +9 -1
  17. package/dist/esm/modules/appCatalog/ui/components/SubResourcesSection.js +12 -3
  18. package/dist/esm/modules/appCatalog/ui/components/SubResourcesSection.js.map +1 -1
  19. package/dist/esm/modules/appCatalog/ui/context/AppCatalogFiltersContext.js +3 -2
  20. package/dist/esm/modules/appCatalog/ui/context/AppCatalogFiltersContext.js.map +1 -1
  21. package/dist/esm/modules/appCatalog/ui/grid/AppCatalogGrid.d.ts +5 -0
  22. package/dist/esm/modules/appCatalog/ui/grid/AppCatalogGrid.js +56 -10
  23. package/dist/esm/modules/appCatalog/ui/grid/AppCatalogGrid.js.map +1 -1
  24. package/dist/esm/modules/appCatalog/ui/launcher/LauncherDetailPanel.d.ts +12 -0
  25. package/dist/esm/modules/appCatalog/ui/launcher/LauncherDetailPanel.js +48 -0
  26. package/dist/esm/modules/appCatalog/ui/launcher/LauncherDetailPanel.js.map +1 -0
  27. package/dist/esm/modules/appCatalog/ui/launcher/LauncherHome.d.ts +19 -0
  28. package/dist/esm/modules/appCatalog/ui/launcher/LauncherHome.js +355 -0
  29. package/dist/esm/modules/appCatalog/ui/launcher/LauncherHome.js.map +1 -0
  30. package/dist/esm/modules/appCatalog/ui/launcher/ResourceIcon.d.ts +13 -0
  31. package/dist/esm/modules/appCatalog/ui/launcher/ResourceIcon.js +45 -0
  32. package/dist/esm/modules/appCatalog/ui/launcher/ResourceIcon.js.map +1 -0
  33. package/dist/esm/modules/appCatalog/ui/launcher/monogram.d.ts +4 -0
  34. package/dist/esm/modules/appCatalog/ui/launcher/monogram.js +41 -0
  35. package/dist/esm/modules/appCatalog/ui/launcher/monogram.js.map +1 -0
  36. package/dist/esm/modules/appCatalog/ui/pages/AppCatalogPage.js +40 -6
  37. package/dist/esm/modules/appCatalog/ui/pages/AppCatalogPage.js.map +1 -1
  38. package/dist/esm/modules/appCatalog/utils/markdownToPlainText.d.ts +1 -1
  39. package/dist/esm/modules/appCatalog/utils/markdownToPlainText.js.map +1 -1
  40. package/dist/esm/modules/appCatalog/utils/resolveHelpers.d.ts +10 -0
  41. package/dist/esm/modules/appCatalog/utils/resolveHelpers.js +17 -0
  42. package/dist/esm/modules/appCatalog/utils/resolveHelpers.js.map +1 -1
  43. package/dist/esm/routeTree.gen.d.ts +1 -1
  44. package/dist/esm/routes/_layout/app.$slug.d.ts +1 -1
  45. package/dist/esm/routes/_layout/app._slug.js +4 -3
  46. package/dist/esm/routes/_layout/app._slug.js.map +1 -1
  47. package/dist/esm/routes/_layout/catalog.apps.index.js +2 -1
  48. package/dist/esm/routes/_layout/catalog.apps.index.js.map +1 -1
  49. package/dist/esm/routes/_layout/index.js +2 -1
  50. package/dist/esm/routes/_layout/index.js.map +1 -1
  51. package/dist/esm/routes/_layout/service-desks.js +2 -1
  52. package/dist/esm/routes/_layout/service-desks.js.map +1 -1
  53. package/dist/esm/ui/components/header/Header.js +1 -1
  54. package/dist/esm/ui/components/header/Header.js.map +1 -1
  55. package/dist/index.css +73 -66
  56. package/package.json +4 -4
  57. package/src/__tests__/integration/appCatalog.integration.test.ts +106 -0
  58. package/src/__tests__/integration/appDeepLink.integration.test.ts +50 -24
  59. package/src/__tests__/integration/crossRefLinkListView.integration.test.ts +8 -8
  60. package/src/__tests__/integration/harness/given.tsx +9 -1
  61. package/src/__tests__/integration/replacementLink.integration.test.ts +6 -2
  62. package/src/__tests__/integration/searchDeprecatedFallback.integration.test.ts +21 -16
  63. package/src/__tests__/integration/setup/testSetup.ts +1 -0
  64. package/src/__tests__/integration/skewWarning.integration.test.ts +56 -0
  65. package/src/__tests__/integration/tools/AppDetailTools.ts +13 -0
  66. package/src/__tests__/integration/tools/CatalogTools.ts +60 -11
  67. package/src/__tests__/modules/appCatalog/utils/markdownToPlainText.test.ts +5 -5
  68. package/src/__tests__/modules/appCatalog/utils/resolveHelpers.test.ts +79 -0
  69. package/src/index.css +73 -66
  70. package/src/modules/appCatalog/hooks/useSessionSyncedState.ts +83 -0
  71. package/src/modules/appCatalog/ui/components/AccessPrerequisiteChain.tsx +89 -0
  72. package/src/modules/appCatalog/ui/components/AccessRequestSection.tsx +31 -2
  73. package/src/modules/appCatalog/ui/components/OnboardingCard.tsx +1 -1
  74. package/src/modules/appCatalog/ui/components/SubResourcesSection.tsx +22 -1
  75. package/src/modules/appCatalog/ui/context/AppCatalogFiltersContext.tsx +11 -5
  76. package/src/modules/appCatalog/ui/grid/AppCatalogGrid.tsx +68 -10
  77. package/src/modules/appCatalog/ui/launcher/LauncherDetailPanel.tsx +55 -0
  78. package/src/modules/appCatalog/ui/launcher/LauncherHome.tsx +497 -0
  79. package/src/modules/appCatalog/ui/launcher/ResourceIcon.tsx +54 -0
  80. package/src/modules/appCatalog/ui/launcher/monogram.ts +41 -0
  81. package/src/modules/appCatalog/ui/pages/AppCatalogPage.tsx +75 -13
  82. package/src/modules/appCatalog/utils/markdownToPlainText.ts +1 -1
  83. package/src/modules/appCatalog/utils/resolveHelpers.ts +31 -0
  84. package/src/routes/_layout/app.$slug.tsx +7 -4
  85. package/src/routes/_layout/catalog.apps.index.tsx +4 -1
  86. package/src/routes/_layout/index.tsx +5 -1
  87. package/src/routes/_layout/service-desks.tsx +4 -1
  88. package/src/ui/components/header/Header.tsx +3 -1
@@ -19,4 +19,5 @@ export interface UiTools {
19
19
  export declare function cleanupTestResources(): Promise<void>;
20
20
  export declare function given(magazine: Magazine, opts?: {
21
21
  initialRoute?: string;
22
+ seedSearch?: string;
22
23
  }): Promise<GivenResult>;
@@ -26,6 +26,11 @@ export declare class AppDetailTools {
26
26
  visible: number;
27
27
  names: string[];
28
28
  } | null;
29
+ /**
30
+ * Read the "How to get access" section text from the detail panel.
31
+ * Returns null if the section is not present.
32
+ */
33
+ getAccessText(): string | null;
29
34
  screenshots: {
30
35
  /**
31
36
  * Click the screenshot preview to open the gallery modal.
@@ -6,6 +6,8 @@ export declare class CatalogTools {
6
6
  private user;
7
7
  /**
8
8
  * Click an app row by display name.
9
+ * Works against both the table/grid view and the launcher home (#38), which
10
+ * renders app rows as buttons rather than a <table>.
9
11
  * Throws with list of visible apps if name not found.
10
12
  */
11
13
  openApp(name: string): Promise<void>;
@@ -20,6 +22,11 @@ export declare class CatalogTools {
20
22
  * Skips group header rows (colspan rows).
21
23
  */
22
24
  getTableData(): TableRow[];
25
+ /**
26
+ * The secondary "open in new tab" launch link for a row, by app name.
27
+ * Returns null if the row has no launch link.
28
+ */
29
+ getLaunchLink(name: string): HTMLAnchorElement | null;
23
30
  /**
24
31
  * Whether the right detail panel is currently visible.
25
32
  */
@@ -28,12 +35,19 @@ export declare class CatalogTools {
28
35
  isShowDeprecatedChecked(): boolean;
29
36
  /** Whether the "showing deprecated matches" fallback notice is visible. */
30
37
  hasDeprecatedFallbackNotice(): boolean;
31
- /** Whether the "No apps found" empty state is visible. */
38
+ /** Whether the "No apps found" empty state or search-morph "No results for" is visible. */
32
39
  isEmptyStateVisible(): boolean;
33
40
  /**
34
41
  * Whether the onboarding/welcome card is visible.
35
42
  */
36
43
  isOnboardingVisible(): boolean;
44
+ /**
45
+ * Read app rows from the launcher home (#38): buttons titled "View <name>".
46
+ * Name is the first `.font-semibold`/`.font-bold` span; description the
47
+ * `.text-muted-foreground` span. Deduped by name (an app can appear both in
48
+ * "Your apps" and "Browse all").
49
+ */
50
+ private getLauncherRows;
37
51
  /**
38
52
  * Get the main catalog table (first table on page, skipping sub-resource tables in detail panel).
39
53
  */
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Options for useSessionSyncedState hook
3
+ */
4
+ export interface UseSessionSyncedStateOptions<T> {
5
+ /** The sessionStorage key */
6
+ key: string;
7
+ /** Default value when the stored value is absent */
8
+ defaultValue: T;
9
+ /** Optional decoder to transform the stored string to a state value */
10
+ decode?: (storedValue: string) => T;
11
+ /** Optional encoder to transform the state value to a string (return undefined to clear) */
12
+ encode?: (stateValue: T) => string | undefined;
13
+ }
14
+ /**
15
+ * Hook for state that persists in `sessionStorage` instead of the URL.
16
+ *
17
+ * Why this exists (see #27): the catalog search value used to be
18
+ * synced to the URL (`?q=`) so it survived the per-route remount of
19
+ * `AppCatalogFiltersProvider` (#10). But `q` leaked into every shared/bookmarked
20
+ * app link. Backing the value with `sessionStorage` keeps the #10 guarantee —
21
+ * the value survives the remount and the auto-navigate to a single match — while
22
+ * keeping the URL clean.
23
+ *
24
+ * Key features vs {@link useUrlSyncedState}:
25
+ * - Initializes synchronously from `sessionStorage` on mount.
26
+ * - Writes synchronously in the setter (no async effect), which also removes the
27
+ * effect-ordering race #10 fought (a child auto-navigate effect could run
28
+ * before the provider's async state->URL sync).
29
+ * - Never navigates, so it never touches the URL.
30
+ *
31
+ * Defensive against environments without a functional `sessionStorage`
32
+ * (some CI jsdom setups) — falls back to plain in-memory state.
33
+ */
34
+ export declare function useSessionSyncedState<T>({ key, defaultValue, decode, encode, }: UseSessionSyncedStateOptions<T>): [T, (value: T) => void];
35
+ /**
36
+ * sessionStorage key backing the catalog search value (#27). Exported so tests
37
+ * (and any tooling that needs to seed a "returning user" search) reference the
38
+ * same key as the provider.
39
+ */
40
+ export declare const SEARCH_STORAGE_KEY = "app-catalog:search";
@@ -0,0 +1,40 @@
1
+ import { useState, useCallback } from "react";
2
+ function useSessionSyncedState({
3
+ key,
4
+ defaultValue,
5
+ decode,
6
+ encode
7
+ }) {
8
+ const [state, setStateInternal] = useState(() => {
9
+ try {
10
+ const stored = globalThis.sessionStorage.getItem(key);
11
+ if (stored !== null) {
12
+ return decode ? decode(stored) : stored;
13
+ }
14
+ } catch {
15
+ }
16
+ return defaultValue;
17
+ });
18
+ const setState = useCallback(
19
+ (value) => {
20
+ setStateInternal(value);
21
+ try {
22
+ const encoded = encode ? encode(value) : value;
23
+ if (encoded === void 0) {
24
+ globalThis.sessionStorage.removeItem(key);
25
+ } else {
26
+ globalThis.sessionStorage.setItem(key, encoded);
27
+ }
28
+ } catch {
29
+ }
30
+ },
31
+ [key, encode]
32
+ );
33
+ return [state, setState];
34
+ }
35
+ const SEARCH_STORAGE_KEY = "app-catalog:search";
36
+ export {
37
+ SEARCH_STORAGE_KEY,
38
+ useSessionSyncedState
39
+ };
40
+ //# sourceMappingURL=useSessionSyncedState.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useSessionSyncedState.js","sources":["../../../../../src/modules/appCatalog/hooks/useSessionSyncedState.ts"],"sourcesContent":["import { useCallback, useState } from 'react'\n\n/**\n * Options for useSessionSyncedState hook\n */\nexport interface UseSessionSyncedStateOptions<T> {\n /** The sessionStorage key */\n key: string\n /** Default value when the stored value is absent */\n defaultValue: T\n /** Optional decoder to transform the stored string to a state value */\n decode?: (storedValue: string) => T\n /** Optional encoder to transform the state value to a string (return undefined to clear) */\n encode?: (stateValue: T) => string | undefined\n}\n\n/**\n * Hook for state that persists in `sessionStorage` instead of the URL.\n *\n * Why this exists (see #27): the catalog search value used to be\n * synced to the URL (`?q=`) so it survived the per-route remount of\n * `AppCatalogFiltersProvider` (#10). But `q` leaked into every shared/bookmarked\n * app link. Backing the value with `sessionStorage` keeps the #10 guarantee —\n * the value survives the remount and the auto-navigate to a single match — while\n * keeping the URL clean.\n *\n * Key features vs {@link useUrlSyncedState}:\n * - Initializes synchronously from `sessionStorage` on mount.\n * - Writes synchronously in the setter (no async effect), which also removes the\n * effect-ordering race #10 fought (a child auto-navigate effect could run\n * before the provider's async state->URL sync).\n * - Never navigates, so it never touches the URL.\n *\n * Defensive against environments without a functional `sessionStorage`\n * (some CI jsdom setups) — falls back to plain in-memory state.\n */\nexport function useSessionSyncedState<T>({\n key,\n defaultValue,\n decode,\n encode,\n}: UseSessionSyncedStateOptions<T>): [T, (value: T) => void] {\n const [state, setStateInternal] = useState<T>(() => {\n try {\n // The try/catch is the real guard against a missing/throwing\n // sessionStorage (SSR, locked-down jsdom); the access itself is typed\n // non-null, so no optional chaining is needed here.\n const stored = globalThis.sessionStorage.getItem(key)\n if (stored !== null) {\n return decode ? decode(stored) : (stored as T)\n }\n } catch {\n // sessionStorage unavailable — fall through to the default\n }\n return defaultValue\n })\n\n const setState = useCallback(\n (value: T) => {\n setStateInternal(value)\n try {\n const encoded = encode ? encode(value) : (value as string | undefined)\n if (encoded === undefined) {\n globalThis.sessionStorage.removeItem(key)\n } else {\n globalThis.sessionStorage.setItem(key, encoded)\n }\n } catch {\n // sessionStorage unavailable — state still works in-memory\n }\n },\n [key, encode],\n )\n\n return [state, setState]\n}\n\n/**\n * sessionStorage key backing the catalog search value (#27). Exported so tests\n * (and any tooling that needs to seed a \"returning user\" search) reference the\n * same key as the provider.\n */\nexport const SEARCH_STORAGE_KEY = 'app-catalog:search'\n"],"names":[],"mappings":";AAoCO,SAAS,sBAAyB;AAAA,EACvC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAA6D;AAC3D,QAAM,CAAC,OAAO,gBAAgB,IAAI,SAAY,MAAM;AAClD,QAAI;AAIF,YAAM,SAAS,WAAW,eAAe,QAAQ,GAAG;AACpD,UAAI,WAAW,MAAM;AACnB,eAAO,SAAS,OAAO,MAAM,IAAK;AAAA,MACpC;AAAA,IACF,QAAQ;AAAA,IAER;AACA,WAAO;AAAA,EACT,CAAC;AAED,QAAM,WAAW;AAAA,IACf,CAAC,UAAa;AACZ,uBAAiB,KAAK;AACtB,UAAI;AACF,cAAM,UAAU,SAAS,OAAO,KAAK,IAAK;AAC1C,YAAI,YAAY,QAAW;AACzB,qBAAW,eAAe,WAAW,GAAG;AAAA,QAC1C,OAAO;AACL,qBAAW,eAAe,QAAQ,KAAK,OAAO;AAAA,QAChD;AAAA,MACF,QAAQ;AAAA,MAER;AAAA,IACF;AAAA,IACA,CAAC,KAAK,MAAM;AAAA,EAAA;AAGd,SAAO,CAAC,OAAO,QAAQ;AACzB;AAOO,MAAM,qBAAqB;"}
@@ -0,0 +1,17 @@
1
+ import { Resource } from '@igstack/app-catalog-backend-core';
2
+ /**
3
+ * Two-step (prerequisite) access banner (#38 item D). When the viewed resource
4
+ * is a child whose ancestor(s) carry an access policy, the domain model says
5
+ * the user must obtain the parent's access FIRST. That chain is invisible
6
+ * otherwise, so surface it explicitly as ordered steps:
7
+ *
8
+ * Step 1 — get access to <parent> (e.g. AWS Console via IT Helpdesk)
9
+ * Step 2 — get access to THIS resource (contact its owner / access contacts)
10
+ *
11
+ * Renders nothing for root resources or children with no access-bearing parent.
12
+ * `onOpenParent` lets the user jump to the parent's full access instructions.
13
+ */
14
+ export declare function AccessPrerequisiteChain({ resource, onOpenParent, }: {
15
+ resource: Resource;
16
+ onOpenParent?: (parent: Resource) => void;
17
+ }): import("react/jsx-runtime").JSX.Element | null;
@@ -0,0 +1,61 @@
1
+ import { jsxs, jsx } from "react/jsx-runtime";
2
+ import { KeyRound, ArrowRight } from "lucide-react";
3
+ import { useAppCatalogContext } from "../../context/AppCatalogContext.js";
4
+ import { getAccessPrerequisiteChain } from "../../utils/resolveHelpers.js";
5
+ function AccessPrerequisiteChain({
6
+ resource,
7
+ onOpenParent
8
+ }) {
9
+ const { resources } = useAppCatalogContext();
10
+ const chain = getAccessPrerequisiteChain(resources, resource);
11
+ if (chain.length === 0) return null;
12
+ const steps = [...chain, resource];
13
+ return /* @__PURE__ */ jsxs(
14
+ "div",
15
+ {
16
+ className: "mt-6 rounded-lg border-[1.5px] border-primary/40 bg-primary/[0.03] p-4",
17
+ role: "note",
18
+ "aria-label": "Access requires prerequisite steps",
19
+ children: [
20
+ /* @__PURE__ */ jsxs("h3", { className: "mb-1 flex items-center gap-1.5 text-sm font-semibold text-foreground", children: [
21
+ /* @__PURE__ */ jsx(KeyRound, { className: "size-4 text-primary" }),
22
+ "Two-step access"
23
+ ] }),
24
+ /* @__PURE__ */ jsx("p", { className: "mb-3 text-xs text-muted-foreground", children: "This resource is nested — you need access to its parent first, then to the resource itself." }),
25
+ /* @__PURE__ */ jsx("ol", { className: "space-y-2", children: steps.map((r, i) => {
26
+ const isLast = i === steps.length - 1;
27
+ return /* @__PURE__ */ jsxs("li", { className: "flex items-start gap-2.5 text-sm", children: [
28
+ /* @__PURE__ */ jsx("span", { className: "mt-0.5 grid size-5 shrink-0 place-items-center rounded-full bg-primary text-[11px] font-bold text-primary-foreground", children: i + 1 }),
29
+ /* @__PURE__ */ jsxs("span", { className: "min-w-0", children: [
30
+ /* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: "Get access to " }),
31
+ isLast ? /* @__PURE__ */ jsx("span", { className: "font-semibold", children: "this resource" }) : onOpenParent ? /* @__PURE__ */ jsxs(
32
+ "button",
33
+ {
34
+ type: "button",
35
+ onClick: () => onOpenParent(r),
36
+ className: "inline-flex items-center gap-1 font-semibold text-primary hover:underline",
37
+ children: [
38
+ r.displayName,
39
+ /* @__PURE__ */ jsx(ArrowRight, { className: "size-3" })
40
+ ]
41
+ }
42
+ ) : /* @__PURE__ */ jsx("span", { className: "font-semibold", children: r.displayName }),
43
+ !isLast && /* @__PURE__ */ jsxs("span", { className: "text-muted-foreground", children: [
44
+ " ",
45
+ "(see its access instructions)"
46
+ ] }),
47
+ isLast && /* @__PURE__ */ jsxs("span", { className: "text-muted-foreground", children: [
48
+ " ",
49
+ "— details below."
50
+ ] })
51
+ ] })
52
+ ] }, r.slug);
53
+ }) })
54
+ ]
55
+ }
56
+ );
57
+ }
58
+ export {
59
+ AccessPrerequisiteChain
60
+ };
61
+ //# sourceMappingURL=AccessPrerequisiteChain.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AccessPrerequisiteChain.js","sources":["../../../../../../src/modules/appCatalog/ui/components/AccessPrerequisiteChain.tsx"],"sourcesContent":["import type { Resource } from '@igstack/app-catalog-backend-core'\nimport { ArrowRight, KeyRound } from 'lucide-react'\nimport { useAppCatalogContext } from '../../context/AppCatalogContext'\nimport { getAccessPrerequisiteChain } from '../../utils/resolveHelpers'\n\n/**\n * Two-step (prerequisite) access banner (#38 item D). When the viewed resource\n * is a child whose ancestor(s) carry an access policy, the domain model says\n * the user must obtain the parent's access FIRST. That chain is invisible\n * otherwise, so surface it explicitly as ordered steps:\n *\n * Step 1 — get access to <parent> (e.g. AWS Console via IT Helpdesk)\n * Step 2 — get access to THIS resource (contact its owner / access contacts)\n *\n * Renders nothing for root resources or children with no access-bearing parent.\n * `onOpenParent` lets the user jump to the parent's full access instructions.\n */\nexport function AccessPrerequisiteChain({\n resource,\n onOpenParent,\n}: {\n resource: Resource\n onOpenParent?: (parent: Resource) => void\n}) {\n const { resources } = useAppCatalogContext()\n const chain = getAccessPrerequisiteChain(resources, resource)\n if (chain.length === 0) return null\n\n // Steps = each prerequisite ancestor (root→nearest), then this resource last.\n const steps = [...chain, resource]\n\n return (\n <div\n className=\"mt-6 rounded-lg border-[1.5px] border-primary/40 bg-primary/[0.03] p-4\"\n role=\"note\"\n aria-label=\"Access requires prerequisite steps\"\n >\n <h3 className=\"mb-1 flex items-center gap-1.5 text-sm font-semibold text-foreground\">\n <KeyRound className=\"size-4 text-primary\" />\n Two-step access\n </h3>\n <p className=\"mb-3 text-xs text-muted-foreground\">\n This resource is nested — you need access to its parent first, then to\n the resource itself.\n </p>\n <ol className=\"space-y-2\">\n {steps.map((r, i) => {\n const isLast = i === steps.length - 1\n return (\n <li key={r.slug} className=\"flex items-start gap-2.5 text-sm\">\n <span className=\"mt-0.5 grid size-5 shrink-0 place-items-center rounded-full bg-primary text-[11px] font-bold text-primary-foreground\">\n {i + 1}\n </span>\n <span className=\"min-w-0\">\n <span className=\"text-muted-foreground\">Get access to </span>\n {isLast ? (\n <span className=\"font-semibold\">this resource</span>\n ) : onOpenParent ? (\n <button\n type=\"button\"\n onClick={() => onOpenParent(r)}\n className=\"inline-flex items-center gap-1 font-semibold text-primary hover:underline\"\n >\n {r.displayName}\n <ArrowRight className=\"size-3\" />\n </button>\n ) : (\n <span className=\"font-semibold\">{r.displayName}</span>\n )}\n {!isLast && (\n <span className=\"text-muted-foreground\">\n {' '}\n (see its access instructions)\n </span>\n )}\n {isLast && (\n <span className=\"text-muted-foreground\">\n {' '}\n — details below.\n </span>\n )}\n </span>\n </li>\n )\n })}\n </ol>\n </div>\n )\n}\n"],"names":[],"mappings":";;;;AAiBO,SAAS,wBAAwB;AAAA,EACtC;AAAA,EACA;AACF,GAGG;AACD,QAAM,EAAE,UAAA,IAAc,qBAAA;AACtB,QAAM,QAAQ,2BAA2B,WAAW,QAAQ;AAC5D,MAAI,MAAM,WAAW,EAAG,QAAO;AAG/B,QAAM,QAAQ,CAAC,GAAG,OAAO,QAAQ;AAEjC,SACE;AAAA,IAAC;AAAA,IAAA;AAAA,MACC,WAAU;AAAA,MACV,MAAK;AAAA,MACL,cAAW;AAAA,MAEX,UAAA;AAAA,QAAA,qBAAC,MAAA,EAAG,WAAU,wEACZ,UAAA;AAAA,UAAA,oBAAC,UAAA,EAAS,WAAU,sBAAA,CAAsB;AAAA,UAAE;AAAA,QAAA,GAE9C;AAAA,QACA,oBAAC,KAAA,EAAE,WAAU,sCAAqC,UAAA,+FAGlD;AAAA,QACA,oBAAC,QAAG,WAAU,aACX,gBAAM,IAAI,CAAC,GAAG,MAAM;AACnB,gBAAM,SAAS,MAAM,MAAM,SAAS;AACpC,iBACE,qBAAC,MAAA,EAAgB,WAAU,oCACzB,UAAA;AAAA,YAAA,oBAAC,QAAA,EAAK,WAAU,wHACb,UAAA,IAAI,GACP;AAAA,YACA,qBAAC,QAAA,EAAK,WAAU,WACd,UAAA;AAAA,cAAA,oBAAC,QAAA,EAAK,WAAU,yBAAwB,UAAA,kBAAc;AAAA,cACrD,SACC,oBAAC,QAAA,EAAK,WAAU,iBAAgB,UAAA,gBAAA,CAAa,IAC3C,eACF;AAAA,gBAAC;AAAA,gBAAA;AAAA,kBACC,MAAK;AAAA,kBACL,SAAS,MAAM,aAAa,CAAC;AAAA,kBAC7B,WAAU;AAAA,kBAET,UAAA;AAAA,oBAAA,EAAE;AAAA,oBACH,oBAAC,YAAA,EAAW,WAAU,SAAA,CAAS;AAAA,kBAAA;AAAA,gBAAA;AAAA,cAAA,IAGjC,oBAAC,QAAA,EAAK,WAAU,iBAAiB,YAAE,aAAY;AAAA,cAEhD,CAAC,UACA,qBAAC,QAAA,EAAK,WAAU,yBACb,UAAA;AAAA,gBAAA;AAAA,gBAAI;AAAA,cAAA,GAEP;AAAA,cAED,UACC,qBAAC,QAAA,EAAK,WAAU,yBACb,UAAA;AAAA,gBAAA;AAAA,gBAAI;AAAA,cAAA,EAAA,CAEP;AAAA,YAAA,EAAA,CAEJ;AAAA,UAAA,EAAA,GAhCO,EAAE,IAiCX;AAAA,QAEJ,CAAC,EAAA,CACH;AAAA,MAAA;AAAA,IAAA;AAAA,EAAA;AAGN;"}
@@ -90,8 +90,15 @@ function AccessRequestSection({
90
90
  }
91
91
  }, [accessRequest == null ? void 0 : accessRequest.requestPrompt, copyToClipboard]);
92
92
  if (!accessRequest) return null;
93
- return /* @__PURE__ */ jsxs("div", { className: "mt-6 space-y-4", children: [
94
- /* @__PURE__ */ jsx("h3", { className: "text-sm font-medium", children: "Access Request" }),
93
+ const isOpen = (approvalMethod == null ? void 0 : approvalMethod.type) === "noAccessRequired";
94
+ const hasWrittenSteps = Boolean(
95
+ accessRequest.requestPrompt || accessRequest.comments
96
+ );
97
+ const isUndocumented = (approvalMethod == null ? void 0 : approvalMethod.type) === "unknown" || (approvalMethod == null ? void 0 : approvalMethod.type) === "custom" && !hasWrittenSteps;
98
+ return /* @__PURE__ */ jsxs("div", { className: "mt-6 rounded-lg border-[1.5px] border-primary/40 bg-primary/[0.03] p-4 space-y-4", children: [
99
+ /* @__PURE__ */ jsx("h3", { className: "text-sm font-semibold text-foreground", children: "How to get access" }),
100
+ isOpen && /* @__PURE__ */ jsx("p", { className: "text-sm font-medium text-primary", children: "✓ Open to everyone — no request needed. Just open it." }),
101
+ isUndocumented && /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: "Access process not documented yet — contact the owner below to find out." }),
95
102
  approvalMethod && approvalMethod.type !== "custom" && /* @__PURE__ */ jsx("div", { className: "flex items-center gap-2", children: approvalMethod.type === "service" && approvalMethod.config.url ? /* @__PURE__ */ jsxs(Fragment, { children: [
96
103
  /* @__PURE__ */ jsxs(
97
104
  "a",
@@ -1 +1 @@
1
- {"version":3,"file":"AccessRequestSection.js","sources":["../../../../../../src/modules/appCatalog/ui/components/AccessRequestSection.tsx"],"sourcesContent":["import type {\n AppApprovalMethod,\n Resource,\n} from '@igstack/app-catalog-backend-core'\nimport { Bot, Check, Copy, ExternalLink, Settings, Users } from 'lucide-react'\nimport { useCallback, useEffect, useRef, useState } from 'react'\nimport ReactMarkdown from 'react-markdown'\nimport { Button } from '~/ui/button'\nimport {\n Accordion,\n AccordionContent,\n AccordionItem,\n AccordionTrigger,\n} from '~/ui/accordion'\nimport {\n Table,\n TableBody,\n TableCell,\n TableHead,\n TableHeader,\n TableRow,\n} from '~/ui/table'\nimport { MarkdownLink } from './MarkdownText'\nimport { PersonBadge } from './PersonBadge'\n\n// Constants\nconst COPY_FEEDBACK_DURATION = 2000\n\ninterface AccessRequestSectionProps {\n app: Resource\n approvalMethods: AppApprovalMethod[]\n}\n\n// Helper function for approval method icons\nfunction getApprovalMethodIcon(\n type: 'service' | 'personTeam' | 'custom' | 'noAccessRequired' | 'unknown',\n) {\n switch (type) {\n case 'service':\n return <Bot className=\"size-5 text-primary\" />\n case 'personTeam':\n return <Users className=\"size-5 text-primary\" />\n case 'custom':\n case 'noAccessRequired':\n case 'unknown':\n return <Settings className=\"size-5 text-primary\" />\n }\n}\n\n/**\n * Custom hook for handling copy-to-clipboard functionality with feedback\n * Includes proper cleanup to prevent memory leaks\n */\nfunction useCopyToClipboard() {\n const [copiedId, setCopiedId] = useState<string | null>(null)\n const [error, setError] = useState<string | null>(null)\n const timeoutRef = useRef<NodeJS.Timeout | null>(null)\n\n // Cleanup timeout on unmount\n useEffect(() => {\n return () => {\n if (timeoutRef.current) {\n clearTimeout(timeoutRef.current)\n }\n }\n }, [])\n\n const copyToClipboard = useCallback(async (text: string, id: string) => {\n // Clear existing timeout\n if (timeoutRef.current) {\n clearTimeout(timeoutRef.current)\n }\n\n try {\n await navigator.clipboard.writeText(text)\n setCopiedId(id)\n setError(null)\n\n // Set new timeout with cleanup\n timeoutRef.current = setTimeout(() => {\n setCopiedId(null)\n }, COPY_FEEDBACK_DURATION)\n } catch (err) {\n console.error('Failed to copy to clipboard:', err)\n setError('Failed to copy')\n\n // Clear error after duration\n timeoutRef.current = setTimeout(() => {\n setError(null)\n }, COPY_FEEDBACK_DURATION)\n }\n }, [])\n\n return { copiedId, error, copyToClipboard }\n}\n\n/**\n * Reusable copy button component with accessibility\n */\ninterface CopyButtonProps {\n onCopy: () => void\n isCopied: boolean\n ariaLabel: string\n className?: string\n}\n\nfunction CopyButton({\n onCopy,\n isCopied,\n ariaLabel,\n className,\n}: CopyButtonProps) {\n return (\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n onClick={onCopy}\n className={className}\n aria-label={ariaLabel}\n title={isCopied ? 'Copied!' : 'Copy to clipboard'}\n >\n {isCopied ? (\n <>\n <Check className=\"h-3.5 w-3.5 text-green-600\" />\n <span className=\"sr-only\" role=\"status\" aria-live=\"polite\">\n Copied to clipboard\n </span>\n </>\n ) : (\n <Copy className=\"h-3.5 w-3.5\" />\n )}\n </Button>\n )\n}\n\nexport function AccessRequestSection({\n app,\n approvalMethods,\n}: AccessRequestSectionProps) {\n const { copiedId, copyToClipboard } = useCopyToClipboard()\n const accessRequest = app.accessRequest\n const approvalMethod = approvalMethods.find(\n (m) => m.slug === accessRequest?.approvalMethodSlug,\n )\n\n const handleCopyPrompt = useCallback(() => {\n if (accessRequest?.requestPrompt) {\n copyToClipboard(accessRequest.requestPrompt, 'prompt')\n }\n }, [accessRequest?.requestPrompt, copyToClipboard])\n\n // Early return if no access request\n if (!accessRequest) return null\n\n return (\n <div className=\"mt-6 space-y-4\">\n <h3 className=\"text-sm font-medium\">Access Request</h3>\n\n {/* Approval Method */}\n {approvalMethod && approvalMethod.type !== 'custom' && (\n <div className=\"flex items-center gap-2\">\n {approvalMethod.type === 'service' && approvalMethod.config.url ? (\n <>\n <a\n href={approvalMethod.config.url}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"flex items-center gap-2 hover:text-primary transition-colors\"\n title={approvalMethod.config.url}\n >\n <div>{getApprovalMethodIcon(approvalMethod.type)}</div>\n <div className=\"font-medium\">{approvalMethod.displayName}</div>\n </a>\n <a\n href={approvalMethod.config.url}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"text-muted-foreground hover:text-primary\"\n title={approvalMethod.config.url}\n >\n <ExternalLink className=\"size-4\" />\n </a>\n </>\n ) : (\n <>\n <div>{getApprovalMethodIcon(approvalMethod.type)}</div>\n <div className=\"font-medium\">{approvalMethod.displayName}</div>\n </>\n )}\n </div>\n )}\n\n {/* Request Prompt - Inline */}\n {accessRequest.requestPrompt && (\n <div className=\"text-sm inline-flex items-center gap-2\">\n <span className=\"whitespace-nowrap shrink-0\">Request Prompt:</span>\n <span className=\"prose prose-sm inline [&>*]:inline [&>*]:m-0\">\n <ReactMarkdown components={{ a: MarkdownLink }}>\n {accessRequest.requestPrompt}\n </ReactMarkdown>\n </span>\n <CopyButton\n onCopy={handleCopyPrompt}\n isCopied={copiedId === 'prompt'}\n ariaLabel=\"Copy request prompt\"\n className=\"h-6 w-6 p-0 shrink-0\"\n />\n </div>\n )}\n\n {/* Comments */}\n {accessRequest.comments && (\n <div className=\"text-sm text-muted-foreground prose prose-sm max-w-none\">\n <ReactMarkdown components={{ a: MarkdownLink }}>\n {accessRequest.comments}\n </ReactMarkdown>\n </div>\n )}\n\n {/* Roles Table */}\n {accessRequest.roles && accessRequest.roles.length > 0 && (\n <div>\n <h4 className=\"mb-2 text-sm font-medium\">Available Roles</h4>\n <div className=\"rounded-lg border\">\n <Table>\n <TableHeader>\n <TableRow>\n <TableHead className=\"whitespace-nowrap\">Role</TableHead>\n <TableHead>Description</TableHead>\n </TableRow>\n </TableHeader>\n <TableBody>\n {accessRequest.roles.map((role, idx) => (\n <TableRow key={`${role.displayName}-${idx}`}>\n <TableCell className=\"font-medium whitespace-nowrap\">\n {role.displayName}\n </TableCell>\n <TableCell className=\"text-sm text-muted-foreground\">\n {role.description || '—'}\n {role.adminNotes && (\n <div className=\"mt-1 text-xs italic text-muted-foreground/80\">\n Note: {role.adminNotes}\n </div>\n )}\n </TableCell>\n </TableRow>\n ))}\n </TableBody>\n </Table>\n </div>\n </div>\n )}\n\n {/* Approvers */}\n {accessRequest.approverPersonSlugs &&\n accessRequest.approverPersonSlugs.length > 0 && (\n <div>\n <h4 className=\"mb-2 text-sm font-medium\">Approvers</h4>\n <div className=\"flex flex-wrap gap-2\">\n {accessRequest.approverPersonSlugs.map((slug) => (\n <PersonBadge key={slug} slug={slug} />\n ))}\n </div>\n </div>\n )}\n\n {/* Documentation URLs */}\n {accessRequest.urls && accessRequest.urls.length > 0 && (\n <div>\n <h4 className=\"mb-2 text-sm font-medium\">Documentation</h4>\n <div className=\"flex flex-col gap-1.5\">\n {accessRequest.urls.map((urlObj, idx) => (\n <a\n key={`${urlObj.url}-${idx}`}\n href={urlObj.url}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"text-sm text-muted-foreground hover:text-primary inline-flex items-center gap-1.5\"\n >\n {urlObj.label || urlObj.url}\n <ExternalLink className=\"size-3\" />\n </a>\n ))}\n </div>\n </div>\n )}\n\n {/* Post-Approval Instructions - Collapsible (secondary info) */}\n {accessRequest.postApprovalInstructions && (\n <Accordion type=\"single\" collapsible>\n <AccordionItem\n value=\"post-approval\"\n className=\"border rounded-lg px-4\"\n >\n <AccordionTrigger className=\"text-sm hover:no-underline py-3\">\n Post-Approval Instructions\n </AccordionTrigger>\n <AccordionContent className=\"pb-3\">\n <div className=\"text-sm text-muted-foreground prose prose-sm max-w-none\">\n <ReactMarkdown components={{ a: MarkdownLink }}>\n {accessRequest.postApprovalInstructions}\n </ReactMarkdown>\n </div>\n </AccordionContent>\n </AccordionItem>\n </Accordion>\n )}\n </div>\n )\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"AccessRequestSection.js","sources":["../../../../../../src/modules/appCatalog/ui/components/AccessRequestSection.tsx"],"sourcesContent":["import type {\n AppApprovalMethod,\n Resource,\n} from '@igstack/app-catalog-backend-core'\nimport { Bot, Check, Copy, ExternalLink, Settings, Users } from 'lucide-react'\nimport { useCallback, useEffect, useRef, useState } from 'react'\nimport ReactMarkdown from 'react-markdown'\nimport { Button } from '~/ui/button'\nimport {\n Accordion,\n AccordionContent,\n AccordionItem,\n AccordionTrigger,\n} from '~/ui/accordion'\nimport {\n Table,\n TableBody,\n TableCell,\n TableHead,\n TableHeader,\n TableRow,\n} from '~/ui/table'\nimport { MarkdownLink } from './MarkdownText'\nimport { PersonBadge } from './PersonBadge'\n\n// Constants\nconst COPY_FEEDBACK_DURATION = 2000\n\ninterface AccessRequestSectionProps {\n app: Resource\n approvalMethods: AppApprovalMethod[]\n}\n\n// Helper function for approval method icons\nfunction getApprovalMethodIcon(\n type: 'service' | 'personTeam' | 'custom' | 'noAccessRequired' | 'unknown',\n) {\n switch (type) {\n case 'service':\n return <Bot className=\"size-5 text-primary\" />\n case 'personTeam':\n return <Users className=\"size-5 text-primary\" />\n case 'custom':\n case 'noAccessRequired':\n case 'unknown':\n return <Settings className=\"size-5 text-primary\" />\n }\n}\n\n/**\n * Custom hook for handling copy-to-clipboard functionality with feedback\n * Includes proper cleanup to prevent memory leaks\n */\nfunction useCopyToClipboard() {\n const [copiedId, setCopiedId] = useState<string | null>(null)\n const [error, setError] = useState<string | null>(null)\n const timeoutRef = useRef<NodeJS.Timeout | null>(null)\n\n // Cleanup timeout on unmount\n useEffect(() => {\n return () => {\n if (timeoutRef.current) {\n clearTimeout(timeoutRef.current)\n }\n }\n }, [])\n\n const copyToClipboard = useCallback(async (text: string, id: string) => {\n // Clear existing timeout\n if (timeoutRef.current) {\n clearTimeout(timeoutRef.current)\n }\n\n try {\n await navigator.clipboard.writeText(text)\n setCopiedId(id)\n setError(null)\n\n // Set new timeout with cleanup\n timeoutRef.current = setTimeout(() => {\n setCopiedId(null)\n }, COPY_FEEDBACK_DURATION)\n } catch (err) {\n console.error('Failed to copy to clipboard:', err)\n setError('Failed to copy')\n\n // Clear error after duration\n timeoutRef.current = setTimeout(() => {\n setError(null)\n }, COPY_FEEDBACK_DURATION)\n }\n }, [])\n\n return { copiedId, error, copyToClipboard }\n}\n\n/**\n * Reusable copy button component with accessibility\n */\ninterface CopyButtonProps {\n onCopy: () => void\n isCopied: boolean\n ariaLabel: string\n className?: string\n}\n\nfunction CopyButton({\n onCopy,\n isCopied,\n ariaLabel,\n className,\n}: CopyButtonProps) {\n return (\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n onClick={onCopy}\n className={className}\n aria-label={ariaLabel}\n title={isCopied ? 'Copied!' : 'Copy to clipboard'}\n >\n {isCopied ? (\n <>\n <Check className=\"h-3.5 w-3.5 text-green-600\" />\n <span className=\"sr-only\" role=\"status\" aria-live=\"polite\">\n Copied to clipboard\n </span>\n </>\n ) : (\n <Copy className=\"h-3.5 w-3.5\" />\n )}\n </Button>\n )\n}\n\nexport function AccessRequestSection({\n app,\n approvalMethods,\n}: AccessRequestSectionProps) {\n const { copiedId, copyToClipboard } = useCopyToClipboard()\n const accessRequest = app.accessRequest\n const approvalMethod = approvalMethods.find(\n (m) => m.slug === accessRequest?.approvalMethodSlug,\n )\n\n const handleCopyPrompt = useCallback(() => {\n if (accessRequest?.requestPrompt) {\n copyToClipboard(accessRequest.requestPrompt, 'prompt')\n }\n }, [accessRequest?.requestPrompt, copyToClipboard])\n\n // Early return if no access request\n if (!accessRequest) return null\n\n // Access UX (#31): some methods carry no clickable target — `noAccessRequired`\n // (open), `unknown` (undocumented), and bare `custom` with no comments. The\n // old UI rendered nothing for `custom`, leaving a blank/dead section. Instead\n // surface an explicit, actionable line so the user is never stranded:\n // - open → \"No request needed — just open it.\"\n // - unknown / undocumented custom → route them to the owner (shown below).\n const isOpen = approvalMethod?.type === 'noAccessRequired'\n const hasWrittenSteps = Boolean(\n accessRequest.requestPrompt || accessRequest.comments,\n )\n const isUndocumented =\n approvalMethod?.type === 'unknown' ||\n (approvalMethod?.type === 'custom' && !hasWrittenSteps)\n\n return (\n <div className=\"mt-6 rounded-lg border-[1.5px] border-primary/40 bg-primary/[0.03] p-4 space-y-4\">\n <h3 className=\"text-sm font-semibold text-foreground\">\n How to get access\n </h3>\n\n {isOpen && (\n <p className=\"text-sm font-medium text-primary\">\n ✓ Open to everyone — no request needed. Just open it.\n </p>\n )}\n\n {isUndocumented && (\n <p className=\"text-sm text-muted-foreground\">\n Access process not documented yet — contact the owner below to find\n out.\n </p>\n )}\n\n {/* Approval Method */}\n {approvalMethod && approvalMethod.type !== 'custom' && (\n <div className=\"flex items-center gap-2\">\n {approvalMethod.type === 'service' && approvalMethod.config.url ? (\n <>\n <a\n href={approvalMethod.config.url}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"flex items-center gap-2 hover:text-primary transition-colors\"\n title={approvalMethod.config.url}\n >\n <div>{getApprovalMethodIcon(approvalMethod.type)}</div>\n <div className=\"font-medium\">{approvalMethod.displayName}</div>\n </a>\n <a\n href={approvalMethod.config.url}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"text-muted-foreground hover:text-primary\"\n title={approvalMethod.config.url}\n >\n <ExternalLink className=\"size-4\" />\n </a>\n </>\n ) : (\n <>\n <div>{getApprovalMethodIcon(approvalMethod.type)}</div>\n <div className=\"font-medium\">{approvalMethod.displayName}</div>\n </>\n )}\n </div>\n )}\n\n {/* Request Prompt - Inline */}\n {accessRequest.requestPrompt && (\n <div className=\"text-sm inline-flex items-center gap-2\">\n <span className=\"whitespace-nowrap shrink-0\">Request Prompt:</span>\n <span className=\"prose prose-sm inline [&>*]:inline [&>*]:m-0\">\n <ReactMarkdown components={{ a: MarkdownLink }}>\n {accessRequest.requestPrompt}\n </ReactMarkdown>\n </span>\n <CopyButton\n onCopy={handleCopyPrompt}\n isCopied={copiedId === 'prompt'}\n ariaLabel=\"Copy request prompt\"\n className=\"h-6 w-6 p-0 shrink-0\"\n />\n </div>\n )}\n\n {/* Comments */}\n {accessRequest.comments && (\n <div className=\"text-sm text-muted-foreground prose prose-sm max-w-none\">\n <ReactMarkdown components={{ a: MarkdownLink }}>\n {accessRequest.comments}\n </ReactMarkdown>\n </div>\n )}\n\n {/* Roles Table */}\n {accessRequest.roles && accessRequest.roles.length > 0 && (\n <div>\n <h4 className=\"mb-2 text-sm font-medium\">Available Roles</h4>\n <div className=\"rounded-lg border\">\n <Table>\n <TableHeader>\n <TableRow>\n <TableHead className=\"whitespace-nowrap\">Role</TableHead>\n <TableHead>Description</TableHead>\n </TableRow>\n </TableHeader>\n <TableBody>\n {accessRequest.roles.map((role, idx) => (\n <TableRow key={`${role.displayName}-${idx}`}>\n <TableCell className=\"font-medium whitespace-nowrap\">\n {role.displayName}\n </TableCell>\n <TableCell className=\"text-sm text-muted-foreground\">\n {role.description || '—'}\n {role.adminNotes && (\n <div className=\"mt-1 text-xs italic text-muted-foreground/80\">\n Note: {role.adminNotes}\n </div>\n )}\n </TableCell>\n </TableRow>\n ))}\n </TableBody>\n </Table>\n </div>\n </div>\n )}\n\n {/* Approvers */}\n {accessRequest.approverPersonSlugs &&\n accessRequest.approverPersonSlugs.length > 0 && (\n <div>\n <h4 className=\"mb-2 text-sm font-medium\">Approvers</h4>\n <div className=\"flex flex-wrap gap-2\">\n {accessRequest.approverPersonSlugs.map((slug) => (\n <PersonBadge key={slug} slug={slug} />\n ))}\n </div>\n </div>\n )}\n\n {/* Documentation URLs */}\n {accessRequest.urls && accessRequest.urls.length > 0 && (\n <div>\n <h4 className=\"mb-2 text-sm font-medium\">Documentation</h4>\n <div className=\"flex flex-col gap-1.5\">\n {accessRequest.urls.map((urlObj, idx) => (\n <a\n key={`${urlObj.url}-${idx}`}\n href={urlObj.url}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"text-sm text-muted-foreground hover:text-primary inline-flex items-center gap-1.5\"\n >\n {urlObj.label || urlObj.url}\n <ExternalLink className=\"size-3\" />\n </a>\n ))}\n </div>\n </div>\n )}\n\n {/* Post-Approval Instructions - Collapsible (secondary info) */}\n {accessRequest.postApprovalInstructions && (\n <Accordion type=\"single\" collapsible>\n <AccordionItem\n value=\"post-approval\"\n className=\"border rounded-lg px-4\"\n >\n <AccordionTrigger className=\"text-sm hover:no-underline py-3\">\n Post-Approval Instructions\n </AccordionTrigger>\n <AccordionContent className=\"pb-3\">\n <div className=\"text-sm text-muted-foreground prose prose-sm max-w-none\">\n <ReactMarkdown components={{ a: MarkdownLink }}>\n {accessRequest.postApprovalInstructions}\n </ReactMarkdown>\n </div>\n </AccordionContent>\n </AccordionItem>\n </Accordion>\n )}\n </div>\n )\n}\n"],"names":[],"mappings":"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"}
@@ -35,7 +35,7 @@ function OnboardingCard({
35
35
  }
36
36
  ),
37
37
  /* @__PURE__ */ jsxs("div", { className: "pr-10", children: [
38
- /* @__PURE__ */ jsx("h2", { className: "text-lg font-bold mb-2", children: title }),
38
+ /* @__PURE__ */ jsx("h2", { className: "font-serif text-lg font-semibold mb-2", children: title }),
39
39
  /* @__PURE__ */ jsx("p", { className: "text-muted-foreground mb-3 text-sm", children: description }),
40
40
  /* @__PURE__ */ jsxs("div", { className: "bg-muted/50 rounded-lg p-3", children: [
41
41
  /* @__PURE__ */ jsx("h3", { className: "font-semibold mb-2 text-xs", children: "Keyboard Navigation" }),
@@ -1 +1 @@
1
- {"version":3,"file":"OnboardingCard.js","sources":["../../../../../../src/modules/appCatalog/ui/components/OnboardingCard.tsx"],"sourcesContent":["import { ArrowDown, ArrowUp, X } from 'lucide-react'\nimport { useState } from 'react'\nimport type { ReactNode } from 'react'\nimport { Button } from '~/ui/button'\nimport { Card } from '~/ui/card'\n\nconst STORAGE_KEY = 'app-catalog-onboarding-dismissed'\n\nexport interface OnboardingCardProps {\n /** Main heading text */\n title?: ReactNode\n /** Description text below the title */\n description?: ReactNode\n}\n\n/**\n * First-time user onboarding card showing keyboard navigation instructions.\n * Dismissible and persists preference to localStorage.\n */\nexport function OnboardingCard({\n title = 'Welcome to App Catalog',\n description = 'Browse and discover applications available to you. Click on any app to view details, screenshots, and request access if needed.',\n}: OnboardingCardProps = {}) {\n // Initialize from localStorage to avoid hydration flash\n const [isDismissed, setIsDismissed] = useState(() => {\n // Check localStorage on mount\n if (typeof window !== 'undefined') {\n const dismissed = localStorage.getItem(STORAGE_KEY)\n return dismissed === 'true'\n }\n return true // Default to dismissed during SSR\n })\n\n const handleDismiss = () => {\n localStorage.setItem(STORAGE_KEY, 'true')\n setIsDismissed(true)\n }\n\n if (isDismissed) {\n return null\n }\n\n return (\n <Card className=\"p-4 mb-3 relative\">\n <Button\n variant=\"ghost\"\n size=\"icon\"\n className=\"absolute top-2 right-2\"\n onClick={handleDismiss}\n aria-label=\"Dismiss onboarding\"\n >\n <X className=\"h-4 w-4\" />\n </Button>\n\n <div className=\"pr-10\">\n <h2 className=\"text-lg font-bold mb-2\">{title}</h2>\n <p className=\"text-muted-foreground mb-3 text-sm\">{description}</p>\n\n <div className=\"bg-muted/50 rounded-lg p-3\">\n <h3 className=\"font-semibold mb-2 text-xs\">Keyboard Navigation</h3>\n <div className=\"flex flex-wrap items-center gap-x-4 gap-y-2 text-xs\">\n <div className=\"flex items-center gap-2\">\n <div className=\"flex items-center gap-1\">\n <kbd className=\"px-2 py-1 bg-background border rounded text-xs flex items-center\">\n <ArrowUp className=\"h-3 w-3\" />\n </kbd>\n <span className=\"text-muted-foreground\">/</span>\n <kbd className=\"px-2 py-1 bg-background border rounded text-xs flex items-center\">\n <ArrowDown className=\"h-3 w-3\" />\n </kbd>\n </div>\n <span className=\"text-muted-foreground\">Navigate apps</span>\n </div>\n <div className=\"flex items-center gap-2\">\n <kbd className=\"px-2 py-1 bg-background border rounded text-xs\">\n Enter\n </kbd>\n <span className=\"text-muted-foreground\">Open screenshots</span>\n </div>\n <div className=\"flex items-center gap-2\">\n <kbd className=\"px-2 py-1 bg-background border rounded text-xs\">\n Esc\n </kbd>\n <span className=\"text-muted-foreground\">Close details</span>\n </div>\n </div>\n </div>\n </div>\n </Card>\n )\n}\n"],"names":[],"mappings":";;;;;AAMA,MAAM,cAAc;AAab,SAAS,eAAe;AAAA,EAC7B,QAAQ;AAAA,EACR,cAAc;AAChB,IAAyB,IAAI;AAE3B,QAAM,CAAC,aAAa,cAAc,IAAI,SAAS,MAAM;AAEnD,QAAI,OAAO,WAAW,aAAa;AACjC,YAAM,YAAY,aAAa,QAAQ,WAAW;AAClD,aAAO,cAAc;AAAA,IACvB;AACA,WAAO;AAAA,EACT,CAAC;AAED,QAAM,gBAAgB,MAAM;AAC1B,iBAAa,QAAQ,aAAa,MAAM;AACxC,mBAAe,IAAI;AAAA,EACrB;AAEA,MAAI,aAAa;AACf,WAAO;AAAA,EACT;AAEA,SACE,qBAAC,MAAA,EAAK,WAAU,qBACd,UAAA;AAAA,IAAA;AAAA,MAAC;AAAA,MAAA;AAAA,QACC,SAAQ;AAAA,QACR,MAAK;AAAA,QACL,WAAU;AAAA,QACV,SAAS;AAAA,QACT,cAAW;AAAA,QAEX,UAAA,oBAAC,GAAA,EAAE,WAAU,UAAA,CAAU;AAAA,MAAA;AAAA,IAAA;AAAA,IAGzB,qBAAC,OAAA,EAAI,WAAU,SACb,UAAA;AAAA,MAAA,oBAAC,MAAA,EAAG,WAAU,0BAA0B,UAAA,OAAM;AAAA,MAC9C,oBAAC,KAAA,EAAE,WAAU,sCAAsC,UAAA,aAAY;AAAA,MAE/D,qBAAC,OAAA,EAAI,WAAU,8BACb,UAAA;AAAA,QAAA,oBAAC,MAAA,EAAG,WAAU,8BAA6B,UAAA,uBAAmB;AAAA,QAC9D,qBAAC,OAAA,EAAI,WAAU,uDACb,UAAA;AAAA,UAAA,qBAAC,OAAA,EAAI,WAAU,2BACb,UAAA;AAAA,YAAA,qBAAC,OAAA,EAAI,WAAU,2BACb,UAAA;AAAA,cAAA,oBAAC,SAAI,WAAU,oEACb,8BAAC,SAAA,EAAQ,WAAU,WAAU,EAAA,CAC/B;AAAA,cACA,oBAAC,QAAA,EAAK,WAAU,yBAAwB,UAAA,KAAC;AAAA,cACzC,oBAAC,SAAI,WAAU,oEACb,8BAAC,WAAA,EAAU,WAAU,WAAU,EAAA,CACjC;AAAA,YAAA,GACF;AAAA,YACA,oBAAC,QAAA,EAAK,WAAU,yBAAwB,UAAA,gBAAA,CAAa;AAAA,UAAA,GACvD;AAAA,UACA,qBAAC,OAAA,EAAI,WAAU,2BACb,UAAA;AAAA,YAAA,oBAAC,OAAA,EAAI,WAAU,kDAAiD,UAAA,SAEhE;AAAA,YACA,oBAAC,QAAA,EAAK,WAAU,yBAAwB,UAAA,mBAAA,CAAgB;AAAA,UAAA,GAC1D;AAAA,UACA,qBAAC,OAAA,EAAI,WAAU,2BACb,UAAA;AAAA,YAAA,oBAAC,OAAA,EAAI,WAAU,kDAAiD,UAAA,OAEhE;AAAA,YACA,oBAAC,QAAA,EAAK,WAAU,yBAAwB,UAAA,gBAAA,CAAa;AAAA,UAAA,EAAA,CACvD;AAAA,QAAA,EAAA,CACF;AAAA,MAAA,EAAA,CACF;AAAA,IAAA,EAAA,CACF;AAAA,EAAA,GACF;AAEJ;"}
1
+ {"version":3,"file":"OnboardingCard.js","sources":["../../../../../../src/modules/appCatalog/ui/components/OnboardingCard.tsx"],"sourcesContent":["import { ArrowDown, ArrowUp, X } from 'lucide-react'\nimport { useState } from 'react'\nimport type { ReactNode } from 'react'\nimport { Button } from '~/ui/button'\nimport { Card } from '~/ui/card'\n\nconst STORAGE_KEY = 'app-catalog-onboarding-dismissed'\n\nexport interface OnboardingCardProps {\n /** Main heading text */\n title?: ReactNode\n /** Description text below the title */\n description?: ReactNode\n}\n\n/**\n * First-time user onboarding card showing keyboard navigation instructions.\n * Dismissible and persists preference to localStorage.\n */\nexport function OnboardingCard({\n title = 'Welcome to App Catalog',\n description = 'Browse and discover applications available to you. Click on any app to view details, screenshots, and request access if needed.',\n}: OnboardingCardProps = {}) {\n // Initialize from localStorage to avoid hydration flash\n const [isDismissed, setIsDismissed] = useState(() => {\n // Check localStorage on mount\n if (typeof window !== 'undefined') {\n const dismissed = localStorage.getItem(STORAGE_KEY)\n return dismissed === 'true'\n }\n return true // Default to dismissed during SSR\n })\n\n const handleDismiss = () => {\n localStorage.setItem(STORAGE_KEY, 'true')\n setIsDismissed(true)\n }\n\n if (isDismissed) {\n return null\n }\n\n return (\n <Card className=\"p-4 mb-3 relative\">\n <Button\n variant=\"ghost\"\n size=\"icon\"\n className=\"absolute top-2 right-2\"\n onClick={handleDismiss}\n aria-label=\"Dismiss onboarding\"\n >\n <X className=\"h-4 w-4\" />\n </Button>\n\n <div className=\"pr-10\">\n <h2 className=\"font-serif text-lg font-semibold mb-2\">{title}</h2>\n <p className=\"text-muted-foreground mb-3 text-sm\">{description}</p>\n\n <div className=\"bg-muted/50 rounded-lg p-3\">\n <h3 className=\"font-semibold mb-2 text-xs\">Keyboard Navigation</h3>\n <div className=\"flex flex-wrap items-center gap-x-4 gap-y-2 text-xs\">\n <div className=\"flex items-center gap-2\">\n <div className=\"flex items-center gap-1\">\n <kbd className=\"px-2 py-1 bg-background border rounded text-xs flex items-center\">\n <ArrowUp className=\"h-3 w-3\" />\n </kbd>\n <span className=\"text-muted-foreground\">/</span>\n <kbd className=\"px-2 py-1 bg-background border rounded text-xs flex items-center\">\n <ArrowDown className=\"h-3 w-3\" />\n </kbd>\n </div>\n <span className=\"text-muted-foreground\">Navigate apps</span>\n </div>\n <div className=\"flex items-center gap-2\">\n <kbd className=\"px-2 py-1 bg-background border rounded text-xs\">\n Enter\n </kbd>\n <span className=\"text-muted-foreground\">Open screenshots</span>\n </div>\n <div className=\"flex items-center gap-2\">\n <kbd className=\"px-2 py-1 bg-background border rounded text-xs\">\n Esc\n </kbd>\n <span className=\"text-muted-foreground\">Close details</span>\n </div>\n </div>\n </div>\n </div>\n </Card>\n )\n}\n"],"names":[],"mappings":";;;;;AAMA,MAAM,cAAc;AAab,SAAS,eAAe;AAAA,EAC7B,QAAQ;AAAA,EACR,cAAc;AAChB,IAAyB,IAAI;AAE3B,QAAM,CAAC,aAAa,cAAc,IAAI,SAAS,MAAM;AAEnD,QAAI,OAAO,WAAW,aAAa;AACjC,YAAM,YAAY,aAAa,QAAQ,WAAW;AAClD,aAAO,cAAc;AAAA,IACvB;AACA,WAAO;AAAA,EACT,CAAC;AAED,QAAM,gBAAgB,MAAM;AAC1B,iBAAa,QAAQ,aAAa,MAAM;AACxC,mBAAe,IAAI;AAAA,EACrB;AAEA,MAAI,aAAa;AACf,WAAO;AAAA,EACT;AAEA,SACE,qBAAC,MAAA,EAAK,WAAU,qBACd,UAAA;AAAA,IAAA;AAAA,MAAC;AAAA,MAAA;AAAA,QACC,SAAQ;AAAA,QACR,MAAK;AAAA,QACL,WAAU;AAAA,QACV,SAAS;AAAA,QACT,cAAW;AAAA,QAEX,UAAA,oBAAC,GAAA,EAAE,WAAU,UAAA,CAAU;AAAA,MAAA;AAAA,IAAA;AAAA,IAGzB,qBAAC,OAAA,EAAI,WAAU,SACb,UAAA;AAAA,MAAA,oBAAC,MAAA,EAAG,WAAU,yCAAyC,UAAA,OAAM;AAAA,MAC7D,oBAAC,KAAA,EAAE,WAAU,sCAAsC,UAAA,aAAY;AAAA,MAE/D,qBAAC,OAAA,EAAI,WAAU,8BACb,UAAA;AAAA,QAAA,oBAAC,MAAA,EAAG,WAAU,8BAA6B,UAAA,uBAAmB;AAAA,QAC9D,qBAAC,OAAA,EAAI,WAAU,uDACb,UAAA;AAAA,UAAA,qBAAC,OAAA,EAAI,WAAU,2BACb,UAAA;AAAA,YAAA,qBAAC,OAAA,EAAI,WAAU,2BACb,UAAA;AAAA,cAAA,oBAAC,SAAI,WAAU,oEACb,8BAAC,SAAA,EAAQ,WAAU,WAAU,EAAA,CAC/B;AAAA,cACA,oBAAC,QAAA,EAAK,WAAU,yBAAwB,UAAA,KAAC;AAAA,cACzC,oBAAC,SAAI,WAAU,oEACb,8BAAC,WAAA,EAAU,WAAU,WAAU,EAAA,CACjC;AAAA,YAAA,GACF;AAAA,YACA,oBAAC,QAAA,EAAK,WAAU,yBAAwB,UAAA,gBAAA,CAAa;AAAA,UAAA,GACvD;AAAA,UACA,qBAAC,OAAA,EAAI,WAAU,2BACb,UAAA;AAAA,YAAA,oBAAC,OAAA,EAAI,WAAU,kDAAiD,UAAA,SAEhE;AAAA,YACA,oBAAC,QAAA,EAAK,WAAU,yBAAwB,UAAA,mBAAA,CAAgB;AAAA,UAAA,GAC1D;AAAA,UACA,qBAAC,OAAA,EAAI,WAAU,2BACb,UAAA;AAAA,YAAA,oBAAC,OAAA,EAAI,WAAU,kDAAiD,UAAA,OAEhE;AAAA,YACA,oBAAC,QAAA,EAAK,WAAU,yBAAwB,UAAA,gBAAA,CAAa;AAAA,UAAA,EAAA,CACvD;AAAA,QAAA,EAAA,CACF;AAAA,MAAA,EAAA,CACF;AAAA,IAAA,EAAA,CACF;AAAA,EAAA,GACF;AAEJ;"}
@@ -1,6 +1,14 @@
1
1
  import { Resource } from '@igstack/app-catalog-backend-core';
2
2
  interface SubResourcesSectionProps {
3
3
  subResources: Resource[];
4
+ /**
5
+ * Initial filter text (#38 item C). When the user reached this app by
6
+ * searching a term that matched a sub-resource, seed the sub-resource filter
7
+ * with that term so the matched child is revealed instead of buried in a
8
+ * long list (e.g. searching "biom" → open AWS Console → the biomarker
9
+ * account is pre-filtered). Only applied when it actually matches a child.
10
+ */
11
+ initialSearch?: string;
4
12
  }
5
- export declare function SubResourcesSection({ subResources, }: SubResourcesSectionProps): import("react/jsx-runtime").JSX.Element | null;
13
+ export declare function SubResourcesSection({ subResources, initialSearch, }: SubResourcesSectionProps): import("react/jsx-runtime").JSX.Element | null;
6
14
  export {};
@@ -1,6 +1,6 @@
1
1
  import { jsxs, jsx } from "react/jsx-runtime";
2
2
  import { Search, Check, Copy } from "lucide-react";
3
- import { useState, useMemo, useRef, useEffect, useCallback } from "react";
3
+ import { useMemo, useState, useRef, useEffect, useCallback } from "react";
4
4
  import { Badge } from "../../../../ui/badge.js";
5
5
  import { Input } from "../../../../ui/input.js";
6
6
  import { Table, TableHeader, TableRow, TableHead, TableBody, TableCell } from "../../../../ui/table.js";
@@ -72,10 +72,19 @@ function CopyAccountIdButton({ accountId }) {
72
72
  );
73
73
  }
74
74
  function SubResourcesSection({
75
- subResources
75
+ subResources,
76
+ initialSearch
76
77
  }) {
77
78
  const { groups } = useAppCatalogContext();
78
- const [search, setSearch] = useState("");
79
+ const seededSearch = useMemo(() => {
80
+ const q = initialSearch == null ? void 0 : initialSearch.trim().toLowerCase();
81
+ if (!q) return "";
82
+ const hit = subResources.some(
83
+ (sr) => sr.displayName.toLowerCase().includes(q) || (sr.aliases ?? []).some((a) => a.toLowerCase().includes(q))
84
+ );
85
+ return hit ? initialSearch.trim() : "";
86
+ }, [initialSearch, subResources]);
87
+ const [search, setSearch] = useState(seededSearch);
79
88
  const [tierFilter, setTierFilter] = useState("all");
80
89
  const uniqueTiers = useMemo(() => {
81
90
  const tiers = /* @__PURE__ */ new Set();
@@ -1 +1 @@
1
- {"version":3,"file":"SubResourcesSection.js","sources":["../../../../../../src/modules/appCatalog/ui/components/SubResourcesSection.tsx"],"sourcesContent":["import type { Resource } from '@igstack/app-catalog-backend-core'\nimport { Check, Copy, Search } from 'lucide-react'\nimport { useCallback, useEffect, useMemo, useRef, useState } from 'react'\nimport { Badge } from '~/ui/badge'\nimport { Input } from '~/ui/input'\nimport {\n Table,\n TableBody,\n TableCell,\n TableHead,\n TableHeader,\n TableRow,\n} from '~/ui/table'\nimport { PersonBadge } from './PersonBadge'\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from '~/ui/select'\nimport { useAppCatalogContext } from '~/modules/appCatalog'\nimport { getGroupBySlug } from '~/modules/appCatalog/utils/resolveHelpers'\nimport { markdownToPlainText } from '~/modules/appCatalog/utils/markdownToPlainText'\n\ninterface SubResourcesSectionProps {\n subResources: Resource[]\n}\n\nfunction getTierBadgeVariant(\n tierSlug: string,\n): 'default' | 'secondary' | 'destructive' | 'outline' {\n if (tierSlug === 'prod' || tierSlug === 'production') return 'destructive'\n if (tierSlug === 'dev' || tierSlug === 'staging') return 'secondary'\n if (tierSlug === 'preprod') return 'outline'\n if (tierSlug === 'sandbox') return 'outline'\n return 'outline'\n}\n\nfunction getTierBadgeClassName(tierSlug: string): string {\n if (tierSlug === 'preprod')\n return 'border-amber-400 bg-amber-100 text-amber-800 hover:bg-amber-200'\n if (tierSlug === 'sandbox')\n return 'border-gray-400 bg-gray-100 text-gray-700 hover:bg-gray-200'\n return ''\n}\n\nfunction getTierDisplayLabel(tierSlug: string): string {\n if (tierSlug === 'preprod') return 'Pre-Prod'\n if (tierSlug === 'sandbox') return 'Sandbox'\n if (tierSlug === 'prod' || tierSlug === 'production') return 'Prod'\n if (tierSlug === 'dev') return 'Dev'\n if (tierSlug === 'staging') return 'Staging'\n return tierSlug\n}\n\nfunction CopyAccountIdButton({ accountId }: { accountId: string }) {\n const [copied, setCopied] = useState(false)\n const timeoutRef = useRef<NodeJS.Timeout | null>(null)\n\n useEffect(() => {\n return () => {\n if (timeoutRef.current) clearTimeout(timeoutRef.current)\n }\n }, [])\n\n const handleCopy = useCallback(() => {\n void navigator.clipboard.writeText(accountId).then(() => {\n setCopied(true)\n timeoutRef.current = setTimeout(() => setCopied(false), 1500)\n })\n }, [accountId])\n\n return (\n <button\n type=\"button\"\n onClick={handleCopy}\n className=\"flex items-center gap-1 font-mono text-xs text-muted-foreground hover:text-foreground transition-colors group\"\n title=\"Copy account ID\"\n >\n <span>{accountId}</span>\n {copied ? (\n <Check className=\"size-3 text-green-500 shrink-0\" />\n ) : (\n <Copy className=\"size-3 shrink-0 opacity-0 group-hover:opacity-100 transition-opacity\" />\n )}\n </button>\n )\n}\n\nexport function SubResourcesSection({\n subResources,\n}: SubResourcesSectionProps) {\n const { groups } = useAppCatalogContext()\n const [search, setSearch] = useState('')\n const [tierFilter, setTierFilter] = useState<string>('all')\n\n const uniqueTiers = useMemo(() => {\n const tiers = new Set<string>()\n for (const sr of subResources) {\n if (sr.tier) tiers.add(sr.tier)\n }\n return [...tiers].sort()\n }, [subResources])\n\n const filtered = useMemo(() => {\n let result = subResources\n\n if (tierFilter !== 'all') {\n result = result.filter((sr) => sr.tier === tierFilter)\n }\n\n if (search.trim()) {\n const q = search.trim().toLowerCase()\n result = result.filter(\n (sr) =>\n sr.displayName.toLowerCase().includes(q) ||\n (sr.aliases ?? []).some((a) => a.toLowerCase().includes(q)) ||\n (sr.description?.toLowerCase().includes(q) ?? false) ||\n (\n (sr.extra as Record<string, unknown>).awsAccountId as\n | string\n | undefined\n )?.includes(q) === true,\n )\n }\n\n return result\n }, [subResources, search, tierFilter])\n\n if (subResources.length === 0) return null\n\n return (\n <div className=\"space-y-3\">\n <div className=\"flex items-center justify-between\">\n <div className=\"text-sm font-medium\">\n Sub-Resources ({filtered.length} of {subResources.length})\n </div>\n </div>\n\n {/* Filters */}\n <div className=\"flex gap-2\">\n <div className=\"relative flex-1\">\n <Search className=\"absolute left-2.5 top-2.5 size-4 text-muted-foreground\" />\n <Input\n placeholder=\"Search resources by name or alias...\"\n value={search}\n onChange={(e) => setSearch(e.target.value)}\n className=\"pl-9 h-9\"\n />\n </div>\n {uniqueTiers.length > 1 && (\n <Select value={tierFilter} onValueChange={setTierFilter}>\n <SelectTrigger className=\"w-[130px] h-9\">\n <SelectValue placeholder=\"All tiers\" />\n </SelectTrigger>\n <SelectContent>\n <SelectItem value=\"all\">All tiers</SelectItem>\n {uniqueTiers.map((tier) => (\n <SelectItem key={tier} value={tier}>\n {tier}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n )}\n </div>\n\n {/* Table */}\n <div className=\"rounded-lg border max-h-[400px] overflow-auto\">\n <Table>\n <TableHeader>\n <TableRow>\n <TableHead>Name</TableHead>\n <TableHead className=\"w-[80px]\">Tier</TableHead>\n <TableHead>Owner</TableHead>\n <TableHead>Access Contacts</TableHead>\n <TableHead className=\"w-[140px]\">AWS Account</TableHead>\n </TableRow>\n </TableHeader>\n <TableBody>\n {filtered.length === 0 ? (\n <TableRow>\n <TableCell\n colSpan={5}\n className=\"text-center text-muted-foreground py-8\"\n >\n No resources match your filters\n </TableCell>\n </TableRow>\n ) : (\n filtered.map((sr) => {\n // Resolve maintainer group members\n const maintainerMembers = (\n sr.accessMaintainerGroupSlugs ?? []\n ).flatMap((groupSlug) => {\n const group = getGroupBySlug(groups, groupSlug)\n return group?.memberSlugs ?? []\n })\n // Deduplicate\n const uniqueMaintainers = [...new Set(maintainerMembers)]\n\n return (\n <TableRow key={sr.slug}>\n <TableCell>\n <div className=\"font-medium text-sm\">\n {sr.displayName}\n </div>\n {(sr.aliases ?? []).length > 0 && (\n <div className=\"text-xs text-muted-foreground mt-0.5\">\n {(sr.aliases ?? []).join(', ')}\n </div>\n )}\n {sr.description && (\n <div className=\"text-xs text-muted-foreground mt-0.5\">\n {markdownToPlainText(sr.description)}\n </div>\n )}\n </TableCell>\n <TableCell>\n {sr.tier && (\n <Badge\n variant={getTierBadgeVariant(sr.tier)}\n className={`text-xs ${getTierBadgeClassName(sr.tier)}`}\n >\n {getTierDisplayLabel(sr.tier)}\n </Badge>\n )}\n </TableCell>\n <TableCell>\n {sr.ownerPersonSlug && (\n <PersonBadge slug={sr.ownerPersonSlug} />\n )}\n </TableCell>\n <TableCell>\n {uniqueMaintainers.length > 0 ? (\n <div className=\"flex flex-wrap gap-1\">\n {uniqueMaintainers.map((personSlug) => (\n <PersonBadge key={personSlug} slug={personSlug} />\n ))}\n </div>\n ) : (\n <span className=\"text-muted-foreground\">-</span>\n )}\n </TableCell>\n <TableCell>\n {(() => {\n const accountId = (\n sr.extra as Record<string, unknown> | null | undefined\n )?.awsAccountId as string | undefined\n return accountId ? (\n <CopyAccountIdButton accountId={accountId} />\n ) : (\n <span className=\"text-muted-foreground\">—</span>\n )\n })()}\n </TableCell>\n </TableRow>\n )\n })\n )}\n </TableBody>\n </Table>\n </div>\n </div>\n )\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"SubResourcesSection.js","sources":["../../../../../../src/modules/appCatalog/ui/components/SubResourcesSection.tsx"],"sourcesContent":["import type { Resource } from '@igstack/app-catalog-backend-core'\nimport { Check, Copy, Search } from 'lucide-react'\nimport { useCallback, useEffect, useMemo, useRef, useState } from 'react'\nimport { Badge } from '~/ui/badge'\nimport { Input } from '~/ui/input'\nimport {\n Table,\n TableBody,\n TableCell,\n TableHead,\n TableHeader,\n TableRow,\n} from '~/ui/table'\nimport { PersonBadge } from './PersonBadge'\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from '~/ui/select'\nimport { useAppCatalogContext } from '~/modules/appCatalog'\nimport { getGroupBySlug } from '~/modules/appCatalog/utils/resolveHelpers'\nimport { markdownToPlainText } from '~/modules/appCatalog/utils/markdownToPlainText'\n\ninterface SubResourcesSectionProps {\n subResources: Resource[]\n /**\n * Initial filter text (#38 item C). When the user reached this app by\n * searching a term that matched a sub-resource, seed the sub-resource filter\n * with that term so the matched child is revealed instead of buried in a\n * long list (e.g. searching \"biom\" → open AWS Console → the biomarker\n * account is pre-filtered). Only applied when it actually matches a child.\n */\n initialSearch?: string\n}\n\nfunction getTierBadgeVariant(\n tierSlug: string,\n): 'default' | 'secondary' | 'destructive' | 'outline' {\n if (tierSlug === 'prod' || tierSlug === 'production') return 'destructive'\n if (tierSlug === 'dev' || tierSlug === 'staging') return 'secondary'\n if (tierSlug === 'preprod') return 'outline'\n if (tierSlug === 'sandbox') return 'outline'\n return 'outline'\n}\n\nfunction getTierBadgeClassName(tierSlug: string): string {\n if (tierSlug === 'preprod')\n return 'border-amber-400 bg-amber-100 text-amber-800 hover:bg-amber-200'\n if (tierSlug === 'sandbox')\n return 'border-gray-400 bg-gray-100 text-gray-700 hover:bg-gray-200'\n return ''\n}\n\nfunction getTierDisplayLabel(tierSlug: string): string {\n if (tierSlug === 'preprod') return 'Pre-Prod'\n if (tierSlug === 'sandbox') return 'Sandbox'\n if (tierSlug === 'prod' || tierSlug === 'production') return 'Prod'\n if (tierSlug === 'dev') return 'Dev'\n if (tierSlug === 'staging') return 'Staging'\n return tierSlug\n}\n\nfunction CopyAccountIdButton({ accountId }: { accountId: string }) {\n const [copied, setCopied] = useState(false)\n const timeoutRef = useRef<NodeJS.Timeout | null>(null)\n\n useEffect(() => {\n return () => {\n if (timeoutRef.current) clearTimeout(timeoutRef.current)\n }\n }, [])\n\n const handleCopy = useCallback(() => {\n void navigator.clipboard.writeText(accountId).then(() => {\n setCopied(true)\n timeoutRef.current = setTimeout(() => setCopied(false), 1500)\n })\n }, [accountId])\n\n return (\n <button\n type=\"button\"\n onClick={handleCopy}\n className=\"flex items-center gap-1 font-mono text-xs text-muted-foreground hover:text-foreground transition-colors group\"\n title=\"Copy account ID\"\n >\n <span>{accountId}</span>\n {copied ? (\n <Check className=\"size-3 text-green-500 shrink-0\" />\n ) : (\n <Copy className=\"size-3 shrink-0 opacity-0 group-hover:opacity-100 transition-opacity\" />\n )}\n </button>\n )\n}\n\nexport function SubResourcesSection({\n subResources,\n initialSearch,\n}: SubResourcesSectionProps) {\n const { groups } = useAppCatalogContext()\n // Seed the filter with the incoming query ONLY if it matches a child, so we\n // reveal the matched sub-resource without hiding everything on a non-match.\n const seededSearch = useMemo(() => {\n const q = initialSearch?.trim().toLowerCase()\n if (!q) return ''\n const hit = subResources.some(\n (sr) =>\n sr.displayName.toLowerCase().includes(q) ||\n (sr.aliases ?? []).some((a) => a.toLowerCase().includes(q)),\n )\n return hit ? initialSearch!.trim() : ''\n }, [initialSearch, subResources])\n const [search, setSearch] = useState(seededSearch)\n const [tierFilter, setTierFilter] = useState<string>('all')\n\n const uniqueTiers = useMemo(() => {\n const tiers = new Set<string>()\n for (const sr of subResources) {\n if (sr.tier) tiers.add(sr.tier)\n }\n return [...tiers].sort()\n }, [subResources])\n\n const filtered = useMemo(() => {\n let result = subResources\n\n if (tierFilter !== 'all') {\n result = result.filter((sr) => sr.tier === tierFilter)\n }\n\n if (search.trim()) {\n const q = search.trim().toLowerCase()\n result = result.filter(\n (sr) =>\n sr.displayName.toLowerCase().includes(q) ||\n (sr.aliases ?? []).some((a) => a.toLowerCase().includes(q)) ||\n (sr.description?.toLowerCase().includes(q) ?? false) ||\n (\n (sr.extra as Record<string, unknown>).awsAccountId as\n | string\n | undefined\n )?.includes(q) === true,\n )\n }\n\n return result\n }, [subResources, search, tierFilter])\n\n if (subResources.length === 0) return null\n\n return (\n <div className=\"space-y-3\">\n <div className=\"flex items-center justify-between\">\n <div className=\"text-sm font-medium\">\n Sub-Resources ({filtered.length} of {subResources.length})\n </div>\n </div>\n\n {/* Filters */}\n <div className=\"flex gap-2\">\n <div className=\"relative flex-1\">\n <Search className=\"absolute left-2.5 top-2.5 size-4 text-muted-foreground\" />\n <Input\n placeholder=\"Search resources by name or alias...\"\n value={search}\n onChange={(e) => setSearch(e.target.value)}\n className=\"pl-9 h-9\"\n />\n </div>\n {uniqueTiers.length > 1 && (\n <Select value={tierFilter} onValueChange={setTierFilter}>\n <SelectTrigger className=\"w-[130px] h-9\">\n <SelectValue placeholder=\"All tiers\" />\n </SelectTrigger>\n <SelectContent>\n <SelectItem value=\"all\">All tiers</SelectItem>\n {uniqueTiers.map((tier) => (\n <SelectItem key={tier} value={tier}>\n {tier}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n )}\n </div>\n\n {/* Table */}\n <div className=\"rounded-lg border max-h-[400px] overflow-auto\">\n <Table>\n <TableHeader>\n <TableRow>\n <TableHead>Name</TableHead>\n <TableHead className=\"w-[80px]\">Tier</TableHead>\n <TableHead>Owner</TableHead>\n <TableHead>Access Contacts</TableHead>\n <TableHead className=\"w-[140px]\">AWS Account</TableHead>\n </TableRow>\n </TableHeader>\n <TableBody>\n {filtered.length === 0 ? (\n <TableRow>\n <TableCell\n colSpan={5}\n className=\"text-center text-muted-foreground py-8\"\n >\n No resources match your filters\n </TableCell>\n </TableRow>\n ) : (\n filtered.map((sr) => {\n // Resolve maintainer group members\n const maintainerMembers = (\n sr.accessMaintainerGroupSlugs ?? []\n ).flatMap((groupSlug) => {\n const group = getGroupBySlug(groups, groupSlug)\n return group?.memberSlugs ?? []\n })\n // Deduplicate\n const uniqueMaintainers = [...new Set(maintainerMembers)]\n\n return (\n <TableRow key={sr.slug}>\n <TableCell>\n <div className=\"font-medium text-sm\">\n {sr.displayName}\n </div>\n {(sr.aliases ?? []).length > 0 && (\n <div className=\"text-xs text-muted-foreground mt-0.5\">\n {(sr.aliases ?? []).join(', ')}\n </div>\n )}\n {sr.description && (\n <div className=\"text-xs text-muted-foreground mt-0.5\">\n {markdownToPlainText(sr.description)}\n </div>\n )}\n </TableCell>\n <TableCell>\n {sr.tier && (\n <Badge\n variant={getTierBadgeVariant(sr.tier)}\n className={`text-xs ${getTierBadgeClassName(sr.tier)}`}\n >\n {getTierDisplayLabel(sr.tier)}\n </Badge>\n )}\n </TableCell>\n <TableCell>\n {sr.ownerPersonSlug && (\n <PersonBadge slug={sr.ownerPersonSlug} />\n )}\n </TableCell>\n <TableCell>\n {uniqueMaintainers.length > 0 ? (\n <div className=\"flex flex-wrap gap-1\">\n {uniqueMaintainers.map((personSlug) => (\n <PersonBadge key={personSlug} slug={personSlug} />\n ))}\n </div>\n ) : (\n <span className=\"text-muted-foreground\">-</span>\n )}\n </TableCell>\n <TableCell>\n {(() => {\n const accountId = (\n sr.extra as Record<string, unknown> | null | undefined\n )?.awsAccountId as string | undefined\n return accountId ? (\n <CopyAccountIdButton accountId={accountId} />\n ) : (\n <span className=\"text-muted-foreground\">—</span>\n )\n })()}\n </TableCell>\n </TableRow>\n )\n })\n )}\n </TableBody>\n </Table>\n </div>\n </div>\n )\n}\n"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;AAqCA,SAAS,oBACP,UACqD;AACrD,MAAI,aAAa,UAAU,aAAa,aAAc,QAAO;AAC7D,MAAI,aAAa,SAAS,aAAa,UAAW,QAAO;AACzD,MAAI,aAAa,UAAW,QAAO;AACnC,MAAI,aAAa,UAAW,QAAO;AACnC,SAAO;AACT;AAEA,SAAS,sBAAsB,UAA0B;AACvD,MAAI,aAAa;AACf,WAAO;AACT,MAAI,aAAa;AACf,WAAO;AACT,SAAO;AACT;AAEA,SAAS,oBAAoB,UAA0B;AACrD,MAAI,aAAa,UAAW,QAAO;AACnC,MAAI,aAAa,UAAW,QAAO;AACnC,MAAI,aAAa,UAAU,aAAa,aAAc,QAAO;AAC7D,MAAI,aAAa,MAAO,QAAO;AAC/B,MAAI,aAAa,UAAW,QAAO;AACnC,SAAO;AACT;AAEA,SAAS,oBAAoB,EAAE,aAAoC;AACjE,QAAM,CAAC,QAAQ,SAAS,IAAI,SAAS,KAAK;AAC1C,QAAM,aAAa,OAA8B,IAAI;AAErD,YAAU,MAAM;AACd,WAAO,MAAM;AACX,UAAI,WAAW,QAAS,cAAa,WAAW,OAAO;AAAA,IACzD;AAAA,EACF,GAAG,CAAA,CAAE;AAEL,QAAM,aAAa,YAAY,MAAM;AACnC,SAAK,UAAU,UAAU,UAAU,SAAS,EAAE,KAAK,MAAM;AACvD,gBAAU,IAAI;AACd,iBAAW,UAAU,WAAW,MAAM,UAAU,KAAK,GAAG,IAAI;AAAA,IAC9D,CAAC;AAAA,EACH,GAAG,CAAC,SAAS,CAAC;AAEd,SACE;AAAA,IAAC;AAAA,IAAA;AAAA,MACC,MAAK;AAAA,MACL,SAAS;AAAA,MACT,WAAU;AAAA,MACV,OAAM;AAAA,MAEN,UAAA;AAAA,QAAA,oBAAC,UAAM,UAAA,UAAA,CAAU;AAAA,QAChB,6BACE,OAAA,EAAM,WAAU,kCAAiC,IAElD,oBAAC,MAAA,EAAK,WAAU,uEAAA,CAAuE;AAAA,MAAA;AAAA,IAAA;AAAA,EAAA;AAI/F;AAEO,SAAS,oBAAoB;AAAA,EAClC;AAAA,EACA;AACF,GAA6B;AAC3B,QAAM,EAAE,OAAA,IAAW,qBAAA;AAGnB,QAAM,eAAe,QAAQ,MAAM;AACjC,UAAM,IAAI,+CAAe,OAAO;AAChC,QAAI,CAAC,EAAG,QAAO;AACf,UAAM,MAAM,aAAa;AAAA,MACvB,CAAC,OACC,GAAG,YAAY,cAAc,SAAS,CAAC,MACtC,GAAG,WAAW,CAAA,GAAI,KAAK,CAAC,MAAM,EAAE,YAAA,EAAc,SAAS,CAAC,CAAC;AAAA,IAAA;AAE9D,WAAO,MAAM,cAAe,KAAA,IAAS;AAAA,EACvC,GAAG,CAAC,eAAe,YAAY,CAAC;AAChC,QAAM,CAAC,QAAQ,SAAS,IAAI,SAAS,YAAY;AACjD,QAAM,CAAC,YAAY,aAAa,IAAI,SAAiB,KAAK;AAE1D,QAAM,cAAc,QAAQ,MAAM;AAChC,UAAM,4BAAY,IAAA;AAClB,eAAW,MAAM,cAAc;AAC7B,UAAI,GAAG,KAAM,OAAM,IAAI,GAAG,IAAI;AAAA,IAChC;AACA,WAAO,CAAC,GAAG,KAAK,EAAE,KAAA;AAAA,EACpB,GAAG,CAAC,YAAY,CAAC;AAEjB,QAAM,WAAW,QAAQ,MAAM;AAC7B,QAAI,SAAS;AAEb,QAAI,eAAe,OAAO;AACxB,eAAS,OAAO,OAAO,CAAC,OAAO,GAAG,SAAS,UAAU;AAAA,IACvD;AAEA,QAAI,OAAO,QAAQ;AACjB,YAAM,IAAI,OAAO,KAAA,EAAO,YAAA;AACxB,eAAS,OAAO;AAAA,QACd,CAAC,OAAA;;AACC,oBAAG,YAAY,YAAA,EAAc,SAAS,CAAC,MACtC,GAAG,WAAW,CAAA,GAAI,KAAK,CAAC,MAAM,EAAE,YAAA,EAAc,SAAS,CAAC,CAAC,QACzD,QAAG,gBAAH,mBAAgB,cAAc,SAAS,OAAM,YAE3C,QAAG,MAAkC,iBAArC,mBAGA,SAAS,QAAO;AAAA;AAAA,MAAA;AAAA,IAEzB;AAEA,WAAO;AAAA,EACT,GAAG,CAAC,cAAc,QAAQ,UAAU,CAAC;AAErC,MAAI,aAAa,WAAW,EAAG,QAAO;AAEtC,SACE,qBAAC,OAAA,EAAI,WAAU,aACb,UAAA;AAAA,IAAA,oBAAC,SAAI,WAAU,qCACb,UAAA,qBAAC,OAAA,EAAI,WAAU,uBAAsB,UAAA;AAAA,MAAA;AAAA,MACnB,SAAS;AAAA,MAAO;AAAA,MAAK,aAAa;AAAA,MAAO;AAAA,IAAA,EAAA,CAC3D,EAAA,CACF;AAAA,IAGA,qBAAC,OAAA,EAAI,WAAU,cACb,UAAA;AAAA,MAAA,qBAAC,OAAA,EAAI,WAAU,mBACb,UAAA;AAAA,QAAA,oBAAC,QAAA,EAAO,WAAU,yDAAA,CAAyD;AAAA,QAC3E;AAAA,UAAC;AAAA,UAAA;AAAA,YACC,aAAY;AAAA,YACZ,OAAO;AAAA,YACP,UAAU,CAAC,MAAM,UAAU,EAAE,OAAO,KAAK;AAAA,YACzC,WAAU;AAAA,UAAA;AAAA,QAAA;AAAA,MACZ,GACF;AAAA,MACC,YAAY,SAAS,KACpB,qBAAC,UAAO,OAAO,YAAY,eAAe,eACxC,UAAA;AAAA,QAAA,oBAAC,iBAAc,WAAU,iBACvB,8BAAC,aAAA,EAAY,aAAY,aAAY,EAAA,CACvC;AAAA,6BACC,eAAA,EACC,UAAA;AAAA,UAAA,oBAAC,YAAA,EAAW,OAAM,OAAM,UAAA,aAAS;AAAA,UAChC,YAAY,IAAI,CAAC,SAChB,oBAAC,cAAsB,OAAO,MAC3B,UAAA,KAAA,GADc,IAEjB,CACD;AAAA,QAAA,EAAA,CACH;AAAA,MAAA,EAAA,CACF;AAAA,IAAA,GAEJ;AAAA,IAGA,oBAAC,OAAA,EAAI,WAAU,iDACb,+BAAC,OAAA,EACC,UAAA;AAAA,MAAA,oBAAC,aAAA,EACC,+BAAC,UAAA,EACC,UAAA;AAAA,QAAA,oBAAC,aAAU,UAAA,OAAA,CAAI;AAAA,QACf,oBAAC,WAAA,EAAU,WAAU,YAAW,UAAA,QAAI;AAAA,QACpC,oBAAC,aAAU,UAAA,QAAA,CAAK;AAAA,QAChB,oBAAC,aAAU,UAAA,kBAAA,CAAe;AAAA,QAC1B,oBAAC,WAAA,EAAU,WAAU,aAAY,UAAA,cAAA,CAAW;AAAA,MAAA,EAAA,CAC9C,EAAA,CACF;AAAA,0BACC,WAAA,EACE,UAAA,SAAS,WAAW,wBAClB,UAAA,EACC,UAAA;AAAA,QAAC;AAAA,QAAA;AAAA,UACC,SAAS;AAAA,UACT,WAAU;AAAA,UACX,UAAA;AAAA,QAAA;AAAA,MAAA,EAED,CACF,IAEA,SAAS,IAAI,CAAC,OAAO;AAEnB,cAAM,qBACJ,GAAG,8BAA8B,CAAA,GACjC,QAAQ,CAAC,cAAc;AACvB,gBAAM,QAAQ,eAAe,QAAQ,SAAS;AAC9C,kBAAO,+BAAO,gBAAe,CAAA;AAAA,QAC/B,CAAC;AAED,cAAM,oBAAoB,CAAC,GAAG,IAAI,IAAI,iBAAiB,CAAC;AAExD,oCACG,UAAA,EACC,UAAA;AAAA,UAAA,qBAAC,WAAA,EACC,UAAA;AAAA,YAAA,oBAAC,OAAA,EAAI,WAAU,uBACZ,UAAA,GAAG,aACN;AAAA,aACE,GAAG,WAAW,CAAA,GAAI,SAAS,KAC3B,oBAAC,OAAA,EAAI,WAAU,wCACX,cAAG,WAAW,CAAA,GAAI,KAAK,IAAI,GAC/B;AAAA,YAED,GAAG,eACF,oBAAC,OAAA,EAAI,WAAU,wCACZ,UAAA,oBAAoB,GAAG,WAAW,EAAA,CACrC;AAAA,UAAA,GAEJ;AAAA,UACA,oBAAC,WAAA,EACE,UAAA,GAAG,QACF;AAAA,YAAC;AAAA,YAAA;AAAA,cACC,SAAS,oBAAoB,GAAG,IAAI;AAAA,cACpC,WAAW,WAAW,sBAAsB,GAAG,IAAI,CAAC;AAAA,cAEnD,UAAA,oBAAoB,GAAG,IAAI;AAAA,YAAA;AAAA,UAAA,GAGlC;AAAA,UACA,oBAAC,aACE,UAAA,GAAG,uCACD,aAAA,EAAY,MAAM,GAAG,gBAAA,CAAiB,EAAA,CAE3C;AAAA,UACA,oBAAC,WAAA,EACE,UAAA,kBAAkB,SAAS,IAC1B,oBAAC,OAAA,EAAI,WAAU,wBACZ,UAAA,kBAAkB,IAAI,CAAC,mCACrB,aAAA,EAA6B,MAAM,WAAA,GAAlB,UAA8B,CACjD,EAAA,CACH,IAEA,oBAAC,QAAA,EAAK,WAAU,yBAAwB,UAAA,IAAA,CAAC,GAE7C;AAAA,UACA,oBAAC,aACG,WAAA,MAAM;;AACN,kBAAM,aACJ,QAAG,UAAH,mBACC;AACH,mBAAO,gCACJ,qBAAA,EAAoB,UAAA,CAAsB,IAE3C,oBAAC,QAAA,EAAK,WAAU,yBAAwB,UAAA,IAAA,CAAC;AAAA,UAE7C,KAAG,CACL;AAAA,QAAA,EAAA,GArDa,GAAG,IAsDlB;AAAA,MAEJ,CAAC,EAAA,CAEL;AAAA,IAAA,EAAA,CACF,EAAA,CACF;AAAA,EAAA,GACF;AAEJ;"}
@@ -2,6 +2,7 @@ import { jsx } from "react/jsx-runtime";
2
2
  import { useMemo, createContext, use } from "react";
3
3
  import { useAppCatalogContext } from "../../context/AppCatalogContext.js";
4
4
  import { useUrlSyncedState } from "../../hooks/useUrlSyncedState.js";
5
+ import { useSessionSyncedState, SEARCH_STORAGE_KEY } from "../../hooks/useSessionSyncedState.js";
5
6
  import { decodeFiltersParam, encodeFiltersParam } from "../../utils/filterHelpers.js";
6
7
  const AppCatalogFiltersContext = createContext(void 0);
7
8
  function AppCatalogFiltersProvider({
@@ -21,8 +22,8 @@ function AppCatalogFiltersProvider({
21
22
  decode: decodeFiltersParam,
22
23
  encode: encodeFiltersParam
23
24
  });
24
- const [searchValue, setSearchValue] = useUrlSyncedState({
25
- key: "q",
25
+ const [searchValue, setSearchValue] = useSessionSyncedState({
26
+ key: SEARCH_STORAGE_KEY,
26
27
  defaultValue: "",
27
28
  decode: (value) => value,
28
29
  encode: (value) => value === "" ? void 0 : value
@@ -1 +1 @@
1
- {"version":3,"file":"AppCatalogFiltersContext.js","sources":["../../../../../../src/modules/appCatalog/ui/context/AppCatalogFiltersContext.tsx"],"sourcesContent":["import type { ReactNode } from 'react'\nimport { createContext, use, useMemo } from 'react'\nimport { useAppCatalogContext } from '../../context/AppCatalogContext'\nimport { useUrlSyncedState } from '../../hooks/useUrlSyncedState'\nimport {\n decodeFiltersParam,\n encodeFiltersParam,\n} from '../../utils/filterHelpers'\n\n/**\n * Tag option for filter combobox\n */\nexport interface TagOption {\n /** Tag value (e.g., \"communication\") */\n value: string\n /** Full tag with prefix (e.g., \"category:communication\") */\n fullTag: string\n /** Display name for UI */\n displayName: string\n /** Description for UI */\n description: string\n}\n\n/**\n * Derived filter data computed from state and catalog\n */\nexport interface FilterData {\n /** Available tag options grouped by prefix */\n availableTagsByPrefix: Record<string, TagOption[]>\n /** Whether any filters are currently active */\n hasActiveFilters: boolean\n}\n\n/**\n * Filter state\n */\nexport interface AppCatalogFiltersState {\n /** Tag prefixes that can be filtered (from uiSettings) */\n filterableTagPrefixes: string[]\n /** Whether \"My Recent\" mode is active */\n recentMode: boolean\n /** Active tag filters (prefix -> value) */\n tagFilters: Record<string, string>\n /** Search query value */\n searchValue: string\n /** Whether to show deprecated apps (default: false) */\n showDeprecated: boolean\n}\n\n/**\n * Filter actions\n */\nexport interface AppCatalogFiltersActions {\n /** Enable/disable \"My Recent\" mode (clears tag filters when enabled) */\n setRecentMode: (enabled: boolean) => void\n /** Set a tag filter (clears recent mode) */\n setTagFilter: (prefix: string, value: string | undefined) => void\n /** Set search value */\n setSearchValue: (value: string) => void\n /** Set whether to show deprecated apps */\n setShowDeprecated: (show: boolean) => void\n /** Clear all filters (keeps search) */\n clearAllFilters: () => void\n}\n\nexport interface AppCatalogFiltersContextValue {\n state: AppCatalogFiltersState\n data: FilterData\n actions: AppCatalogFiltersActions\n}\n\nconst AppCatalogFiltersContext = createContext<\n AppCatalogFiltersContextValue | undefined\n>(undefined)\n\ninterface AppCatalogFiltersProviderProps {\n children: ReactNode\n /** Tag prefixes that can be filtered (from uiSettings) */\n filterableTagPrefixes: string[]\n}\n\nexport function AppCatalogFiltersProvider({\n children,\n filterableTagPrefixes,\n}: AppCatalogFiltersProviderProps) {\n const { tagsDefinitions } = useAppCatalogContext()\n\n // URL-synced state\n const [recentMode, setRecentMode] = useUrlSyncedState({\n key: 'recent',\n defaultValue: false,\n decode: (value) => value === '1',\n encode: (value) => (value ? '1' : undefined),\n })\n\n const [tagFilters, setTagFilters] = useUrlSyncedState({\n key: 'filters',\n defaultValue: {},\n decode: decodeFiltersParam,\n encode: encodeFiltersParam,\n })\n\n // Search value is synced to URL (key `q`) so it survives navigation —\n // e.g. auto-opening an app's detail page when the query narrows to one match\n // (the filters provider is mounted per-route and remounts on navigation).\n const [searchValue, setSearchValue] = useUrlSyncedState<string>({\n key: 'q',\n defaultValue: '',\n decode: (value) => value,\n encode: (value) => (value === '' ? undefined : value),\n })\n\n const [showDeprecated, setShowDeprecated] = useUrlSyncedState({\n key: 'deprecated',\n defaultValue: false,\n decode: (value) => value === '1',\n encode: (value) => (value ? '1' : undefined),\n })\n\n // Compute available tags by prefix\n const availableTagsByPrefix = useMemo(() => {\n const result: Record<string, TagOption[]> = {}\n\n filterableTagPrefixes.forEach((prefix) => {\n const definition = tagsDefinitions.find((def) => def.prefix === prefix)\n if (definition) {\n result[prefix] = definition.values.map((tagValue) => ({\n value: tagValue.value,\n fullTag: `${prefix}:${tagValue.value}`,\n displayName: tagValue.displayName,\n description: tagValue.description,\n }))\n }\n })\n\n return result\n }, [filterableTagPrefixes, tagsDefinitions])\n\n // Compute whether any filters are active\n const hasActiveFilters = useMemo(() => {\n return recentMode || Object.keys(tagFilters).length > 0\n }, [recentMode, tagFilters])\n\n // Actions\n const actions = useMemo<AppCatalogFiltersActions>(\n () => ({\n setRecentMode: (enabled: boolean) => {\n // Always clear tag filters when changing mode\n setTagFilters({})\n setRecentMode(enabled)\n },\n setTagFilter: (prefix: string, value: string | undefined) => {\n // Clear recent mode when setting tag filter\n if (value !== undefined) {\n setRecentMode(false)\n }\n\n // Update tag filters\n if (value === undefined) {\n // Remove filter\n const { [prefix]: _, ...rest } = tagFilters\n setTagFilters(rest)\n } else {\n // Add/update filter\n setTagFilters({ ...tagFilters, [prefix]: value })\n }\n },\n setSearchValue,\n setShowDeprecated,\n clearAllFilters: () => {\n setRecentMode(false)\n setTagFilters({})\n },\n }),\n [\n setRecentMode,\n setTagFilters,\n tagFilters,\n setSearchValue,\n setShowDeprecated,\n ],\n )\n\n const contextValue = useMemo<AppCatalogFiltersContextValue>(\n () => ({\n state: {\n filterableTagPrefixes,\n recentMode,\n tagFilters,\n searchValue,\n showDeprecated,\n },\n data: {\n availableTagsByPrefix,\n hasActiveFilters,\n },\n actions,\n }),\n [\n filterableTagPrefixes,\n recentMode,\n tagFilters,\n searchValue,\n showDeprecated,\n availableTagsByPrefix,\n hasActiveFilters,\n actions,\n ],\n )\n\n return (\n <AppCatalogFiltersContext value={contextValue}>\n {children}\n </AppCatalogFiltersContext>\n )\n}\n\nexport function useAppCatalogFilters(): AppCatalogFiltersContextValue {\n const context = use(AppCatalogFiltersContext)\n if (!context) {\n throw new Error(\n 'useAppCatalogFilters must be used within AppCatalogFiltersProvider',\n )\n }\n return context\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"AppCatalogFiltersContext.js","sources":["../../../../../../src/modules/appCatalog/ui/context/AppCatalogFiltersContext.tsx"],"sourcesContent":["import type { ReactNode } from 'react'\nimport { createContext, use, useMemo } from 'react'\nimport { useAppCatalogContext } from '../../context/AppCatalogContext'\nimport { useUrlSyncedState } from '../../hooks/useUrlSyncedState'\nimport {\n SEARCH_STORAGE_KEY,\n useSessionSyncedState,\n} from '../../hooks/useSessionSyncedState'\nimport {\n decodeFiltersParam,\n encodeFiltersParam,\n} from '../../utils/filterHelpers'\n\n/**\n * Tag option for filter combobox\n */\nexport interface TagOption {\n /** Tag value (e.g., \"communication\") */\n value: string\n /** Full tag with prefix (e.g., \"category:communication\") */\n fullTag: string\n /** Display name for UI */\n displayName: string\n /** Description for UI */\n description: string\n}\n\n/**\n * Derived filter data computed from state and catalog\n */\nexport interface FilterData {\n /** Available tag options grouped by prefix */\n availableTagsByPrefix: Record<string, TagOption[]>\n /** Whether any filters are currently active */\n hasActiveFilters: boolean\n}\n\n/**\n * Filter state\n */\nexport interface AppCatalogFiltersState {\n /** Tag prefixes that can be filtered (from uiSettings) */\n filterableTagPrefixes: string[]\n /** Whether \"My Recent\" mode is active */\n recentMode: boolean\n /** Active tag filters (prefix -> value) */\n tagFilters: Record<string, string>\n /** Search query value */\n searchValue: string\n /** Whether to show deprecated apps (default: false) */\n showDeprecated: boolean\n}\n\n/**\n * Filter actions\n */\nexport interface AppCatalogFiltersActions {\n /** Enable/disable \"My Recent\" mode (clears tag filters when enabled) */\n setRecentMode: (enabled: boolean) => void\n /** Set a tag filter (clears recent mode) */\n setTagFilter: (prefix: string, value: string | undefined) => void\n /** Set search value */\n setSearchValue: (value: string) => void\n /** Set whether to show deprecated apps */\n setShowDeprecated: (show: boolean) => void\n /** Clear all filters (keeps search) */\n clearAllFilters: () => void\n}\n\nexport interface AppCatalogFiltersContextValue {\n state: AppCatalogFiltersState\n data: FilterData\n actions: AppCatalogFiltersActions\n}\n\nconst AppCatalogFiltersContext = createContext<\n AppCatalogFiltersContextValue | undefined\n>(undefined)\n\ninterface AppCatalogFiltersProviderProps {\n children: ReactNode\n /** Tag prefixes that can be filtered (from uiSettings) */\n filterableTagPrefixes: string[]\n}\n\nexport function AppCatalogFiltersProvider({\n children,\n filterableTagPrefixes,\n}: AppCatalogFiltersProviderProps) {\n const { tagsDefinitions } = useAppCatalogContext()\n\n // URL-synced state\n const [recentMode, setRecentMode] = useUrlSyncedState({\n key: 'recent',\n defaultValue: false,\n decode: (value) => value === '1',\n encode: (value) => (value ? '1' : undefined),\n })\n\n const [tagFilters, setTagFilters] = useUrlSyncedState({\n key: 'filters',\n defaultValue: {},\n decode: decodeFiltersParam,\n encode: encodeFiltersParam,\n })\n\n // Search value persists in sessionStorage (not the URL) so it survives the\n // per-route remount of this provider — e.g. auto-opening an app's detail page\n // when the query narrows to one match (#10) — WITHOUT leaking `?q=` into every\n // shared/bookmarked app link (#27). The synchronous store read/write also\n // removes the effect-ordering race the URL sync had (#10).\n const [searchValue, setSearchValue] = useSessionSyncedState<string>({\n key: SEARCH_STORAGE_KEY,\n defaultValue: '',\n decode: (value) => value,\n encode: (value) => (value === '' ? undefined : value),\n })\n\n const [showDeprecated, setShowDeprecated] = useUrlSyncedState({\n key: 'deprecated',\n defaultValue: false,\n decode: (value) => value === '1',\n encode: (value) => (value ? '1' : undefined),\n })\n\n // Compute available tags by prefix\n const availableTagsByPrefix = useMemo(() => {\n const result: Record<string, TagOption[]> = {}\n\n filterableTagPrefixes.forEach((prefix) => {\n const definition = tagsDefinitions.find((def) => def.prefix === prefix)\n if (definition) {\n result[prefix] = definition.values.map((tagValue) => ({\n value: tagValue.value,\n fullTag: `${prefix}:${tagValue.value}`,\n displayName: tagValue.displayName,\n description: tagValue.description,\n }))\n }\n })\n\n return result\n }, [filterableTagPrefixes, tagsDefinitions])\n\n // Compute whether any filters are active\n const hasActiveFilters = useMemo(() => {\n return recentMode || Object.keys(tagFilters).length > 0\n }, [recentMode, tagFilters])\n\n // Actions\n const actions = useMemo<AppCatalogFiltersActions>(\n () => ({\n setRecentMode: (enabled: boolean) => {\n // Always clear tag filters when changing mode\n setTagFilters({})\n setRecentMode(enabled)\n },\n setTagFilter: (prefix: string, value: string | undefined) => {\n // Clear recent mode when setting tag filter\n if (value !== undefined) {\n setRecentMode(false)\n }\n\n // Update tag filters\n if (value === undefined) {\n // Remove filter\n const { [prefix]: _, ...rest } = tagFilters\n setTagFilters(rest)\n } else {\n // Add/update filter\n setTagFilters({ ...tagFilters, [prefix]: value })\n }\n },\n setSearchValue,\n setShowDeprecated,\n clearAllFilters: () => {\n setRecentMode(false)\n setTagFilters({})\n },\n }),\n [\n setRecentMode,\n setTagFilters,\n tagFilters,\n setSearchValue,\n setShowDeprecated,\n ],\n )\n\n const contextValue = useMemo<AppCatalogFiltersContextValue>(\n () => ({\n state: {\n filterableTagPrefixes,\n recentMode,\n tagFilters,\n searchValue,\n showDeprecated,\n },\n data: {\n availableTagsByPrefix,\n hasActiveFilters,\n },\n actions,\n }),\n [\n filterableTagPrefixes,\n recentMode,\n tagFilters,\n searchValue,\n showDeprecated,\n availableTagsByPrefix,\n hasActiveFilters,\n actions,\n ],\n )\n\n return (\n <AppCatalogFiltersContext value={contextValue}>\n {children}\n </AppCatalogFiltersContext>\n )\n}\n\nexport function useAppCatalogFilters(): AppCatalogFiltersContextValue {\n const context = use(AppCatalogFiltersContext)\n if (!context) {\n throw new Error(\n 'useAppCatalogFilters must be used within AppCatalogFiltersProvider',\n )\n }\n return context\n}\n"],"names":[],"mappings":"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"}
@@ -15,4 +15,9 @@ export interface AppCatalogGridProps {
15
15
  /** Called when the user closes the detail panel (Esc or X) — e.g. to navigate back to the catalog URL */
16
16
  onClosePanel?: () => void;
17
17
  }
18
+ export declare function AppDetails({ app, onAppClick, onClosePanel, }: {
19
+ app: Resource;
20
+ onAppClick?: (app: Resource) => void;
21
+ onClosePanel: () => void;
22
+ }): import("react/jsx-runtime").JSX.Element;
18
23
  export declare function AppCatalogGrid({ apps, selectedAppSlug, groupingDefinition, onAppClick, hasSearch, searchQuery, totalAppsCount, onClearFilters, onClosePanel, }: AppCatalogGridProps): import("react/jsx-runtime").JSX.Element;