@igstack/app-catalog-frontend-core 0.3.1-alpha-20260405015231 → 0.4.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 (65) hide show
  1. package/dist/esm/__tests__/integration/harness/MockBackendVerifier.d.ts +3 -3
  2. package/dist/esm/__tests__/integration/mock-backend/MockBackendConfigurer.d.ts +4 -4
  3. package/dist/esm/__tests__/integration/mock-backend/MockDb.d.ts +11 -7
  4. package/dist/esm/api/infra/trpc.d.ts +3 -3
  5. package/dist/esm/modules/appCatalog/context/AppCatalogContext.d.ts +2 -3
  6. package/dist/esm/modules/appCatalog/context/AppCatalogContext.js +3 -4
  7. package/dist/esm/modules/appCatalog/context/AppCatalogContext.js.map +1 -1
  8. package/dist/esm/modules/appCatalog/hooks/useAppCounts.d.ts +2 -2
  9. package/dist/esm/modules/appCatalog/hooks/useAppCounts.js +3 -3
  10. package/dist/esm/modules/appCatalog/hooks/useAppCounts.js.map +1 -1
  11. package/dist/esm/modules/appCatalog/hooks/useUpdateApp.d.ts +9 -0
  12. package/dist/esm/modules/appCatalog/ui/components/AccessRequestSection.d.ts +2 -2
  13. package/dist/esm/modules/appCatalog/ui/components/AccessRequestSection.js.map +1 -1
  14. package/dist/esm/modules/appCatalog/ui/components/AppDetailModal.d.ts +2 -2
  15. package/dist/esm/modules/appCatalog/ui/components/PersonBadge.js +1 -0
  16. package/dist/esm/modules/appCatalog/ui/components/PersonBadge.js.map +1 -1
  17. package/dist/esm/modules/appCatalog/ui/components/ScreenshotGallery.d.ts +2 -2
  18. package/dist/esm/modules/appCatalog/ui/components/ScreenshotGallery.js.map +1 -1
  19. package/dist/esm/modules/appCatalog/ui/components/SubResourcesSection.d.ts +2 -2
  20. package/dist/esm/modules/appCatalog/ui/components/SubResourcesSection.js +13 -14
  21. package/dist/esm/modules/appCatalog/ui/components/SubResourcesSection.js.map +1 -1
  22. package/dist/esm/modules/appCatalog/ui/components/TierVariantsSection.d.ts +2 -2
  23. package/dist/esm/modules/appCatalog/ui/components/TierVariantsSection.js +1 -0
  24. package/dist/esm/modules/appCatalog/ui/components/TierVariantsSection.js.map +1 -1
  25. package/dist/esm/modules/appCatalog/ui/filters/FilterBar.d.ts +2 -2
  26. package/dist/esm/modules/appCatalog/ui/filters/FilterBar.js.map +1 -1
  27. package/dist/esm/modules/appCatalog/ui/grid/AppCatalogGrid.d.ts +3 -3
  28. package/dist/esm/modules/appCatalog/ui/grid/AppCatalogGrid.js +19 -19
  29. package/dist/esm/modules/appCatalog/ui/grid/AppCatalogGrid.js.map +1 -1
  30. package/dist/esm/modules/appCatalog/ui/grid/AppCatalogTable.d.ts +2 -2
  31. package/dist/esm/modules/appCatalog/ui/grid/appCatalogUtils.d.ts +2 -2
  32. package/dist/esm/modules/appCatalog/ui/hooks/useKeyboardNavigation.d.ts +3 -3
  33. package/dist/esm/modules/appCatalog/ui/hooks/useKeyboardNavigation.js.map +1 -1
  34. package/dist/esm/modules/appCatalog/ui/pages/AppCatalogPage.js +20 -12
  35. package/dist/esm/modules/appCatalog/ui/pages/AppCatalogPage.js.map +1 -1
  36. package/dist/esm/modules/appCatalog/utils/resolveHelpers.d.ts +5 -2
  37. package/dist/esm/modules/appCatalog/utils/resolveHelpers.js +4 -4
  38. package/dist/esm/modules/appCatalog/utils/resolveHelpers.js.map +1 -1
  39. package/dist/esm/modules/appCatalog/utils/searchApps.d.ts +11 -6
  40. package/dist/esm/modules/appCatalog/utils/searchApps.js +15 -14
  41. package/dist/esm/modules/appCatalog/utils/searchApps.js.map +1 -1
  42. package/package.json +3 -3
  43. package/src/__tests__/integration/appCatalog.integration.test.ts +3 -3
  44. package/src/__tests__/integration/harness/MockBackendVerifier.ts +5 -5
  45. package/src/__tests__/integration/mock-backend/MockBackendConfigurer.ts +16 -12
  46. package/src/__tests__/integration/mock-backend/MockDb.ts +30 -22
  47. package/src/__tests__/modules/appCatalog/ScreenshotPreview.test.tsx +4 -5
  48. package/src/__tests__/modules/appCatalog/utils/searchApps.test.ts +28 -30
  49. package/src/__tests__/modules/gallery/EscapeDismissal.integration.test.tsx +3 -4
  50. package/src/modules/appCatalog/context/AppCatalogContext.tsx +4 -8
  51. package/src/modules/appCatalog/hooks/useAppCounts.ts +5 -5
  52. package/src/modules/appCatalog/ui/components/AccessRequestSection.tsx +2 -2
  53. package/src/modules/appCatalog/ui/components/AppDetailModal.tsx +10 -10
  54. package/src/modules/appCatalog/ui/components/ScreenshotGallery.tsx +2 -2
  55. package/src/modules/appCatalog/ui/components/SearchAndFilterHeader.tsx +6 -2
  56. package/src/modules/appCatalog/ui/components/SubResourcesSection.tsx +17 -17
  57. package/src/modules/appCatalog/ui/components/TierVariantsSection.tsx +2 -2
  58. package/src/modules/appCatalog/ui/filters/FilterBar.tsx +2 -2
  59. package/src/modules/appCatalog/ui/grid/AppCatalogGrid.tsx +34 -37
  60. package/src/modules/appCatalog/ui/grid/AppCatalogTable.tsx +3 -3
  61. package/src/modules/appCatalog/ui/grid/appCatalogUtils.ts +2 -2
  62. package/src/modules/appCatalog/ui/hooks/useKeyboardNavigation.ts +3 -3
  63. package/src/modules/appCatalog/ui/pages/AppCatalogPage.tsx +24 -14
  64. package/src/modules/appCatalog/utils/resolveHelpers.ts +13 -10
  65. package/src/modules/appCatalog/utils/searchApps.ts +36 -31
@@ -1,13 +1,8 @@
1
1
  import { describe, expect, it } from 'vitest'
2
- import type {
3
- AppForCatalog,
4
- SubResource,
5
- } from '@igstack/app-catalog-backend-core'
6
- import { searchApps } from '~/modules/appCatalog/utils/searchApps'
2
+ import type { Resource } from '@igstack/app-catalog-backend-core'
3
+ import { searchResources } from '~/modules/appCatalog/utils/searchApps'
7
4
 
8
- function makeApp(
9
- overrides: Partial<AppForCatalog> & { slug: string },
10
- ): AppForCatalog {
5
+ function makeApp(overrides: Partial<Resource> & { slug: string }): Resource {
11
6
  return {
12
7
  id: overrides.slug,
13
8
  displayName: overrides.slug,
@@ -15,10 +10,11 @@ function makeApp(
15
10
  }
16
11
  }
17
12
 
18
- function makeSubResource(
19
- overrides: Partial<SubResource> & { slug: string; appSlug: string },
20
- ): SubResource {
13
+ function makeChildResource(
14
+ overrides: Partial<Resource> & { slug: string; parentSlug: string },
15
+ ): Resource {
21
16
  return {
17
+ id: overrides.slug,
22
18
  displayName: overrides.slug,
23
19
  aliases: [],
24
20
  accessMaintainerGroupSlugs: [],
@@ -26,8 +22,8 @@ function makeSubResource(
26
22
  }
27
23
  }
28
24
 
29
- describe('searchApps', () => {
30
- const apps: AppForCatalog[] = [
25
+ describe('searchResources', () => {
26
+ const apps: Resource[] = [
31
27
  makeApp({
32
28
  slug: 'jira',
33
29
  displayName: 'Jira',
@@ -41,51 +37,53 @@ describe('searchApps', () => {
41
37
  makeApp({ slug: 'slack', displayName: 'Slack', description: 'Messaging' }),
42
38
  ]
43
39
 
44
- it('returns all apps when query is empty', () => {
45
- expect(searchApps(apps, '')).toHaveLength(3)
40
+ it('returns all root apps when query is empty', () => {
41
+ expect(searchResources(apps, '')).toHaveLength(3)
46
42
  })
47
43
 
48
44
  it('finds app by displayName', () => {
49
- const results = searchApps(apps, 'jira')
45
+ const results = searchResources(apps, 'jira')
50
46
  expect(results).toHaveLength(1)
51
47
  expect(results[0]!.slug).toBe('jira')
52
48
  })
53
49
 
54
- describe('sub-resource search', () => {
55
- const subResources: SubResource[] = [
56
- makeSubResource({
50
+ describe('child resource search', () => {
51
+ const childResources: Resource[] = [
52
+ makeChildResource({
57
53
  slug: 'aws-natera-pipelines-dev',
58
54
  displayName: 'natera-pipelines-biomarkers-ici-dev',
59
- appSlug: 'aws-console',
55
+ parentSlug: 'aws-console',
60
56
  aliases: ['043902793406'],
61
57
  }),
62
- makeSubResource({
58
+ makeChildResource({
63
59
  slug: 'aws-natera-infosec-dev',
64
60
  displayName: 'natera-infosec-dev',
65
- appSlug: 'aws-console',
61
+ parentSlug: 'aws-console',
66
62
  aliases: [],
67
63
  }),
68
64
  ]
69
65
 
70
- it('finds app by sub-resource displayName', () => {
71
- const results = searchApps(apps, 'pipelines biomarkers', subResources)
66
+ const allResources = [...apps, ...childResources]
67
+
68
+ it('finds app by child resource displayName', () => {
69
+ const results = searchResources(allResources, 'pipelines biomarkers')
72
70
  expect(results).toHaveLength(1)
73
71
  expect(results[0]!.slug).toBe('aws-console')
74
72
  })
75
73
 
76
- it('finds app by sub-resource alias (account ID)', () => {
77
- const results = searchApps(apps, '043902793406', subResources)
74
+ it('finds app by child resource alias (account ID)', () => {
75
+ const results = searchResources(allResources, '043902793406')
78
76
  expect(results).toHaveLength(1)
79
77
  expect(results[0]!.slug).toBe('aws-console')
80
78
  })
81
79
 
82
- it('does not match sub-resources when no subResources provided', () => {
83
- const results = searchApps(apps, '043902793406')
80
+ it('does not match child resources when none provided', () => {
81
+ const results = searchResources(apps, '043902793406')
84
82
  expect(results).toHaveLength(0)
85
83
  })
86
84
 
87
- it('direct app match ranks higher than sub-resource match', () => {
88
- const results = searchApps(apps, 'aws', subResources)
85
+ it('direct app match ranks higher than child resource match', () => {
86
+ const results = searchResources(allResources, 'aws')
89
87
  // 'aws-console' matches by displayName, should appear
90
88
  expect(results.length).toBeGreaterThanOrEqual(1)
91
89
  expect(results[0]!.slug).toBe('aws-console')
@@ -2,7 +2,7 @@ import { act, fireEvent, render, screen, waitFor } from '@testing-library/react'
2
2
  import { describe, expect, it, vi } from 'vitest'
3
3
  import '@testing-library/jest-dom/vitest'
4
4
 
5
- import type { AppForCatalog } from '@igstack/app-catalog-backend-core'
5
+ import type { Resource } from '@igstack/app-catalog-backend-core'
6
6
  import { AppDetailModal } from '~/modules/appCatalog/ui/components/AppDetailModal'
7
7
  import { AppCatalogContext } from '~/modules/appCatalog/context/AppCatalogContext'
8
8
  import type { AppCatalogContextIface } from '~/modules/appCatalog/context/AppCatalogContext'
@@ -13,16 +13,15 @@ import type { AppCatalogContextIface } from '~/modules/appCatalog/context/AppCat
13
13
  // Each Escape press dismisses only the innermost active layer.
14
14
 
15
15
  const minimalContext: AppCatalogContextIface = {
16
- apps: [],
16
+ resources: [],
17
17
  isLoadingApps: false,
18
18
  tagsDefinitions: [],
19
19
  approvalMethods: [],
20
20
  persons: [],
21
21
  groups: [],
22
- subResources: [],
23
22
  }
24
23
 
25
- function makeApp(): AppForCatalog {
24
+ function makeApp(): Resource {
26
25
  return {
27
26
  id: 'app-1',
28
27
  slug: 'test-app',
@@ -1,11 +1,10 @@
1
1
  import type {
2
2
  AppApprovalMethod,
3
- AppForCatalog,
4
3
  AppVersionInfo,
5
4
  Group,
6
5
  GroupingTagDefinition,
7
6
  Person,
8
- SubResource,
7
+ Resource,
9
8
  } from '@igstack/app-catalog-backend-core'
10
9
  import { useQuery } from '@tanstack/react-query'
11
10
  import type { ReactNode } from 'react'
@@ -14,13 +13,12 @@ import { ApiQueryMagazineAppCatalog } from '~/modules/appCatalog'
14
13
  import { useUiSettings } from '~/context/UiSettingsContext'
15
14
 
16
15
  export interface AppCatalogContextIface {
17
- apps: AppForCatalog[]
16
+ resources: Resource[]
18
17
  isLoadingApps: boolean
19
18
  tagsDefinitions: GroupingTagDefinition[]
20
19
  approvalMethods: AppApprovalMethod[]
21
20
  persons: Person[]
22
21
  groups: Group[]
23
- subResources?: SubResource[]
24
22
  versions?: AppVersionInfo
25
23
  }
26
24
 
@@ -40,13 +38,12 @@ export function AppCatalogProvider({ children }: AppCatalogProviderProps) {
40
38
 
41
39
  const contextValue = useMemo<AppCatalogContextIface>(
42
40
  () => ({
43
- apps: data?.apps ?? [],
41
+ resources: data?.resources ?? [],
44
42
  isLoadingApps,
45
43
  tagsDefinitions: data?.tagsDefinitions ?? [],
46
44
  approvalMethods: data?.approvalMethods ?? [],
47
45
  persons: data?.persons ?? [],
48
46
  groups: data?.groups ?? [],
49
- subResources: data?.subResources ?? [],
50
47
  versions: {
51
48
  ...data?.versions,
52
49
  ...(uiSettings.frontendBuildId && {
@@ -61,11 +58,10 @@ export function AppCatalogProvider({ children }: AppCatalogProviderProps) {
61
58
  }),
62
59
  [
63
60
  data?.approvalMethods,
64
- data?.apps,
61
+ data?.resources,
65
62
  data?.tagsDefinitions,
66
63
  data?.persons,
67
64
  data?.groups,
68
- data?.subResources,
69
65
  data?.versions,
70
66
  uiSettings.frontendBuildId,
71
67
  isLoadingApps,
@@ -1,10 +1,10 @@
1
1
  import { useMemo } from 'react'
2
- import type { AppForCatalog } from '@igstack/app-catalog-backend-core'
3
- import { searchApps } from '../utils/searchApps'
2
+ import type { Resource } from '@igstack/app-catalog-backend-core'
3
+ import { searchResources } from '../utils/searchApps'
4
4
  import { useAppCatalogFilters } from '../ui/context/AppCatalogFiltersContext'
5
5
 
6
6
  interface UseAppCountsOptions {
7
- apps: AppForCatalog[]
7
+ apps: Resource[]
8
8
  topAppSlugs: string[]
9
9
  searchValue: string
10
10
  }
@@ -31,7 +31,7 @@ export function useAppCounts({
31
31
  if (!filterState.showDeprecated) {
32
32
  recentApps = recentApps.filter((app) => !app.deprecated)
33
33
  }
34
- return searchApps(recentApps, searchValue).length
34
+ return searchResources(recentApps, searchValue).length
35
35
  }, [apps, topAppSlugs, searchValue, filterState.showDeprecated])
36
36
 
37
37
  // Count for "Show All" (respects showDeprecated, tag filters, and search)
@@ -57,7 +57,7 @@ export function useAppCounts({
57
57
  })
58
58
  }
59
59
 
60
- return searchApps(result, searchValue).length
60
+ return searchResources(result, searchValue).length
61
61
  }, [apps, filterState.tagFilters, filterState.showDeprecated, searchValue])
62
62
 
63
63
  return { recentCount, allCount, deprecatedCount }
@@ -1,6 +1,6 @@
1
1
  import type {
2
2
  AppApprovalMethod,
3
- AppForCatalog,
3
+ Resource,
4
4
  } from '@igstack/app-catalog-backend-core'
5
5
  import { Bot, Check, Copy, ExternalLink, Settings, Users } from 'lucide-react'
6
6
  import { useCallback, useEffect, useRef, useState } from 'react'
@@ -26,7 +26,7 @@ import { PersonBadge } from './PersonBadge'
26
26
  const COPY_FEEDBACK_DURATION = 2000
27
27
 
28
28
  interface AccessRequestSectionProps {
29
- app: AppForCatalog
29
+ app: Resource
30
30
  approvalMethods: AppApprovalMethod[]
31
31
  }
32
32
 
@@ -1,4 +1,4 @@
1
- import type { AppForCatalog } from '@igstack/app-catalog-backend-core'
1
+ import type { Resource } from '@igstack/app-catalog-backend-core'
2
2
  import { AppWindowIcon, ExternalLinkIcon, XIcon } from 'lucide-react'
3
3
  import React, { useEffect, useMemo, useState } from 'react'
4
4
  import { Badge } from '~/ui/badge'
@@ -19,10 +19,10 @@ import {
19
19
  import { ScreenshotGallery } from './ScreenshotGallery'
20
20
  import { TierVariantsSection } from './TierVariantsSection'
21
21
  import { SubResourcesSection } from './SubResourcesSection'
22
- import { getSubResourcesForApp } from '~/modules/appCatalog/utils/resolveHelpers'
22
+ import { getChildResources } from '~/modules/appCatalog/utils/resolveHelpers'
23
23
 
24
24
  export interface AppDetailModalProps {
25
- app: AppForCatalog
25
+ app: Resource
26
26
  isOpen: boolean
27
27
  onClose: () => void
28
28
  }
@@ -31,7 +31,7 @@ function getIconUrl(iconName: string): string {
31
31
  return `/api/icons/${iconName}`
32
32
  }
33
33
 
34
- function AppIcon({ app }: { app: AppForCatalog }) {
34
+ function AppIcon({ app }: { app: Resource }) {
35
35
  const [imageError, setImageError] = React.useState(false)
36
36
 
37
37
  if (app.iconName && !imageError) {
@@ -52,7 +52,7 @@ function AppIcon({ app }: { app: AppForCatalog }) {
52
52
  )
53
53
  }
54
54
 
55
- function ScreenshotPreview({ app }: { app: AppForCatalog }) {
55
+ function ScreenshotPreview({ app }: { app: Resource }) {
56
56
  const [imageErrors, setImageErrors] = useState<Set<string>>(() => new Set())
57
57
  const [galleryOpen, setGalleryOpen] = useState(false)
58
58
  const [initialIndex, setInitialIndex] = useState(0)
@@ -124,7 +124,7 @@ function ScreenshotPreview({ app }: { app: AppForCatalog }) {
124
124
  )
125
125
  }
126
126
 
127
- function AccessSection({ app }: { app: AppForCatalog }) {
127
+ function AccessSection({ app }: { app: Resource }) {
128
128
  const { approvalMethods } = useAppCatalogContext()
129
129
  const { accessRequest } = app
130
130
  if (!accessRequest) {
@@ -186,11 +186,11 @@ function AccessSection({ app }: { app: AppForCatalog }) {
186
186
  )
187
187
  }
188
188
 
189
- function TiersAndSubResources({ app }: { app: AppForCatalog }) {
190
- const { subResources } = useAppCatalogContext()
189
+ function TiersAndSubResources({ app }: { app: Resource }) {
190
+ const { resources } = useAppCatalogContext()
191
191
  const appSubResources = useMemo(
192
- () => getSubResourcesForApp(subResources ?? [], app.slug),
193
- [subResources, app.slug],
192
+ () => getChildResources(resources, app.slug),
193
+ [resources, app.slug],
194
194
  )
195
195
 
196
196
  return (
@@ -1,6 +1,6 @@
1
1
  import { useMemo, useRef } from 'react'
2
2
 
3
- import type { AppForCatalog } from '@igstack/app-catalog-backend-core'
3
+ import type { Resource } from '@igstack/app-catalog-backend-core'
4
4
 
5
5
  import { Gallery } from '~/modules/gallery/Gallery'
6
6
  import type { GalleryImage } from '~/modules/gallery/Gallery'
@@ -8,7 +8,7 @@ import { Dialog, DialogContent, DialogTitle } from '~/ui/dialog'
8
8
  import { VisuallyHidden } from '~/ui/visually-hidden'
9
9
 
10
10
  export interface ScreenshotGalleryProps {
11
- app: AppForCatalog
11
+ app: Resource
12
12
  screenshotIds: string[]
13
13
  initialIndex?: number
14
14
  open: boolean
@@ -1,4 +1,4 @@
1
- import { useDeferredValue, useEffect, useState } from 'react'
1
+ import { useDeferredValue, useEffect, useMemo, useState } from 'react'
2
2
  import { useAppCatalogContext } from '../../context/AppCatalogContext'
3
3
  import { useAppClickHistory } from '../../hooks/useAppClickHistory'
4
4
  import { useAppCounts } from '../../hooks/useAppCounts'
@@ -11,7 +11,11 @@ import { FilterBar } from '../filters/FilterBar'
11
11
  * Uses deferred search value to avoid blocking the input.
12
12
  */
13
13
  export function SearchAndFilterHeader() {
14
- const { apps } = useAppCatalogContext()
14
+ const { resources } = useAppCatalogContext()
15
+ const apps = useMemo(
16
+ () => resources.filter((r) => !r.parentSlug),
17
+ [resources],
18
+ )
15
19
  const { getTopApps } = useAppClickHistory()
16
20
  const { state } = useAppCatalogFilters()
17
21
 
@@ -1,4 +1,4 @@
1
- import type { SubResource } from '@igstack/app-catalog-backend-core'
1
+ import type { Resource } from '@igstack/app-catalog-backend-core'
2
2
  import { Search } from 'lucide-react'
3
3
  import { useMemo, useState } from 'react'
4
4
  import { Badge } from '~/ui/badge'
@@ -23,7 +23,7 @@ import { useAppCatalogContext } from '~/modules/appCatalog'
23
23
  import { getGroupBySlug } from '~/modules/appCatalog/utils/resolveHelpers'
24
24
 
25
25
  interface SubResourcesSectionProps {
26
- subResources: SubResource[]
26
+ subResources: Resource[]
27
27
  }
28
28
 
29
29
  function getTierBadgeVariant(
@@ -63,7 +63,7 @@ export function SubResourcesSection({
63
63
  const uniqueTiers = useMemo(() => {
64
64
  const tiers = new Set<string>()
65
65
  for (const sr of subResources) {
66
- if (sr.tierSlug) tiers.add(sr.tierSlug)
66
+ if (sr.tier) tiers.add(sr.tier)
67
67
  }
68
68
  return [...tiers].sort()
69
69
  }, [subResources])
@@ -72,7 +72,7 @@ export function SubResourcesSection({
72
72
  let result = subResources
73
73
 
74
74
  if (tierFilter !== 'all') {
75
- result = result.filter((sr) => sr.tierSlug === tierFilter)
75
+ result = result.filter((sr) => sr.tier === tierFilter)
76
76
  }
77
77
 
78
78
  if (search.trim()) {
@@ -80,7 +80,7 @@ export function SubResourcesSection({
80
80
  result = result.filter(
81
81
  (sr) =>
82
82
  sr.displayName.toLowerCase().includes(q) ||
83
- sr.aliases.some((a) => a.toLowerCase().includes(q)) ||
83
+ (sr.aliases ?? []).some((a) => a.toLowerCase().includes(q)) ||
84
84
  (sr.description?.toLowerCase().includes(q) ?? false),
85
85
  )
86
86
  }
@@ -150,12 +150,12 @@ export function SubResourcesSection({
150
150
  ) : (
151
151
  filtered.map((sr) => {
152
152
  // Resolve maintainer group members
153
- const maintainerMembers = sr.accessMaintainerGroupSlugs.flatMap(
154
- (groupSlug) => {
155
- const group = getGroupBySlug(groups, groupSlug)
156
- return group?.memberSlugs ?? []
157
- },
158
- )
153
+ const maintainerMembers = (
154
+ sr.accessMaintainerGroupSlugs ?? []
155
+ ).flatMap((groupSlug) => {
156
+ const group = getGroupBySlug(groups, groupSlug)
157
+ return group?.memberSlugs ?? []
158
+ })
159
159
  // Deduplicate
160
160
  const uniqueMaintainers = [...new Set(maintainerMembers)]
161
161
 
@@ -165,9 +165,9 @@ export function SubResourcesSection({
165
165
  <div className="font-medium text-sm">
166
166
  {sr.displayName}
167
167
  </div>
168
- {sr.aliases.length > 0 && (
168
+ {(sr.aliases ?? []).length > 0 && (
169
169
  <div className="text-xs text-muted-foreground mt-0.5">
170
- {sr.aliases.join(', ')}
170
+ {(sr.aliases ?? []).join(', ')}
171
171
  </div>
172
172
  )}
173
173
  {sr.description && (
@@ -177,12 +177,12 @@ export function SubResourcesSection({
177
177
  )}
178
178
  </TableCell>
179
179
  <TableCell>
180
- {sr.tierSlug && (
180
+ {sr.tier && (
181
181
  <Badge
182
- variant={getTierBadgeVariant(sr.tierSlug)}
183
- className={`text-xs ${getTierBadgeClassName(sr.tierSlug)}`}
182
+ variant={getTierBadgeVariant(sr.tier)}
183
+ className={`text-xs ${getTierBadgeClassName(sr.tier)}`}
184
184
  >
185
- {getTierDisplayLabel(sr.tierSlug)}
185
+ {getTierDisplayLabel(sr.tier)}
186
186
  </Badge>
187
187
  )}
188
188
  </TableCell>
@@ -1,7 +1,7 @@
1
1
  import type {
2
2
  AppAccessRequest,
3
3
  AppApprovalMethod,
4
- AppTierVariant,
4
+ TierVariant,
5
5
  } from '@igstack/app-catalog-backend-core'
6
6
  import { Bot, ExternalLinkIcon, Settings, Users } from 'lucide-react'
7
7
  import { useState } from 'react'
@@ -19,7 +19,7 @@ import { useAppCatalogContext } from '~/modules/appCatalog'
19
19
  import { PersonBadge } from './PersonBadge'
20
20
 
21
21
  interface TierVariantsSectionProps {
22
- tiers: AppTierVariant[]
22
+ tiers: TierVariant[]
23
23
  }
24
24
 
25
25
  function getTierBadgeVariant(
@@ -1,4 +1,4 @@
1
- import type { AppForCatalog } from '@igstack/app-catalog-backend-core'
1
+ import type { Resource } from '@igstack/app-catalog-backend-core'
2
2
  import { X } from 'lucide-react'
3
3
  import { useMemo } from 'react'
4
4
  import { Button } from '~/ui/button'
@@ -21,7 +21,7 @@ interface FilterBarProps {
21
21
  /** Number of deprecated apps (total) */
22
22
  deprecatedCount: number
23
23
  /** All apps for counting filter options */
24
- apps: AppForCatalog[]
24
+ apps: Resource[]
25
25
  }
26
26
 
27
27
  /**
@@ -1,6 +1,6 @@
1
1
  import type {
2
- AppForCatalog,
3
2
  GroupingTagDefinition,
3
+ Resource,
4
4
  } from '@igstack/app-catalog-backend-core'
5
5
  import type { ColumnDef } from '@tanstack/react-table'
6
6
  import {
@@ -39,13 +39,13 @@ import { useKeyboardNavigation } from '../hooks/useKeyboardNavigation'
39
39
  import { highlightText } from '../../utils/searchApps'
40
40
  import { TierVariantsSection } from '../components/TierVariantsSection'
41
41
  import { SubResourcesSection } from '../components/SubResourcesSection'
42
- import { getSubResourcesForApp } from '../../utils/resolveHelpers'
42
+ import { getChildResources } from '../../utils/resolveHelpers'
43
43
 
44
44
  export interface AppCatalogGridProps {
45
- apps: AppForCatalog[]
45
+ apps: Resource[]
46
46
  selectedAppSlug?: string
47
47
  groupingDefinition?: GroupingTagDefinition
48
- onAppClick?: (app: AppForCatalog) => void
48
+ onAppClick?: (app: Resource) => void
49
49
  /** Whether search is active (affects group sorting) */
50
50
  hasSearch?: boolean
51
51
  /** Search query for highlighting matches */
@@ -91,13 +91,7 @@ function HighlightedText({
91
91
  )
92
92
  }
93
93
 
94
- function AppIcon({
95
- app,
96
- className,
97
- }: {
98
- app: AppForCatalog
99
- className?: string
100
- }) {
94
+ function AppIcon({ app, className }: { app: Resource; className?: string }) {
101
95
  const [imageError, setImageError] = React.useState(false)
102
96
 
103
97
  // Use iconName from backend if available
@@ -127,7 +121,7 @@ function AppIcon({
127
121
  )
128
122
  }
129
123
 
130
- function AppScreenshot({ app }: { app: AppForCatalog }) {
124
+ function AppScreenshot({ app }: { app: Resource }) {
131
125
  const [imageError, setImageError] = React.useState(false)
132
126
  const [isLoadingImage, setIsLoadingImage] = React.useState(true)
133
127
 
@@ -172,11 +166,11 @@ function AppScreenshot({ app }: { app: AppForCatalog }) {
172
166
  )
173
167
  }
174
168
 
175
- function TiersAndSubResourcesPanel({ app }: { app: AppForCatalog }) {
176
- const { subResources } = useAppCatalogContext()
169
+ function TiersAndSubResourcesPanel({ app }: { app: Resource }) {
170
+ const { resources } = useAppCatalogContext()
177
171
  const appSubResources = React.useMemo(
178
- () => getSubResourcesForApp(subResources ?? [], app.slug),
179
- [subResources, app.slug],
172
+ () => getChildResources(resources, app.slug),
173
+ [resources, app.slug],
180
174
  )
181
175
 
182
176
  return (
@@ -200,13 +194,13 @@ function AppDetails({
200
194
  onAppClick,
201
195
  onClosePanel,
202
196
  }: {
203
- app: AppForCatalog
204
- onAppClick?: (app: AppForCatalog) => void
197
+ app: Resource
198
+ onAppClick?: (app: Resource) => void
205
199
  onClosePanel: () => void
206
200
  }) {
207
201
  const [isGalleryOpen, setIsGalleryOpen] = React.useState(false)
208
202
  const [galleryInitialIndex, setGalleryInitialIndex] = React.useState(0)
209
- const { approvalMethods, apps } = useAppCatalogContext()
203
+ const { approvalMethods, resources: allResources } = useAppCatalogContext()
210
204
  const { recordClick } = useAppClickHistory()
211
205
  const updateApp = useUpdateApp()
212
206
  const [draftSource, setDraftSource] = React.useState<string | null>(null)
@@ -258,7 +252,7 @@ function AppDetails({
258
252
 
259
253
  // Find replacement app if deprecated
260
254
  const replacementApp = app.deprecated?.replacementSlug
261
- ? apps.find((a) => a.slug === app.deprecated?.replacementSlug)
255
+ ? allResources.find((a) => a.slug === app.deprecated?.replacementSlug)
262
256
  : null
263
257
 
264
258
  return (
@@ -621,11 +615,11 @@ function AppDetails({
621
615
 
622
616
  interface GroupedApps {
623
617
  groupName: string
624
- apps: AppForCatalog[]
618
+ apps: Resource[]
625
619
  }
626
620
 
627
621
  function groupApps(
628
- apps: AppForCatalog[],
622
+ apps: Resource[],
629
623
  groupingDef?: GroupingTagDefinition,
630
624
  hasSearch?: boolean,
631
625
  ): GroupedApps[] {
@@ -642,8 +636,8 @@ function groupApps(
642
636
  return [{ groupName: 'All Apps', apps: sortedApps }]
643
637
  }
644
638
 
645
- const grouped = new Map<string, AppForCatalog[]>()
646
- const ungrouped: AppForCatalog[] = []
639
+ const grouped = new Map<string, Resource[]>()
640
+ const ungrouped: Resource[] = []
647
641
 
648
642
  for (const app of apps) {
649
643
  const matchingTag = app.tags?.find((tag) =>
@@ -720,11 +714,11 @@ export function AppCatalogGrid({
720
714
  onAppClick,
721
715
  })
722
716
 
723
- // Build a map of appSlug -> matched sub-resource displayName for search annotation
724
- const { subResources: allSubResources } = useAppCatalogContext()
717
+ // Build a map of parentSlug -> matched child resource displayName for search annotation
718
+ const { resources: allResources2 } = useAppCatalogContext()
725
719
  const matchedSubResourceMap = React.useMemo(() => {
726
720
  const map = new Map<string, string>()
727
- if (!searchQuery?.trim() || !allSubResources?.length) return map
721
+ if (!searchQuery?.trim() || allResources2.length === 0) return map
728
722
  const queryTerms = searchQuery
729
723
  .trim()
730
724
  .toLowerCase()
@@ -733,22 +727,25 @@ export function AppCatalogGrid({
733
727
  const allTermsMatch = (text: string): boolean =>
734
728
  queryTerms.every((term) => text.includes(term))
735
729
 
736
- for (const sr of allSubResources) {
737
- if (map.has(sr.appSlug)) continue
738
- const nameMatch = allTermsMatch(sr.displayName.toLowerCase())
739
- const aliasMatch = sr.aliases.some((a) => allTermsMatch(a.toLowerCase()))
740
- const descMatch = sr.description
741
- ? allTermsMatch(sr.description.toLowerCase())
730
+ for (const r of allResources2) {
731
+ if (!r.parentSlug) continue
732
+ if (map.has(r.parentSlug)) continue
733
+ const nameMatch = allTermsMatch(r.displayName.toLowerCase())
734
+ const aliasMatch = (r.aliases ?? []).some((a) =>
735
+ allTermsMatch(a.toLowerCase()),
736
+ )
737
+ const descMatch = r.description
738
+ ? allTermsMatch(r.description.toLowerCase())
742
739
  : false
743
740
  if (nameMatch || aliasMatch || descMatch) {
744
- map.set(sr.appSlug, sr.displayName)
741
+ map.set(r.parentSlug, r.displayName)
745
742
  }
746
743
  }
747
744
  return map
748
- }, [searchQuery, allSubResources])
745
+ }, [searchQuery, allResources2])
749
746
 
750
747
  // Define columns
751
- const columns = React.useMemo<ColumnDef<AppForCatalog>[]>(
748
+ const columns = React.useMemo<ColumnDef<Resource>[]>(
752
749
  () => [
753
750
  {
754
751
  id: 'application',
@@ -856,7 +853,7 @@ export function AppCatalogGrid({
856
853
  }
857
854
  }, [selectedAppSlug, rowRefs])
858
855
 
859
- const handleAppClick = (app: AppForCatalog) => {
856
+ const handleAppClick = (app: Resource) => {
860
857
  onAppClick?.(app)
861
858
  }
862
859
 
@@ -1,4 +1,4 @@
1
- import type { AppForCatalog } from '@igstack/app-catalog-backend-core'
1
+ import type { Resource } from '@igstack/app-catalog-backend-core'
2
2
  import type { ColumnDef } from '@tanstack/react-table'
3
3
  import {
4
4
  flexRender,
@@ -17,11 +17,11 @@ import {
17
17
  import { getAppUrl } from './appCatalogUtils'
18
18
 
19
19
  export interface AppCatalogTableProps {
20
- apps: AppForCatalog[]
20
+ apps: Resource[]
21
21
  }
22
22
 
23
23
  export function AppCatalogTable({ apps }: AppCatalogTableProps) {
24
- const columns = useMemo<ColumnDef<AppForCatalog>[]>(
24
+ const columns = useMemo<ColumnDef<Resource>[]>(
25
25
  () => [
26
26
  {
27
27
  slug: 'name',
@@ -1,5 +1,5 @@
1
- import type { AppForCatalog } from '@igstack/app-catalog-backend-core'
1
+ import type { Resource } from '@igstack/app-catalog-backend-core'
2
2
 
3
- export function getAppUrl(app: AppForCatalog): string {
3
+ export function getAppUrl(app: Resource): string {
4
4
  return app.appUrl || '#'
5
5
  }