@igstack/app-catalog-frontend-core 0.3.1-alpha-20260403020019 → 0.3.1-alpha-20260405015231

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 (53) hide show
  1. package/dist/esm/__tests__/integration/mock-backend/MockBackendConfigurer.d.ts +4 -1
  2. package/dist/esm/__tests__/integration/mock-backend/MockDb.d.ts +3 -1
  3. package/dist/esm/__tests__/integration/tools/AppDetailTools.d.ts +9 -0
  4. package/dist/esm/__tests__/integration/tools/CatalogTools.d.ts +4 -0
  5. package/dist/esm/__tests__/modules/appCatalog/utils/searchApps.test.d.ts +1 -0
  6. package/dist/esm/modules/appCatalog/context/AppCatalogContext.d.ts +4 -1
  7. package/dist/esm/modules/appCatalog/context/AppCatalogContext.js +6 -0
  8. package/dist/esm/modules/appCatalog/context/AppCatalogContext.js.map +1 -1
  9. package/dist/esm/modules/appCatalog/hooks/useUpdateApp.d.ts +24 -6
  10. package/dist/esm/modules/appCatalog/ui/components/AccessRequestSection.js +7 -57
  11. package/dist/esm/modules/appCatalog/ui/components/AccessRequestSection.js.map +1 -1
  12. package/dist/esm/modules/appCatalog/ui/components/PersonBadge.d.ts +5 -0
  13. package/dist/esm/modules/appCatalog/ui/components/PersonBadge.js +68 -0
  14. package/dist/esm/modules/appCatalog/ui/components/PersonBadge.js.map +1 -0
  15. package/dist/esm/modules/appCatalog/ui/components/SubResourcesSection.d.ts +6 -0
  16. package/dist/esm/modules/appCatalog/ui/components/SubResourcesSection.js +148 -0
  17. package/dist/esm/modules/appCatalog/ui/components/SubResourcesSection.js.map +1 -0
  18. package/dist/esm/modules/appCatalog/ui/components/TierVariantsSection.d.ts +6 -0
  19. package/dist/esm/modules/appCatalog/ui/components/TierVariantsSection.js +156 -0
  20. package/dist/esm/modules/appCatalog/ui/components/TierVariantsSection.js.map +1 -0
  21. package/dist/esm/modules/appCatalog/ui/grid/AppCatalogGrid.js +47 -8
  22. package/dist/esm/modules/appCatalog/ui/grid/AppCatalogGrid.js.map +1 -1
  23. package/dist/esm/modules/appCatalog/ui/pages/AppCatalogPage.js +4 -3
  24. package/dist/esm/modules/appCatalog/ui/pages/AppCatalogPage.js.map +1 -1
  25. package/dist/esm/modules/appCatalog/utils/resolveHelpers.d.ts +4 -0
  26. package/dist/esm/modules/appCatalog/utils/resolveHelpers.js +15 -0
  27. package/dist/esm/modules/appCatalog/utils/resolveHelpers.js.map +1 -0
  28. package/dist/esm/modules/appCatalog/utils/searchApps.d.ts +3 -3
  29. package/dist/esm/modules/appCatalog/utils/searchApps.js +24 -4
  30. package/dist/esm/modules/appCatalog/utils/searchApps.js.map +1 -1
  31. package/dist/esm/ui/select.js +138 -0
  32. package/dist/esm/ui/select.js.map +1 -0
  33. package/package.json +3 -3
  34. package/src/__tests__/integration/appCatalog.integration.test.ts +40 -1
  35. package/src/__tests__/integration/harness/given.tsx +1 -1
  36. package/src/__tests__/integration/mock-backend/MockBackendConfigurer.ts +15 -0
  37. package/src/__tests__/integration/mock-backend/MockDb.ts +12 -0
  38. package/src/__tests__/integration/mock-backend/magazines.ts +5 -9
  39. package/src/__tests__/integration/tools/AppDetailTools.ts +31 -0
  40. package/src/__tests__/integration/tools/CatalogTools.ts +12 -2
  41. package/src/__tests__/modules/appCatalog/ScreenshotPreview.test.tsx +3 -0
  42. package/src/__tests__/modules/appCatalog/utils/searchApps.test.ts +94 -0
  43. package/src/__tests__/modules/gallery/EscapeDismissal.integration.test.tsx +3 -0
  44. package/src/modules/appCatalog/context/AppCatalogContext.tsx +12 -0
  45. package/src/modules/appCatalog/ui/components/AccessRequestSection.tsx +17 -62
  46. package/src/modules/appCatalog/ui/components/AppDetailModal.tsx +26 -4
  47. package/src/modules/appCatalog/ui/components/PersonBadge.tsx +69 -0
  48. package/src/modules/appCatalog/ui/components/SubResourcesSection.tsx +214 -0
  49. package/src/modules/appCatalog/ui/components/TierVariantsSection.tsx +212 -0
  50. package/src/modules/appCatalog/ui/grid/AppCatalogGrid.tsx +71 -7
  51. package/src/modules/appCatalog/ui/pages/AppCatalogPage.tsx +4 -2
  52. package/src/modules/appCatalog/utils/resolveHelpers.ts +26 -0
  53. package/src/modules/appCatalog/utils/searchApps.ts +45 -6
@@ -1,4 +1,4 @@
1
- import { AppApprovalMethod, AppForCatalog, GroupingTagDefinition } from '@igstack/app-catalog-backend-core';
1
+ import { AppApprovalMethod, AppForCatalog, GroupingTagDefinition, SubResource } from '@igstack/app-catalog-backend-core';
2
2
  import { MockDb } from './MockDb.js';
3
3
  import { MockUserContext, UserConfig } from './MockUserContext.js';
4
4
  export declare class MockBackendConfigurer {
@@ -8,6 +8,9 @@ export declare class MockBackendConfigurer {
8
8
  withApp(overrides?: Partial<AppForCatalog>): AppForCatalog;
9
9
  withTag(overrides?: Partial<GroupingTagDefinition>): GroupingTagDefinition;
10
10
  withApprovalMethod(overrides?: Partial<AppApprovalMethod>): AppApprovalMethod;
11
+ withSubResource(overrides: Partial<SubResource> & {
12
+ appSlug: string;
13
+ }): SubResource;
11
14
  withUser(overrides: Partial<UserConfig>): void;
12
15
  }
13
16
  /** Reset the counter between tests */
@@ -1,12 +1,14 @@
1
- import { AppApprovalMethod, AppCatalogData, AppForCatalog, GroupingTagDefinition } from '@igstack/app-catalog-backend-core';
1
+ import { AppApprovalMethod, AppCatalogData, AppForCatalog, GroupingTagDefinition, SubResource } from '@igstack/app-catalog-backend-core';
2
2
  export declare class MockDb {
3
3
  apps: AppForCatalog[];
4
4
  tagsDefinitions: GroupingTagDefinition[];
5
5
  approvalMethods: AppApprovalMethod[];
6
+ subResources: SubResource[];
6
7
  upsertApp(app: AppForCatalog): void;
7
8
  getApps(): AppForCatalog[];
8
9
  getApp(slug: string): AppForCatalog;
9
10
  setTagDefinitions(defs: GroupingTagDefinition[]): void;
10
11
  setApprovalMethods(methods: AppApprovalMethod[]): void;
12
+ addSubResource(sr: SubResource): void;
11
13
  getAppCatalogData(): AppCatalogData;
12
14
  }
@@ -17,6 +17,15 @@ export declare class AppDetailTools {
17
17
  * Scrape all visible data from the app detail panel into a structured object.
18
18
  */
19
19
  getVisibleData(): AppVisibleData;
20
+ /**
21
+ * Get the sub-resources section data from the detail panel.
22
+ * Returns null if no sub-resources section is visible.
23
+ */
24
+ getSubResources(): {
25
+ total: number;
26
+ visible: number;
27
+ names: string[];
28
+ } | null;
20
29
  screenshots: {
21
30
  /**
22
31
  * Click the screenshot preview to open the gallery modal.
@@ -26,4 +26,8 @@ export declare class CatalogTools {
26
26
  * Whether the onboarding/welcome card is visible.
27
27
  */
28
28
  isOnboardingVisible(): boolean;
29
+ /**
30
+ * Get the main catalog table (first table on page, skipping sub-resource tables in detail panel).
31
+ */
32
+ private getCatalogTable;
29
33
  }
@@ -1,10 +1,13 @@
1
- import { AppApprovalMethod, AppForCatalog, AppVersionInfo, GroupingTagDefinition } from '@igstack/app-catalog-backend-core';
1
+ import { AppApprovalMethod, AppForCatalog, AppVersionInfo, Group, GroupingTagDefinition, Person, SubResource } from '@igstack/app-catalog-backend-core';
2
2
  import { ReactNode } from 'react';
3
3
  export interface AppCatalogContextIface {
4
4
  apps: AppForCatalog[];
5
5
  isLoadingApps: boolean;
6
6
  tagsDefinitions: GroupingTagDefinition[];
7
7
  approvalMethods: AppApprovalMethod[];
8
+ persons: Person[];
9
+ groups: Group[];
10
+ subResources?: SubResource[];
8
11
  versions?: AppVersionInfo;
9
12
  }
10
13
  export declare const AppCatalogContext: import('react').Context<AppCatalogContextIface | undefined>;
@@ -27,6 +27,9 @@ function AppCatalogProvider({ children }) {
27
27
  isLoadingApps,
28
28
  tagsDefinitions: (data == null ? void 0 : data.tagsDefinitions) ?? [],
29
29
  approvalMethods: (data == null ? void 0 : data.approvalMethods) ?? [],
30
+ persons: (data == null ? void 0 : data.persons) ?? [],
31
+ groups: (data == null ? void 0 : data.groups) ?? [],
32
+ subResources: (data == null ? void 0 : data.subResources) ?? [],
30
33
  versions: {
31
34
  ...data == null ? void 0 : data.versions,
32
35
  ...uiSettings.frontendBuildId && {
@@ -40,6 +43,9 @@ function AppCatalogProvider({ children }) {
40
43
  data == null ? void 0 : data.approvalMethods,
41
44
  data == null ? void 0 : data.apps,
42
45
  data == null ? void 0 : data.tagsDefinitions,
46
+ data == null ? void 0 : data.persons,
47
+ data == null ? void 0 : data.groups,
48
+ data == null ? void 0 : data.subResources,
43
49
  data == null ? void 0 : data.versions,
44
50
  uiSettings.frontendBuildId,
45
51
  isLoadingApps
@@ -1 +1 @@
1
- {"version":3,"file":"AppCatalogContext.js","sources":["../../../../../src/modules/appCatalog/context/AppCatalogContext.tsx"],"sourcesContent":["import type {\n AppApprovalMethod,\n AppForCatalog,\n AppVersionInfo,\n GroupingTagDefinition,\n} from '@igstack/app-catalog-backend-core'\nimport { useQuery } from '@tanstack/react-query'\nimport type { ReactNode } from 'react'\nimport { createContext, use, useEffect, useMemo } from 'react'\nimport { ApiQueryMagazineAppCatalog } from '~/modules/appCatalog'\nimport { useUiSettings } from '~/context/UiSettingsContext'\n\nexport interface AppCatalogContextIface {\n apps: AppForCatalog[]\n isLoadingApps: boolean\n tagsDefinitions: GroupingTagDefinition[]\n approvalMethods: AppApprovalMethod[]\n versions?: AppVersionInfo\n}\n\nexport const AppCatalogContext = createContext<\n AppCatalogContextIface | undefined\n>(undefined)\n\ninterface AppCatalogProviderProps {\n children: ReactNode\n}\n\nexport function AppCatalogProvider({ children }: AppCatalogProviderProps) {\n const { data, isLoading: isLoadingApps } = useQuery(\n ApiQueryMagazineAppCatalog.getAppCatalog(),\n )\n const uiSettings = useUiSettings()\n\n const contextValue = useMemo<AppCatalogContextIface>(\n () => ({\n apps: data?.apps ?? [],\n isLoadingApps,\n tagsDefinitions: data?.tagsDefinitions ?? [],\n approvalMethods: data?.approvalMethods ?? [],\n versions: {\n ...data?.versions,\n ...(uiSettings.frontendBuildId && {\n frontend: {\n displayName:\n uiSettings.frontendBuildId === 'local'\n ? 'local'\n : `#${uiSettings.frontendBuildId}`,\n },\n }),\n },\n }),\n [\n data?.approvalMethods,\n data?.apps,\n data?.tagsDefinitions,\n data?.versions,\n uiSettings.frontendBuildId,\n isLoadingApps,\n ],\n )\n\n // Update document title based on backend version\n useEffect(() => {\n if (data?.versions?.backend?.displayName === 'local') {\n document.title = 'Local'\n } else {\n document.title = 'App Catalog'\n }\n }, [data?.versions?.backend?.displayName])\n\n return <AppCatalogContext value={contextValue}>{children}</AppCatalogContext>\n}\n\nexport function useAppCatalogContext(): AppCatalogContextIface {\n const context = use(AppCatalogContext)\n if (!context) {\n throw new Error(\n 'useAppCatalogContext must be used within AppCatalogProvider',\n )\n }\n return context\n}\n"],"names":["_b","_a"],"mappings":";;;;;;;;;;;;;;;;AAoBO,MAAM,oBAAoB,cAE/B,MAAS;AAMJ,SAAS,mBAAmB,EAAE,YAAqC;;AACxE,QAAM,EAAE,MAAM,WAAW,cAAA,IAAkB;AAAA,IACzC,2BAA2B,cAAA;AAAA,EAAc;AAE3C,QAAM,aAAa,cAAA;AAEnB,QAAM,eAAe;AAAA,IACnB,OAAO;AAAA,MACL,OAAM,6BAAM,SAAQ,CAAA;AAAA,MACpB;AAAA,MACA,kBAAiB,6BAAM,oBAAmB,CAAA;AAAA,MAC1C,kBAAiB,6BAAM,oBAAmB,CAAA;AAAA,MAC1C,UAAU;AAAA,QACR,GAAG,6BAAM;AAAA,QACT,GAAI,WAAW,mBAAmB;AAAA,UAChC,UAAU;AAAA,YACR,aACE,WAAW,oBAAoB,UAC3B,UACA,IAAI,WAAW,eAAe;AAAA,UAAA;AAAA,QACtC;AAAA,MACF;AAAA,IACF;AAAA,IAEF;AAAA,MACE,6BAAM;AAAA,MACN,6BAAM;AAAA,MACN,6BAAM;AAAA,MACN,6BAAM;AAAA,MACN,WAAW;AAAA,MACX;AAAA,IAAA;AAAA,EACF;AAIF,YAAU,MAAM;;AACd,UAAIA,OAAAC,MAAA,6BAAM,aAAN,gBAAAA,IAAgB,YAAhB,gBAAAD,IAAyB,iBAAgB,SAAS;AACpD,eAAS,QAAQ;AAAA,IACnB,OAAO;AACL,eAAS,QAAQ;AAAA,IACnB;AAAA,EACF,GAAG,EAAC,wCAAM,aAAN,mBAAgB,YAAhB,mBAAyB,WAAW,CAAC;AAEzC,SAAO,oBAAC,mBAAA,EAAkB,OAAO,cAAe,SAAA,CAAS;AAC3D;AAEO,SAAS,uBAA+C;AAC7D,QAAM,UAAU,IAAI,iBAAiB;AACrC,MAAI,CAAC,SAAS;AACZ,UAAM,IAAI;AAAA,MACR;AAAA,IAAA;AAAA,EAEJ;AACA,SAAO;AACT;"}
1
+ {"version":3,"file":"AppCatalogContext.js","sources":["../../../../../src/modules/appCatalog/context/AppCatalogContext.tsx"],"sourcesContent":["import type {\n AppApprovalMethod,\n AppForCatalog,\n AppVersionInfo,\n Group,\n GroupingTagDefinition,\n Person,\n SubResource,\n} from '@igstack/app-catalog-backend-core'\nimport { useQuery } from '@tanstack/react-query'\nimport type { ReactNode } from 'react'\nimport { createContext, use, useEffect, useMemo } from 'react'\nimport { ApiQueryMagazineAppCatalog } from '~/modules/appCatalog'\nimport { useUiSettings } from '~/context/UiSettingsContext'\n\nexport interface AppCatalogContextIface {\n apps: AppForCatalog[]\n isLoadingApps: boolean\n tagsDefinitions: GroupingTagDefinition[]\n approvalMethods: AppApprovalMethod[]\n persons: Person[]\n groups: Group[]\n subResources?: SubResource[]\n versions?: AppVersionInfo\n}\n\nexport const AppCatalogContext = createContext<\n AppCatalogContextIface | undefined\n>(undefined)\n\ninterface AppCatalogProviderProps {\n children: ReactNode\n}\n\nexport function AppCatalogProvider({ children }: AppCatalogProviderProps) {\n const { data, isLoading: isLoadingApps } = useQuery(\n ApiQueryMagazineAppCatalog.getAppCatalog(),\n )\n const uiSettings = useUiSettings()\n\n const contextValue = useMemo<AppCatalogContextIface>(\n () => ({\n apps: data?.apps ?? [],\n isLoadingApps,\n tagsDefinitions: data?.tagsDefinitions ?? [],\n approvalMethods: data?.approvalMethods ?? [],\n persons: data?.persons ?? [],\n groups: data?.groups ?? [],\n subResources: data?.subResources ?? [],\n versions: {\n ...data?.versions,\n ...(uiSettings.frontendBuildId && {\n frontend: {\n displayName:\n uiSettings.frontendBuildId === 'local'\n ? 'local'\n : `#${uiSettings.frontendBuildId}`,\n },\n }),\n },\n }),\n [\n data?.approvalMethods,\n data?.apps,\n data?.tagsDefinitions,\n data?.persons,\n data?.groups,\n data?.subResources,\n data?.versions,\n uiSettings.frontendBuildId,\n isLoadingApps,\n ],\n )\n\n // Update document title based on backend version\n useEffect(() => {\n if (data?.versions?.backend?.displayName === 'local') {\n document.title = 'Local'\n } else {\n document.title = 'App Catalog'\n }\n }, [data?.versions?.backend?.displayName])\n\n return <AppCatalogContext value={contextValue}>{children}</AppCatalogContext>\n}\n\nexport function useAppCatalogContext(): AppCatalogContextIface {\n const context = use(AppCatalogContext)\n if (!context) {\n throw new Error(\n 'useAppCatalogContext must be used within AppCatalogProvider',\n )\n }\n return context\n}\n"],"names":["_b","_a"],"mappings":";;;;;;;;;;;;;;;;AA0BO,MAAM,oBAAoB,cAE/B,MAAS;AAMJ,SAAS,mBAAmB,EAAE,YAAqC;;AACxE,QAAM,EAAE,MAAM,WAAW,cAAA,IAAkB;AAAA,IACzC,2BAA2B,cAAA;AAAA,EAAc;AAE3C,QAAM,aAAa,cAAA;AAEnB,QAAM,eAAe;AAAA,IACnB,OAAO;AAAA,MACL,OAAM,6BAAM,SAAQ,CAAA;AAAA,MACpB;AAAA,MACA,kBAAiB,6BAAM,oBAAmB,CAAA;AAAA,MAC1C,kBAAiB,6BAAM,oBAAmB,CAAA;AAAA,MAC1C,UAAS,6BAAM,YAAW,CAAA;AAAA,MAC1B,SAAQ,6BAAM,WAAU,CAAA;AAAA,MACxB,eAAc,6BAAM,iBAAgB,CAAA;AAAA,MACpC,UAAU;AAAA,QACR,GAAG,6BAAM;AAAA,QACT,GAAI,WAAW,mBAAmB;AAAA,UAChC,UAAU;AAAA,YACR,aACE,WAAW,oBAAoB,UAC3B,UACA,IAAI,WAAW,eAAe;AAAA,UAAA;AAAA,QACtC;AAAA,MACF;AAAA,IACF;AAAA,IAEF;AAAA,MACE,6BAAM;AAAA,MACN,6BAAM;AAAA,MACN,6BAAM;AAAA,MACN,6BAAM;AAAA,MACN,6BAAM;AAAA,MACN,6BAAM;AAAA,MACN,6BAAM;AAAA,MACN,WAAW;AAAA,MACX;AAAA,IAAA;AAAA,EACF;AAIF,YAAU,MAAM;;AACd,UAAIA,OAAAC,MAAA,6BAAM,aAAN,gBAAAA,IAAgB,YAAhB,gBAAAD,IAAyB,iBAAgB,SAAS;AACpD,eAAS,QAAQ;AAAA,IACnB,OAAO;AACL,eAAS,QAAQ;AAAA,IACnB;AAAA,EACF,GAAG,EAAC,wCAAM,aAAN,mBAAgB,YAAhB,mBAAyB,WAAW,CAAC;AAEzC,SAAO,oBAAC,mBAAA,EAAkB,OAAO,cAAe,SAAA,CAAS;AAC3D;AAEO,SAAS,uBAA+C;AAC7D,QAAM,UAAU,IAAI,iBAAiB;AACrC,MAAI,CAAC,SAAS;AACZ,UAAM,IAAI;AAAA,MACR;AAAA,IAAA;AAAA,EAEJ;AACA,SAAO;AACT;"}
@@ -23,7 +23,7 @@ export declare function useUpdateApp(): import('@tanstack/react-query').UseMutat
23
23
  description?: string | undefined;
24
24
  teams?: string[] | undefined;
25
25
  accessRequest?: {
26
- approvalMethodId: string;
26
+ approvalMethodSlug: string;
27
27
  comments?: string | undefined;
28
28
  urls?: {
29
29
  url: string;
@@ -36,11 +36,7 @@ export declare function useUpdateApp(): import('@tanstack/react-query').UseMutat
36
36
  description?: string | undefined;
37
37
  adminNotes?: string | undefined;
38
38
  }[] | undefined;
39
- approvers?: {
40
- displayName: string;
41
- contact?: string | undefined;
42
- }[] | undefined;
43
- whoToReachOut?: string | undefined;
39
+ approverPersonSlugs?: string[] | undefined;
44
40
  } | undefined;
45
41
  notes?: string | undefined;
46
42
  tags?: string[] | undefined;
@@ -53,6 +49,28 @@ export declare function useUpdateApp(): import('@tanstack/react-query').UseMutat
53
49
  screenshotIds?: string[] | undefined;
54
50
  aiPrompt?: string | undefined;
55
51
  urlIssues?: string[] | undefined;
52
+ tiers?: {
53
+ tierSlug: string;
54
+ displayName?: string | undefined;
55
+ description?: string | undefined;
56
+ accessRequest?: {
57
+ approvalMethodSlug: string;
58
+ comments?: string | undefined;
59
+ urls?: {
60
+ url: string;
61
+ label?: string | undefined;
62
+ }[] | undefined;
63
+ requestPrompt?: string | undefined;
64
+ postApprovalInstructions?: string | undefined;
65
+ roles?: {
66
+ displayName: string;
67
+ description?: string | undefined;
68
+ adminNotes?: string | undefined;
69
+ }[] | undefined;
70
+ approverPersonSlugs?: string[] | undefined;
71
+ } | undefined;
72
+ appUrl?: string | undefined;
73
+ }[] | undefined;
56
74
  sources?: string[] | {
57
75
  sourceSlug: string;
58
76
  url: string;
@@ -1,11 +1,11 @@
1
1
  import { jsxs, jsx, Fragment } from "react/jsx-runtime";
2
- import { ExternalLink, Check, Copy, Settings, Users, Bot } from "lucide-react";
2
+ import { ExternalLink, Settings, Users, Bot, Check, Copy } from "lucide-react";
3
3
  import { useCallback, useState, useRef, useEffect } from "react";
4
4
  import ReactMarkdown from "react-markdown";
5
- import { Badge } from "../../../../ui/badge.js";
6
5
  import { Button } from "../../../../ui/button.js";
7
6
  import { Accordion, AccordionItem, AccordionTrigger, AccordionContent } from "../../../../ui/accordion.js";
8
7
  import { Table, TableHeader, TableRow, TableHead, TableBody, TableCell } from "../../../../ui/table.js";
8
+ import { PersonBadge } from "./PersonBadge.js";
9
9
  const COPY_FEEDBACK_DURATION = 2e3;
10
10
  const MarkdownLink = ({
11
11
  href,
@@ -27,6 +27,8 @@ function getApprovalMethodIcon(type) {
27
27
  case "personTeam":
28
28
  return /* @__PURE__ */ jsx(Users, { className: "size-5 text-primary" });
29
29
  case "custom":
30
+ case "noAccessRequired":
31
+ case "unknown":
30
32
  return /* @__PURE__ */ jsx(Settings, { className: "size-5 text-primary" });
31
33
  }
32
34
  }
@@ -92,19 +94,13 @@ function AccessRequestSection({
92
94
  const { copiedId, copyToClipboard } = useCopyToClipboard();
93
95
  const accessRequest = app.accessRequest;
94
96
  const approvalMethod = approvalMethods.find(
95
- (m) => m.slug === (accessRequest == null ? void 0 : accessRequest.approvalMethodId)
97
+ (m) => m.slug === (accessRequest == null ? void 0 : accessRequest.approvalMethodSlug)
96
98
  );
97
99
  const handleCopyPrompt = useCallback(() => {
98
100
  if (accessRequest == null ? void 0 : accessRequest.requestPrompt) {
99
101
  copyToClipboard(accessRequest.requestPrompt, "prompt");
100
102
  }
101
103
  }, [accessRequest == null ? void 0 : accessRequest.requestPrompt, copyToClipboard]);
102
- const handleCopyApproverEmail = useCallback(
103
- (email, index) => {
104
- copyToClipboard(email, `approver-${index}`);
105
- },
106
- [copyToClipboard]
107
- );
108
104
  if (!accessRequest) return null;
109
105
  return /* @__PURE__ */ jsxs("div", { className: "mt-6 space-y-4", children: [
110
106
  /* @__PURE__ */ jsx("h3", { className: "text-sm font-medium", children: "Access Request" }),
@@ -171,55 +167,9 @@ function AccessRequestSection({
171
167
  ] }, `${role.displayName}-${idx}`)) })
172
168
  ] }) })
173
169
  ] }),
174
- accessRequest.approvers && accessRequest.approvers.length > 0 && /* @__PURE__ */ jsxs("div", { children: [
170
+ accessRequest.approverPersonSlugs && accessRequest.approverPersonSlugs.length > 0 && /* @__PURE__ */ jsxs("div", { children: [
175
171
  /* @__PURE__ */ jsx("h4", { className: "mb-2 text-sm font-medium", children: "Approvers" }),
176
- /* @__PURE__ */ jsx("div", { className: "flex flex-wrap gap-2", children: accessRequest.approvers.map((approver, idx) => {
177
- const approverId = `approver-${idx}`;
178
- const isCopied = copiedId === approverId;
179
- return /* @__PURE__ */ jsxs(
180
- Badge,
181
- {
182
- variant: "outline",
183
- className: "font-normal inline-flex items-center gap-1",
184
- children: [
185
- approver.displayName,
186
- approver.contact && /* @__PURE__ */ jsxs(Fragment, { children: [
187
- /* @__PURE__ */ jsxs("span", { className: "text-xs opacity-70", children: [
188
- "(",
189
- approver.contact,
190
- ")"
191
- ] }),
192
- /* @__PURE__ */ jsx(
193
- "button",
194
- {
195
- type: "button",
196
- onClick: (e) => {
197
- e.stopPropagation();
198
- handleCopyApproverEmail(approver.contact, idx);
199
- },
200
- className: "inline-flex items-center justify-center hover:bg-accent rounded p-0.5 transition-colors focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-1",
201
- "aria-label": `Copy ${approver.displayName}'s email`,
202
- title: isCopied ? "Copied!" : "Copy email",
203
- children: isCopied ? /* @__PURE__ */ jsxs(Fragment, { children: [
204
- /* @__PURE__ */ jsx(Check, { className: "h-3 w-3 text-green-600" }),
205
- /* @__PURE__ */ jsx(
206
- "span",
207
- {
208
- className: "sr-only",
209
- role: "status",
210
- "aria-live": "polite",
211
- children: "Email copied to clipboard"
212
- }
213
- )
214
- ] }) : /* @__PURE__ */ jsx(Copy, { className: "h-3 w-3 opacity-50 hover:opacity-100" })
215
- }
216
- )
217
- ] })
218
- ]
219
- },
220
- `${approver.displayName}-${idx}`
221
- );
222
- }) })
172
+ /* @__PURE__ */ jsx("div", { className: "flex flex-wrap gap-2", children: accessRequest.approverPersonSlugs.map((slug) => /* @__PURE__ */ jsx(PersonBadge, { slug }, slug)) })
223
173
  ] }),
224
174
  accessRequest.urls && accessRequest.urls.length > 0 && /* @__PURE__ */ jsxs("div", { children: [
225
175
  /* @__PURE__ */ jsx("h4", { className: "mb-2 text-sm font-medium", children: "Documentation" }),
@@ -1 +1 @@
1
- {"version":3,"file":"AccessRequestSection.js","sources":["../../../../../../src/modules/appCatalog/ui/components/AccessRequestSection.tsx"],"sourcesContent":["import type {\n AppApprovalMethod,\n AppForCatalog,\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 { Badge } from '~/ui/badge'\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'\n\n// Constants\nconst COPY_FEEDBACK_DURATION = 2000\n\ninterface AccessRequestSectionProps {\n app: AppForCatalog\n approvalMethods: AppApprovalMethod[]\n}\n\n// Component for rendering markdown links with security attributes\nconst MarkdownLink = ({\n href,\n children,\n}: {\n href?: string\n children?: React.ReactNode\n}) => (\n <a\n href={href}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"text-primary hover:underline\"\n >\n {children}\n </a>\n)\n\n// Helper function for approval method icons\nfunction getApprovalMethodIcon(type: 'service' | 'personTeam' | 'custom') {\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 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?.approvalMethodId,\n )\n\n const handleCopyPrompt = useCallback(() => {\n if (accessRequest?.requestPrompt) {\n copyToClipboard(accessRequest.requestPrompt, 'prompt')\n }\n }, [accessRequest?.requestPrompt, copyToClipboard])\n\n const handleCopyApproverEmail = useCallback(\n (email: string, index: number) => {\n copyToClipboard(email, `approver-${index}`)\n },\n [copyToClipboard],\n )\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.approvers && accessRequest.approvers.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.approvers.map((approver, idx) => {\n const approverId = `approver-${idx}`\n const isCopied = copiedId === approverId\n\n return (\n <Badge\n key={`${approver.displayName}-${idx}`}\n variant=\"outline\"\n className=\"font-normal inline-flex items-center gap-1\"\n >\n {approver.displayName}\n {approver.contact && (\n <>\n <span className=\"text-xs opacity-70\">\n ({approver.contact})\n </span>\n <button\n type=\"button\"\n onClick={(e) => {\n e.stopPropagation()\n handleCopyApproverEmail(approver.contact!, idx)\n }}\n className=\"inline-flex items-center justify-center hover:bg-accent rounded p-0.5 transition-colors focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-1\"\n aria-label={`Copy ${approver.displayName}'s email`}\n title={isCopied ? 'Copied!' : 'Copy email'}\n >\n {isCopied ? (\n <>\n <Check className=\"h-3 w-3 text-green-600\" />\n <span\n className=\"sr-only\"\n role=\"status\"\n aria-live=\"polite\"\n >\n Email copied to clipboard\n </span>\n </>\n ) : (\n <Copy className=\"h-3 w-3 opacity-50 hover:opacity-100\" />\n )}\n </button>\n </>\n )}\n </Badge>\n )\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 AppForCatalog,\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 { PersonBadge } from './PersonBadge'\n\n// Constants\nconst COPY_FEEDBACK_DURATION = 2000\n\ninterface AccessRequestSectionProps {\n app: AppForCatalog\n approvalMethods: AppApprovalMethod[]\n}\n\n// Component for rendering markdown links with security attributes\nconst MarkdownLink = ({\n href,\n children,\n}: {\n href?: string\n children?: React.ReactNode\n}) => (\n <a\n href={href}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"text-primary hover:underline\"\n >\n {children}\n </a>\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":";;;;;;;;AAyBA,MAAM,yBAAyB;AAQ/B,MAAM,eAAe,CAAC;AAAA,EACpB;AAAA,EACA;AACF,MAIE;AAAA,EAAC;AAAA,EAAA;AAAA,IACC;AAAA,IACA,QAAO;AAAA,IACP,KAAI;AAAA,IACJ,WAAU;AAAA,IAET;AAAA,EAAA;AACH;AAIF,SAAS,sBACP,MACA;AACA,UAAQ,MAAA;AAAA,IACN,KAAK;AACH,aAAO,oBAAC,KAAA,EAAI,WAAU,sBAAA,CAAsB;AAAA,IAC9C,KAAK;AACH,aAAO,oBAAC,OAAA,EAAM,WAAU,sBAAA,CAAsB;AAAA,IAChD,KAAK;AAAA,IACL,KAAK;AAAA,IACL,KAAK;AACH,aAAO,oBAAC,UAAA,EAAS,WAAU,sBAAA,CAAsB;AAAA,EAAA;AAEvD;AAMA,SAAS,qBAAqB;AAC5B,QAAM,CAAC,UAAU,WAAW,IAAI,SAAwB,IAAI;AAC5D,QAAM,CAAC,OAAO,QAAQ,IAAI,SAAwB,IAAI;AACtD,QAAM,aAAa,OAA8B,IAAI;AAGrD,YAAU,MAAM;AACd,WAAO,MAAM;AACX,UAAI,WAAW,SAAS;AACtB,qBAAa,WAAW,OAAO;AAAA,MACjC;AAAA,IACF;AAAA,EACF,GAAG,CAAA,CAAE;AAEL,QAAM,kBAAkB,YAAY,OAAO,MAAc,OAAe;AAEtE,QAAI,WAAW,SAAS;AACtB,mBAAa,WAAW,OAAO;AAAA,IACjC;AAEA,QAAI;AACF,YAAM,UAAU,UAAU,UAAU,IAAI;AACxC,kBAAY,EAAE;AACd,eAAS,IAAI;AAGb,iBAAW,UAAU,WAAW,MAAM;AACpC,oBAAY,IAAI;AAAA,MAClB,GAAG,sBAAsB;AAAA,IAC3B,SAAS,KAAK;AACZ,cAAQ,MAAM,gCAAgC,GAAG;AACjD,eAAS,gBAAgB;AAGzB,iBAAW,UAAU,WAAW,MAAM;AACpC,iBAAS,IAAI;AAAA,MACf,GAAG,sBAAsB;AAAA,IAC3B;AAAA,EACF,GAAG,CAAA,CAAE;AAEL,SAAO,EAAE,UAAU,OAAO,gBAAA;AAC5B;AAYA,SAAS,WAAW;AAAA,EAClB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAAoB;AAClB,SACE;AAAA,IAAC;AAAA,IAAA;AAAA,MACC,MAAK;AAAA,MACL,SAAQ;AAAA,MACR,MAAK;AAAA,MACL,SAAS;AAAA,MACT;AAAA,MACA,cAAY;AAAA,MACZ,OAAO,WAAW,YAAY;AAAA,MAE7B,qBACC,qBAAA,UAAA,EACE,UAAA;AAAA,QAAA,oBAAC,OAAA,EAAM,WAAU,6BAAA,CAA6B;AAAA,QAC9C,oBAAC,UAAK,WAAU,WAAU,MAAK,UAAS,aAAU,UAAS,UAAA,sBAAA,CAE3D;AAAA,MAAA,EAAA,CACF,IAEA,oBAAC,MAAA,EAAK,WAAU,cAAA,CAAc;AAAA,IAAA;AAAA,EAAA;AAItC;AAEO,SAAS,qBAAqB;AAAA,EACnC;AAAA,EACA;AACF,GAA8B;AAC5B,QAAM,EAAE,UAAU,gBAAA,IAAoB,mBAAA;AACtC,QAAM,gBAAgB,IAAI;AAC1B,QAAM,iBAAiB,gBAAgB;AAAA,IACrC,CAAC,MAAM,EAAE,UAAS,+CAAe;AAAA,EAAA;AAGnC,QAAM,mBAAmB,YAAY,MAAM;AACzC,QAAI,+CAAe,eAAe;AAChC,sBAAgB,cAAc,eAAe,QAAQ;AAAA,IACvD;AAAA,EACF,GAAG,CAAC,+CAAe,eAAe,eAAe,CAAC;AAGlD,MAAI,CAAC,cAAe,QAAO;AAE3B,SACE,qBAAC,OAAA,EAAI,WAAU,kBACb,UAAA;AAAA,IAAA,oBAAC,MAAA,EAAG,WAAU,uBAAsB,UAAA,kBAAc;AAAA,IAGjD,kBAAkB,eAAe,SAAS,gCACxC,OAAA,EAAI,WAAU,2BACZ,UAAA,eAAe,SAAS,aAAa,eAAe,OAAO,MAC1D,qBAAA,UAAA,EACE,UAAA;AAAA,MAAA;AAAA,QAAC;AAAA,QAAA;AAAA,UACC,MAAM,eAAe,OAAO;AAAA,UAC5B,QAAO;AAAA,UACP,KAAI;AAAA,UACJ,WAAU;AAAA,UACV,OAAO,eAAe,OAAO;AAAA,UAE7B,UAAA;AAAA,YAAA,oBAAC,OAAA,EAAK,UAAA,sBAAsB,eAAe,IAAI,GAAE;AAAA,YACjD,oBAAC,OAAA,EAAI,WAAU,eAAe,yBAAe,YAAA,CAAY;AAAA,UAAA;AAAA,QAAA;AAAA,MAAA;AAAA,MAE3D;AAAA,QAAC;AAAA,QAAA;AAAA,UACC,MAAM,eAAe,OAAO;AAAA,UAC5B,QAAO;AAAA,UACP,KAAI;AAAA,UACJ,WAAU;AAAA,UACV,OAAO,eAAe,OAAO;AAAA,UAE7B,UAAA,oBAAC,cAAA,EAAa,WAAU,SAAA,CAAS;AAAA,QAAA;AAAA,MAAA;AAAA,IACnC,EAAA,CACF,IAEA,qBAAA,UAAA,EACE,UAAA;AAAA,MAAA,oBAAC,OAAA,EAAK,UAAA,sBAAsB,eAAe,IAAI,GAAE;AAAA,MACjD,oBAAC,OAAA,EAAI,WAAU,eAAe,yBAAe,YAAA,CAAY;AAAA,IAAA,EAAA,CAC3D,EAAA,CAEJ;AAAA,IAID,cAAc,iBACb,qBAAC,OAAA,EAAI,WAAU,0CACb,UAAA;AAAA,MAAA,oBAAC,QAAA,EAAK,WAAU,8BAA6B,UAAA,mBAAe;AAAA,MAC5D,oBAAC,QAAA,EAAK,WAAU,gDACd,UAAA,oBAAC,eAAA,EAAc,YAAY,EAAE,GAAG,gBAC7B,UAAA,cAAc,eACjB,GACF;AAAA,MACA;AAAA,QAAC;AAAA,QAAA;AAAA,UACC,QAAQ;AAAA,UACR,UAAU,aAAa;AAAA,UACvB,WAAU;AAAA,UACV,WAAU;AAAA,QAAA;AAAA,MAAA;AAAA,IACZ,GACF;AAAA,IAID,cAAc,YACb,oBAAC,OAAA,EAAI,WAAU,2DACb,UAAA,oBAAC,eAAA,EAAc,YAAY,EAAE,GAAG,aAAA,GAC7B,UAAA,cAAc,UACjB,GACF;AAAA,IAID,cAAc,SAAS,cAAc,MAAM,SAAS,0BAClD,OAAA,EACC,UAAA;AAAA,MAAA,oBAAC,MAAA,EAAG,WAAU,4BAA2B,UAAA,mBAAe;AAAA,MACxD,oBAAC,OAAA,EAAI,WAAU,qBACb,+BAAC,OAAA,EACC,UAAA;AAAA,QAAA,oBAAC,aAAA,EACC,+BAAC,UAAA,EACC,UAAA;AAAA,UAAA,oBAAC,WAAA,EAAU,WAAU,qBAAoB,UAAA,QAAI;AAAA,UAC7C,oBAAC,aAAU,UAAA,cAAA,CAAW;AAAA,QAAA,EAAA,CACxB,EAAA,CACF;AAAA,QACA,oBAAC,aACE,UAAA,cAAc,MAAM,IAAI,CAAC,MAAM,QAC9B,qBAAC,UAAA,EACC,UAAA;AAAA,UAAA,oBAAC,WAAA,EAAU,WAAU,iCAClB,UAAA,KAAK,aACR;AAAA,UACA,qBAAC,WAAA,EAAU,WAAU,iCAClB,UAAA;AAAA,YAAA,KAAK,eAAe;AAAA,YACpB,KAAK,cACJ,qBAAC,OAAA,EAAI,WAAU,gDAA+C,UAAA;AAAA,cAAA;AAAA,cACrD,KAAK;AAAA,YAAA,EAAA,CACd;AAAA,UAAA,EAAA,CAEJ;AAAA,QAAA,EAAA,GAXa,GAAG,KAAK,WAAW,IAAI,GAAG,EAYzC,CACD,EAAA,CACH;AAAA,MAAA,EAAA,CACF,EAAA,CACF;AAAA,IAAA,GACF;AAAA,IAID,cAAc,uBACb,cAAc,oBAAoB,SAAS,0BACxC,OAAA,EACC,UAAA;AAAA,MAAA,oBAAC,MAAA,EAAG,WAAU,4BAA2B,UAAA,aAAS;AAAA,MAClD,oBAAC,OAAA,EAAI,WAAU,wBACZ,wBAAc,oBAAoB,IAAI,CAAC,SACtC,oBAAC,aAAA,EAAuB,KAAA,GAAN,IAAkB,CACrC,EAAA,CACH;AAAA,IAAA,GACF;AAAA,IAIH,cAAc,QAAQ,cAAc,KAAK,SAAS,0BAChD,OAAA,EACC,UAAA;AAAA,MAAA,oBAAC,MAAA,EAAG,WAAU,4BAA2B,UAAA,iBAAa;AAAA,MACtD,oBAAC,SAAI,WAAU,yBACZ,wBAAc,KAAK,IAAI,CAAC,QAAQ,QAC/B;AAAA,QAAC;AAAA,QAAA;AAAA,UAEC,MAAM,OAAO;AAAA,UACb,QAAO;AAAA,UACP,KAAI;AAAA,UACJ,WAAU;AAAA,UAET,UAAA;AAAA,YAAA,OAAO,SAAS,OAAO;AAAA,YACxB,oBAAC,cAAA,EAAa,WAAU,SAAA,CAAS;AAAA,UAAA;AAAA,QAAA;AAAA,QAP5B,GAAG,OAAO,GAAG,IAAI,GAAG;AAAA,MAAA,CAS5B,EAAA,CACH;AAAA,IAAA,GACF;AAAA,IAID,cAAc,4BACb,oBAAC,aAAU,MAAK,UAAS,aAAW,MAClC,UAAA;AAAA,MAAC;AAAA,MAAA;AAAA,QACC,OAAM;AAAA,QACN,WAAU;AAAA,QAEV,UAAA;AAAA,UAAA,oBAAC,kBAAA,EAAiB,WAAU,mCAAkC,UAAA,8BAE9D;AAAA,8BACC,kBAAA,EAAiB,WAAU,QAC1B,UAAA,oBAAC,OAAA,EAAI,WAAU,2DACb,UAAA,oBAAC,eAAA,EAAc,YAAY,EAAE,GAAG,aAAA,GAC7B,UAAA,cAAc,0BACjB,GACF,EAAA,CACF;AAAA,QAAA;AAAA,MAAA;AAAA,IAAA,EACF,CACF;AAAA,EAAA,GAEJ;AAEJ;"}
@@ -0,0 +1,5 @@
1
+ interface PersonBadgeProps {
2
+ slug: string;
3
+ }
4
+ export declare function PersonBadge({ slug }: PersonBadgeProps): import("react/jsx-runtime").JSX.Element;
5
+ export {};
@@ -0,0 +1,68 @@
1
+ import { jsxs, jsx } from "react/jsx-runtime";
2
+ import { User, Check, Copy } from "lucide-react";
3
+ import { useState, useRef, useEffect, useCallback } from "react";
4
+ import { Badge } from "../../../../ui/badge.js";
5
+ import { useAppCatalogContext } from "../../context/AppCatalogContext.js";
6
+ import "@tanstack/react-query-devtools";
7
+ import "next-themes";
8
+ import "@radix-ui/react-dialog";
9
+ import "clsx";
10
+ import "tailwind-merge";
11
+ import "../../../config/GlobalConfigContext.js";
12
+ import "../../../pluginCore/PluginManagerContext.js";
13
+ import "@radix-ui/react-tooltip";
14
+ import "../../../../ui/empty.js";
15
+ import "../../../../ui/button.js";
16
+ import { getPersonBySlug } from "../../utils/resolveHelpers.js";
17
+ function PersonBadge({ slug }) {
18
+ const { persons } = useAppCatalogContext();
19
+ const person = getPersonBySlug(persons, slug);
20
+ const displayName = person ? `${person.firstName} ${person.lastName}`.trim() || slug : slug;
21
+ const email = person == null ? void 0 : person.email;
22
+ return /* @__PURE__ */ jsxs(
23
+ Badge,
24
+ {
25
+ variant: "outline",
26
+ className: "font-normal inline-flex items-center gap-1",
27
+ title: email ? `${displayName} (${email})` : displayName,
28
+ children: [
29
+ /* @__PURE__ */ jsx(User, { className: "size-3" }),
30
+ displayName,
31
+ email && /* @__PURE__ */ jsx(CopyEmailButton, { email })
32
+ ]
33
+ }
34
+ );
35
+ }
36
+ function CopyEmailButton({ email }) {
37
+ const [copied, setCopied] = useState(false);
38
+ const timeoutRef = useRef(null);
39
+ useEffect(() => {
40
+ return () => {
41
+ if (timeoutRef.current) clearTimeout(timeoutRef.current);
42
+ };
43
+ }, []);
44
+ const handleCopy = useCallback(
45
+ (e) => {
46
+ e.stopPropagation();
47
+ navigator.clipboard.writeText(email);
48
+ setCopied(true);
49
+ if (timeoutRef.current) clearTimeout(timeoutRef.current);
50
+ timeoutRef.current = setTimeout(() => setCopied(false), 2e3);
51
+ },
52
+ [email]
53
+ );
54
+ return /* @__PURE__ */ jsx(
55
+ "button",
56
+ {
57
+ onClick: handleCopy,
58
+ className: "ml-0.5 hover:text-primary transition-colors",
59
+ title: copied ? "Copied!" : `Copy ${email}`,
60
+ type: "button",
61
+ children: copied ? /* @__PURE__ */ jsx(Check, { className: "size-3 text-green-600" }) : /* @__PURE__ */ jsx(Copy, { className: "size-3" })
62
+ }
63
+ );
64
+ }
65
+ export {
66
+ PersonBadge
67
+ };
68
+ //# sourceMappingURL=PersonBadge.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"PersonBadge.js","sources":["../../../../../../src/modules/appCatalog/ui/components/PersonBadge.tsx"],"sourcesContent":["import { Check, Copy, User } from 'lucide-react'\nimport { useCallback, useEffect, useRef, useState } from 'react'\nimport { Badge } from '~/ui/badge'\nimport { useAppCatalogContext } from '~/modules/appCatalog'\nimport { getPersonBySlug } from '~/modules/appCatalog/utils/resolveHelpers'\n\ninterface PersonBadgeProps {\n slug: string\n}\n\nexport function PersonBadge({ slug }: PersonBadgeProps) {\n const { persons } = useAppCatalogContext()\n const person = getPersonBySlug(persons, slug)\n\n const displayName = person\n ? `${person.firstName} ${person.lastName}`.trim() || slug\n : slug\n\n const email = person?.email\n\n return (\n <Badge\n variant=\"outline\"\n className=\"font-normal inline-flex items-center gap-1\"\n title={email ? `${displayName} (${email})` : displayName}\n >\n <User className=\"size-3\" />\n {displayName}\n {email && <CopyEmailButton email={email} />}\n </Badge>\n )\n}\n\nfunction CopyEmailButton({ email }: { email: 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 (e: React.MouseEvent) => {\n e.stopPropagation()\n navigator.clipboard.writeText(email)\n setCopied(true)\n if (timeoutRef.current) clearTimeout(timeoutRef.current)\n timeoutRef.current = setTimeout(() => setCopied(false), 2000)\n },\n [email],\n )\n\n return (\n <button\n onClick={handleCopy}\n className=\"ml-0.5 hover:text-primary transition-colors\"\n title={copied ? 'Copied!' : `Copy ${email}`}\n type=\"button\"\n >\n {copied ? (\n <Check className=\"size-3 text-green-600\" />\n ) : (\n <Copy className=\"size-3\" />\n )}\n </button>\n )\n}\n"],"names":[],"mappings":";;;;;;;;;;;;;;;;AAUO,SAAS,YAAY,EAAE,QAA0B;AACtD,QAAM,EAAE,QAAA,IAAY,qBAAA;AACpB,QAAM,SAAS,gBAAgB,SAAS,IAAI;AAE5C,QAAM,cAAc,SAChB,GAAG,OAAO,SAAS,IAAI,OAAO,QAAQ,GAAG,KAAA,KAAU,OACnD;AAEJ,QAAM,QAAQ,iCAAQ;AAEtB,SACE;AAAA,IAAC;AAAA,IAAA;AAAA,MACC,SAAQ;AAAA,MACR,WAAU;AAAA,MACV,OAAO,QAAQ,GAAG,WAAW,KAAK,KAAK,MAAM;AAAA,MAE7C,UAAA;AAAA,QAAA,oBAAC,MAAA,EAAK,WAAU,SAAA,CAAS;AAAA,QACxB;AAAA,QACA,SAAS,oBAAC,iBAAA,EAAgB,MAAA,CAAc;AAAA,MAAA;AAAA,IAAA;AAAA,EAAA;AAG/C;AAEA,SAAS,gBAAgB,EAAE,SAA4B;AACrD,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;AAAA,IACjB,CAAC,MAAwB;AACvB,QAAE,gBAAA;AACF,gBAAU,UAAU,UAAU,KAAK;AACnC,gBAAU,IAAI;AACd,UAAI,WAAW,QAAS,cAAa,WAAW,OAAO;AACvD,iBAAW,UAAU,WAAW,MAAM,UAAU,KAAK,GAAG,GAAI;AAAA,IAC9D;AAAA,IACA,CAAC,KAAK;AAAA,EAAA;AAGR,SACE;AAAA,IAAC;AAAA,IAAA;AAAA,MACC,SAAS;AAAA,MACT,WAAU;AAAA,MACV,OAAO,SAAS,YAAY,QAAQ,KAAK;AAAA,MACzC,MAAK;AAAA,MAEJ,UAAA,6BACE,OAAA,EAAM,WAAU,yBAAwB,IAEzC,oBAAC,MAAA,EAAK,WAAU,SAAA,CAAS;AAAA,IAAA;AAAA,EAAA;AAIjC;"}
@@ -0,0 +1,6 @@
1
+ import { SubResource } from '@igstack/app-catalog-backend-core';
2
+ interface SubResourcesSectionProps {
3
+ subResources: SubResource[];
4
+ }
5
+ export declare function SubResourcesSection({ subResources, }: SubResourcesSectionProps): import("react/jsx-runtime").JSX.Element | null;
6
+ export {};
@@ -0,0 +1,148 @@
1
+ import { jsxs, jsx } from "react/jsx-runtime";
2
+ import { Search } from "lucide-react";
3
+ import { useState, useMemo } from "react";
4
+ import { Badge } from "../../../../ui/badge.js";
5
+ import { Input } from "../../../../ui/input.js";
6
+ import { Table, TableHeader, TableRow, TableHead, TableBody, TableCell } from "../../../../ui/table.js";
7
+ import { PersonBadge } from "./PersonBadge.js";
8
+ import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from "../../../../ui/select.js";
9
+ import { useAppCatalogContext } from "../../context/AppCatalogContext.js";
10
+ import "@tanstack/react-query-devtools";
11
+ import "next-themes";
12
+ import "@radix-ui/react-dialog";
13
+ import "clsx";
14
+ import "tailwind-merge";
15
+ import "../../../config/GlobalConfigContext.js";
16
+ import "../../../pluginCore/PluginManagerContext.js";
17
+ import "@radix-ui/react-tooltip";
18
+ import "../../../../ui/empty.js";
19
+ import "../../../../ui/button.js";
20
+ import { getGroupBySlug } from "../../utils/resolveHelpers.js";
21
+ function getTierBadgeVariant(tierSlug) {
22
+ if (tierSlug === "prod" || tierSlug === "production") return "destructive";
23
+ if (tierSlug === "dev" || tierSlug === "staging") return "secondary";
24
+ if (tierSlug === "preprod") return "outline";
25
+ if (tierSlug === "sandbox") return "outline";
26
+ return "outline";
27
+ }
28
+ function getTierBadgeClassName(tierSlug) {
29
+ if (tierSlug === "preprod")
30
+ return "border-amber-400 bg-amber-100 text-amber-800 hover:bg-amber-200";
31
+ if (tierSlug === "sandbox")
32
+ return "border-gray-400 bg-gray-100 text-gray-700 hover:bg-gray-200";
33
+ return "";
34
+ }
35
+ function getTierDisplayLabel(tierSlug) {
36
+ if (tierSlug === "preprod") return "Pre-Prod";
37
+ if (tierSlug === "sandbox") return "Sandbox";
38
+ if (tierSlug === "prod" || tierSlug === "production") return "Prod";
39
+ if (tierSlug === "dev") return "Dev";
40
+ if (tierSlug === "staging") return "Staging";
41
+ return tierSlug;
42
+ }
43
+ function SubResourcesSection({
44
+ subResources
45
+ }) {
46
+ const { groups } = useAppCatalogContext();
47
+ const [search, setSearch] = useState("");
48
+ const [tierFilter, setTierFilter] = useState("all");
49
+ const uniqueTiers = useMemo(() => {
50
+ const tiers = /* @__PURE__ */ new Set();
51
+ for (const sr of subResources) {
52
+ if (sr.tierSlug) tiers.add(sr.tierSlug);
53
+ }
54
+ return [...tiers].sort();
55
+ }, [subResources]);
56
+ const filtered = useMemo(() => {
57
+ let result = subResources;
58
+ if (tierFilter !== "all") {
59
+ result = result.filter((sr) => sr.tierSlug === tierFilter);
60
+ }
61
+ if (search.trim()) {
62
+ const q = search.trim().toLowerCase();
63
+ result = result.filter(
64
+ (sr) => {
65
+ var _a;
66
+ return sr.displayName.toLowerCase().includes(q) || sr.aliases.some((a) => a.toLowerCase().includes(q)) || (((_a = sr.description) == null ? void 0 : _a.toLowerCase().includes(q)) ?? false);
67
+ }
68
+ );
69
+ }
70
+ return result;
71
+ }, [subResources, search, tierFilter]);
72
+ if (subResources.length === 0) return null;
73
+ return /* @__PURE__ */ jsxs("div", { className: "space-y-3", children: [
74
+ /* @__PURE__ */ jsx("div", { className: "flex items-center justify-between", children: /* @__PURE__ */ jsxs("div", { className: "text-sm font-medium", children: [
75
+ "Sub-Resources (",
76
+ filtered.length,
77
+ " of ",
78
+ subResources.length,
79
+ ")"
80
+ ] }) }),
81
+ /* @__PURE__ */ jsxs("div", { className: "flex gap-2", children: [
82
+ /* @__PURE__ */ jsxs("div", { className: "relative flex-1", children: [
83
+ /* @__PURE__ */ jsx(Search, { className: "absolute left-2.5 top-2.5 size-4 text-muted-foreground" }),
84
+ /* @__PURE__ */ jsx(
85
+ Input,
86
+ {
87
+ placeholder: "Search resources by name or alias...",
88
+ value: search,
89
+ onChange: (e) => setSearch(e.target.value),
90
+ className: "pl-9 h-9"
91
+ }
92
+ )
93
+ ] }),
94
+ uniqueTiers.length > 1 && /* @__PURE__ */ jsxs(Select, { value: tierFilter, onValueChange: setTierFilter, children: [
95
+ /* @__PURE__ */ jsx(SelectTrigger, { className: "w-[130px] h-9", children: /* @__PURE__ */ jsx(SelectValue, { placeholder: "All tiers" }) }),
96
+ /* @__PURE__ */ jsxs(SelectContent, { children: [
97
+ /* @__PURE__ */ jsx(SelectItem, { value: "all", children: "All tiers" }),
98
+ uniqueTiers.map((tier) => /* @__PURE__ */ jsx(SelectItem, { value: tier, children: tier }, tier))
99
+ ] })
100
+ ] })
101
+ ] }),
102
+ /* @__PURE__ */ jsx("div", { className: "rounded-lg border max-h-[400px] overflow-auto", children: /* @__PURE__ */ jsxs(Table, { children: [
103
+ /* @__PURE__ */ jsx(TableHeader, { children: /* @__PURE__ */ jsxs(TableRow, { children: [
104
+ /* @__PURE__ */ jsx(TableHead, { children: "Name" }),
105
+ /* @__PURE__ */ jsx(TableHead, { className: "w-[80px]", children: "Tier" }),
106
+ /* @__PURE__ */ jsx(TableHead, { children: "Owner" }),
107
+ /* @__PURE__ */ jsx(TableHead, { children: "Access Contacts" })
108
+ ] }) }),
109
+ /* @__PURE__ */ jsx(TableBody, { children: filtered.length === 0 ? /* @__PURE__ */ jsx(TableRow, { children: /* @__PURE__ */ jsx(
110
+ TableCell,
111
+ {
112
+ colSpan: 4,
113
+ className: "text-center text-muted-foreground py-8",
114
+ children: "No resources match your filters"
115
+ }
116
+ ) }) : filtered.map((sr) => {
117
+ const maintainerMembers = sr.accessMaintainerGroupSlugs.flatMap(
118
+ (groupSlug) => {
119
+ const group = getGroupBySlug(groups, groupSlug);
120
+ return (group == null ? void 0 : group.memberSlugs) ?? [];
121
+ }
122
+ );
123
+ const uniqueMaintainers = [...new Set(maintainerMembers)];
124
+ return /* @__PURE__ */ jsxs(TableRow, { children: [
125
+ /* @__PURE__ */ jsxs(TableCell, { children: [
126
+ /* @__PURE__ */ jsx("div", { className: "font-medium text-sm", children: sr.displayName }),
127
+ sr.aliases.length > 0 && /* @__PURE__ */ jsx("div", { className: "text-xs text-muted-foreground mt-0.5", children: sr.aliases.join(", ") }),
128
+ sr.description && /* @__PURE__ */ jsx("div", { className: "text-xs text-muted-foreground mt-0.5", children: sr.description })
129
+ ] }),
130
+ /* @__PURE__ */ jsx(TableCell, { children: sr.tierSlug && /* @__PURE__ */ jsx(
131
+ Badge,
132
+ {
133
+ variant: getTierBadgeVariant(sr.tierSlug),
134
+ className: `text-xs ${getTierBadgeClassName(sr.tierSlug)}`,
135
+ children: getTierDisplayLabel(sr.tierSlug)
136
+ }
137
+ ) }),
138
+ /* @__PURE__ */ jsx(TableCell, { children: sr.ownerPersonSlug && /* @__PURE__ */ jsx(PersonBadge, { slug: sr.ownerPersonSlug }) }),
139
+ /* @__PURE__ */ jsx(TableCell, { children: uniqueMaintainers.length > 0 ? /* @__PURE__ */ jsx("div", { className: "flex flex-wrap gap-1", children: uniqueMaintainers.map((personSlug) => /* @__PURE__ */ jsx(PersonBadge, { slug: personSlug }, personSlug)) }) : /* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: "-" }) })
140
+ ] }, sr.slug);
141
+ }) })
142
+ ] }) })
143
+ ] });
144
+ }
145
+ export {
146
+ SubResourcesSection
147
+ };
148
+ //# sourceMappingURL=SubResourcesSection.js.map