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

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 (62) 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 +2 -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/ScreenshotGallery.d.ts +2 -2
  16. package/dist/esm/modules/appCatalog/ui/components/ScreenshotGallery.js.map +1 -1
  17. package/dist/esm/modules/appCatalog/ui/components/SubResourcesSection.d.ts +2 -2
  18. package/dist/esm/modules/appCatalog/ui/components/SubResourcesSection.js +12 -14
  19. package/dist/esm/modules/appCatalog/ui/components/SubResourcesSection.js.map +1 -1
  20. package/dist/esm/modules/appCatalog/ui/components/TierVariantsSection.d.ts +2 -2
  21. package/dist/esm/modules/appCatalog/ui/components/TierVariantsSection.js.map +1 -1
  22. package/dist/esm/modules/appCatalog/ui/filters/FilterBar.d.ts +2 -2
  23. package/dist/esm/modules/appCatalog/ui/filters/FilterBar.js.map +1 -1
  24. package/dist/esm/modules/appCatalog/ui/grid/AppCatalogGrid.d.ts +3 -3
  25. package/dist/esm/modules/appCatalog/ui/grid/AppCatalogGrid.js +19 -19
  26. package/dist/esm/modules/appCatalog/ui/grid/AppCatalogGrid.js.map +1 -1
  27. package/dist/esm/modules/appCatalog/ui/grid/AppCatalogTable.d.ts +2 -2
  28. package/dist/esm/modules/appCatalog/ui/grid/appCatalogUtils.d.ts +2 -2
  29. package/dist/esm/modules/appCatalog/ui/hooks/useKeyboardNavigation.d.ts +3 -3
  30. package/dist/esm/modules/appCatalog/ui/hooks/useKeyboardNavigation.js.map +1 -1
  31. package/dist/esm/modules/appCatalog/ui/pages/AppCatalogPage.js +20 -12
  32. package/dist/esm/modules/appCatalog/ui/pages/AppCatalogPage.js.map +1 -1
  33. package/dist/esm/modules/appCatalog/utils/resolveHelpers.d.ts +5 -2
  34. package/dist/esm/modules/appCatalog/utils/resolveHelpers.js +4 -4
  35. package/dist/esm/modules/appCatalog/utils/resolveHelpers.js.map +1 -1
  36. package/dist/esm/modules/appCatalog/utils/searchApps.d.ts +11 -6
  37. package/dist/esm/modules/appCatalog/utils/searchApps.js +15 -14
  38. package/dist/esm/modules/appCatalog/utils/searchApps.js.map +1 -1
  39. package/package.json +3 -3
  40. package/src/__tests__/integration/appCatalog.integration.test.ts +3 -3
  41. package/src/__tests__/integration/harness/MockBackendVerifier.ts +5 -5
  42. package/src/__tests__/integration/mock-backend/MockBackendConfigurer.ts +16 -12
  43. package/src/__tests__/integration/mock-backend/MockDb.ts +30 -22
  44. package/src/__tests__/modules/appCatalog/ScreenshotPreview.test.tsx +4 -5
  45. package/src/__tests__/modules/appCatalog/utils/searchApps.test.ts +28 -30
  46. package/src/__tests__/modules/gallery/EscapeDismissal.integration.test.tsx +3 -4
  47. package/src/modules/appCatalog/context/AppCatalogContext.tsx +4 -8
  48. package/src/modules/appCatalog/hooks/useAppCounts.ts +5 -5
  49. package/src/modules/appCatalog/ui/components/AccessRequestSection.tsx +2 -2
  50. package/src/modules/appCatalog/ui/components/AppDetailModal.tsx +10 -10
  51. package/src/modules/appCatalog/ui/components/ScreenshotGallery.tsx +2 -2
  52. package/src/modules/appCatalog/ui/components/SearchAndFilterHeader.tsx +6 -2
  53. package/src/modules/appCatalog/ui/components/SubResourcesSection.tsx +17 -17
  54. package/src/modules/appCatalog/ui/components/TierVariantsSection.tsx +2 -2
  55. package/src/modules/appCatalog/ui/filters/FilterBar.tsx +2 -2
  56. package/src/modules/appCatalog/ui/grid/AppCatalogGrid.tsx +34 -37
  57. package/src/modules/appCatalog/ui/grid/AppCatalogTable.tsx +3 -3
  58. package/src/modules/appCatalog/ui/grid/appCatalogUtils.ts +2 -2
  59. package/src/modules/appCatalog/ui/hooks/useKeyboardNavigation.ts +3 -3
  60. package/src/modules/appCatalog/ui/pages/AppCatalogPage.tsx +24 -14
  61. package/src/modules/appCatalog/utils/resolveHelpers.ts +13 -10
  62. package/src/modules/appCatalog/utils/searchApps.ts +36 -31
@@ -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
  }
@@ -1,10 +1,10 @@
1
- import type { AppForCatalog } from '@igstack/app-catalog-backend-core'
1
+ import type { Resource } from '@igstack/app-catalog-backend-core'
2
2
  import React from 'react'
3
3
 
4
4
  export interface UseKeyboardNavigationProps {
5
- apps: AppForCatalog[]
5
+ apps: Resource[]
6
6
  selectedAppSlug?: string
7
- onAppClick?: (app: AppForCatalog) => void
7
+ onAppClick?: (app: Resource) => void
8
8
  }
9
9
 
10
10
  export function useKeyboardNavigation({
@@ -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 { useDeferredValue, useEffect, useMemo, useState } from 'react'
4
4
  import { Button } from '~/ui/button'
@@ -14,15 +14,14 @@ import { useAppCatalogContext } from '../../context/AppCatalogContext'
14
14
  import { useAppClickHistory } from '../../hooks/useAppClickHistory'
15
15
  import { useAppCounts } from '../../hooks/useAppCounts'
16
16
  import { useUrlSyncedState } from '../../hooks/useUrlSyncedState'
17
- import { searchApps } from '../../utils/searchApps'
17
+ import { searchResources } from '../../utils/searchApps'
18
18
  import { OnboardingCard } from '../components/OnboardingCard'
19
19
  import { useAppCatalogFilters } from '../context/AppCatalogFiltersContext'
20
20
  import { FilterBar } from '../filters/FilterBar'
21
21
  import { AppCatalogGrid } from '../grid/AppCatalogGrid'
22
22
 
23
23
  export function AppCatalogPage() {
24
- const { apps, isLoadingApps, tagsDefinitions, subResources } =
25
- useAppCatalogContext()
24
+ const { resources, isLoadingApps, tagsDefinitions } = useAppCatalogContext()
26
25
  const { state: filterState, actions } = useAppCatalogFilters()
27
26
  const { getTopApps } = useAppClickHistory()
28
27
 
@@ -49,8 +48,14 @@ export function AppCatalogPage() {
49
48
  void getTopApps(10).then(setTopAppSlugs)
50
49
  }, [getTopApps])
51
50
 
51
+ // Get root resources for filtering (children handled internally by searchResources)
52
+ const rootResources = useMemo(
53
+ () => resources.filter((r) => !r.parentSlug),
54
+ [resources],
55
+ )
56
+
52
57
  const filteredApps = useMemo(() => {
53
- let result = apps
58
+ let result = rootResources
54
59
 
55
60
  // Step 1: Filter deprecated apps (if not showing them)
56
61
  if (!filterState.showDeprecated) {
@@ -76,22 +81,27 @@ export function AppCatalogPage() {
76
81
  }
77
82
 
78
83
  // Step 3: Apply search (using deferred value)
79
- result = searchApps(result, deferredSearchValue, subResources)
84
+ // Pass all resources so children contribute to parent scoring
85
+ const childResources = resources.filter((r) => r.parentSlug)
86
+ result = searchResources(
87
+ [...result, ...childResources],
88
+ deferredSearchValue,
89
+ )
80
90
 
81
91
  return result
82
92
  }, [
83
- apps,
93
+ rootResources,
94
+ resources,
84
95
  deferredSearchValue,
85
96
  filterState.recentMode,
86
97
  filterState.tagFilters,
87
98
  filterState.showDeprecated,
88
99
  topAppSlugs,
89
- subResources,
90
100
  ])
91
101
 
92
102
  // Calculate counts for FilterBar
93
103
  const { allCount, recentCount, deprecatedCount } = useAppCounts({
94
- apps,
104
+ apps: rootResources,
95
105
  topAppSlugs,
96
106
  searchValue: deferredSearchValue,
97
107
  })
@@ -103,7 +113,7 @@ export function AppCatalogPage() {
103
113
  }
104
114
  }, [filteredApps, setSelectedAppSlug])
105
115
 
106
- const handleAppClick = (app: AppForCatalog) => {
116
+ const handleAppClick = (app: Resource) => {
107
117
  setSelectedAppSlug(app.slug)
108
118
  }
109
119
 
@@ -115,12 +125,12 @@ export function AppCatalogPage() {
115
125
 
116
126
  // Calculate total apps count (respecting showDeprecated setting)
117
127
  const totalAppsCount = useMemo(() => {
118
- let count = apps.length
128
+ let count = rootResources.length
119
129
  if (!filterState.showDeprecated) {
120
- count = apps.filter((app) => !app.deprecated).length
130
+ count = rootResources.filter((app) => !app.deprecated).length
121
131
  }
122
132
  return count
123
- }, [apps, filterState.showDeprecated])
133
+ }, [rootResources, filterState.showDeprecated])
124
134
 
125
135
  if (isLoadingApps) {
126
136
  return <div className="py-6 text-muted-foreground">Loading…</div>
@@ -140,7 +150,7 @@ export function AppCatalogPage() {
140
150
  totalCount={allCount}
141
151
  recentCount={recentCount}
142
152
  deprecatedCount={deprecatedCount}
143
- apps={apps}
153
+ apps={rootResources}
144
154
  />
145
155
  </div>
146
156
 
@@ -1,8 +1,4 @@
1
- import type {
2
- Group,
3
- Person,
4
- SubResource,
5
- } from '@igstack/app-catalog-backend-core'
1
+ import type { Group, Person, Resource } from '@igstack/app-catalog-backend-core'
6
2
 
7
3
  export function getPersonBySlug(
8
4
  persons: Person[],
@@ -18,9 +14,16 @@ export function getGroupBySlug(
18
14
  return groups.find((g) => g.slug === slug)
19
15
  }
20
16
 
21
- export function getSubResourcesForApp(
22
- subResources: SubResource[],
23
- appSlug: string,
24
- ): SubResource[] {
25
- return subResources.filter((sr) => sr.appSlug === appSlug)
17
+ export function getChildResources(
18
+ resources: Resource[],
19
+ parentSlug: string,
20
+ ): Resource[] {
21
+ return resources.filter((r) => r.parentSlug === parentSlug)
26
22
  }
23
+
24
+ export function getRootResources(resources: Resource[]): Resource[] {
25
+ return resources.filter((r) => !r.parentSlug)
26
+ }
27
+
28
+ /** @deprecated Use getChildResources instead */
29
+ export const getSubResourcesForApp = getChildResources