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

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 (67) 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 +1 -13
  14. package/dist/esm/modules/appCatalog/ui/components/AccessRequestSection.js.map +1 -1
  15. package/dist/esm/modules/appCatalog/ui/components/AppDetailModal.d.ts +2 -2
  16. package/dist/esm/modules/appCatalog/ui/components/MarkdownText.d.ts +18 -0
  17. package/dist/esm/modules/appCatalog/ui/components/MarkdownText.js +19 -0
  18. package/dist/esm/modules/appCatalog/ui/components/MarkdownText.js.map +1 -0
  19. package/dist/esm/modules/appCatalog/ui/components/ScreenshotGallery.d.ts +2 -2
  20. package/dist/esm/modules/appCatalog/ui/components/ScreenshotGallery.js.map +1 -1
  21. package/dist/esm/modules/appCatalog/ui/components/SubResourcesSection.d.ts +2 -2
  22. package/dist/esm/modules/appCatalog/ui/components/SubResourcesSection.js +12 -14
  23. package/dist/esm/modules/appCatalog/ui/components/SubResourcesSection.js.map +1 -1
  24. package/dist/esm/modules/appCatalog/ui/components/TierVariantsSection.d.ts +2 -2
  25. package/dist/esm/modules/appCatalog/ui/components/TierVariantsSection.js.map +1 -1
  26. package/dist/esm/modules/appCatalog/ui/filters/FilterBar.d.ts +2 -2
  27. package/dist/esm/modules/appCatalog/ui/filters/FilterBar.js.map +1 -1
  28. package/dist/esm/modules/appCatalog/ui/grid/AppCatalogGrid.d.ts +3 -3
  29. package/dist/esm/modules/appCatalog/ui/grid/AppCatalogGrid.js +19 -19
  30. package/dist/esm/modules/appCatalog/ui/grid/AppCatalogGrid.js.map +1 -1
  31. package/dist/esm/modules/appCatalog/ui/grid/AppCatalogTable.d.ts +2 -2
  32. package/dist/esm/modules/appCatalog/ui/grid/appCatalogUtils.d.ts +2 -2
  33. package/dist/esm/modules/appCatalog/ui/hooks/useKeyboardNavigation.d.ts +3 -3
  34. package/dist/esm/modules/appCatalog/ui/hooks/useKeyboardNavigation.js.map +1 -1
  35. package/dist/esm/modules/appCatalog/ui/pages/AppCatalogPage.js +20 -12
  36. package/dist/esm/modules/appCatalog/ui/pages/AppCatalogPage.js.map +1 -1
  37. package/dist/esm/modules/appCatalog/utils/resolveHelpers.d.ts +5 -2
  38. package/dist/esm/modules/appCatalog/utils/resolveHelpers.js +4 -4
  39. package/dist/esm/modules/appCatalog/utils/resolveHelpers.js.map +1 -1
  40. package/dist/esm/modules/appCatalog/utils/searchApps.d.ts +11 -6
  41. package/dist/esm/modules/appCatalog/utils/searchApps.js +15 -14
  42. package/dist/esm/modules/appCatalog/utils/searchApps.js.map +1 -1
  43. package/package.json +3 -3
  44. package/src/__tests__/integration/appCatalog.integration.test.ts +3 -3
  45. package/src/__tests__/integration/harness/MockBackendVerifier.ts +5 -5
  46. package/src/__tests__/integration/mock-backend/MockBackendConfigurer.ts +16 -12
  47. package/src/__tests__/integration/mock-backend/MockDb.ts +30 -22
  48. package/src/__tests__/modules/appCatalog/ScreenshotPreview.test.tsx +4 -5
  49. package/src/__tests__/modules/appCatalog/utils/searchApps.test.ts +28 -30
  50. package/src/__tests__/modules/gallery/EscapeDismissal.integration.test.tsx +3 -4
  51. package/src/modules/appCatalog/context/AppCatalogContext.tsx +4 -8
  52. package/src/modules/appCatalog/hooks/useAppCounts.ts +5 -5
  53. package/src/modules/appCatalog/ui/components/AccessRequestSection.tsx +3 -20
  54. package/src/modules/appCatalog/ui/components/AppDetailModal.tsx +14 -11
  55. package/src/modules/appCatalog/ui/components/MarkdownText.tsx +42 -0
  56. package/src/modules/appCatalog/ui/components/ScreenshotGallery.tsx +2 -2
  57. package/src/modules/appCatalog/ui/components/SearchAndFilterHeader.tsx +6 -2
  58. package/src/modules/appCatalog/ui/components/SubResourcesSection.tsx +17 -17
  59. package/src/modules/appCatalog/ui/components/TierVariantsSection.tsx +2 -2
  60. package/src/modules/appCatalog/ui/filters/FilterBar.tsx +2 -2
  61. package/src/modules/appCatalog/ui/grid/AppCatalogGrid.tsx +34 -37
  62. package/src/modules/appCatalog/ui/grid/AppCatalogTable.tsx +3 -3
  63. package/src/modules/appCatalog/ui/grid/appCatalogUtils.ts +2 -2
  64. package/src/modules/appCatalog/ui/hooks/useKeyboardNavigation.ts +3 -3
  65. package/src/modules/appCatalog/ui/pages/AppCatalogPage.tsx +24 -14
  66. package/src/modules/appCatalog/utils/resolveHelpers.ts +13 -10
  67. 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'
@@ -20,34 +20,17 @@ import {
20
20
  TableHeader,
21
21
  TableRow,
22
22
  } from '~/ui/table'
23
+ import { MarkdownLink } from './MarkdownText'
23
24
  import { PersonBadge } from './PersonBadge'
24
25
 
25
26
  // Constants
26
27
  const COPY_FEEDBACK_DURATION = 2000
27
28
 
28
29
  interface AccessRequestSectionProps {
29
- app: AppForCatalog
30
+ app: Resource
30
31
  approvalMethods: AppApprovalMethod[]
31
32
  }
32
33
 
33
- // Component for rendering markdown links with security attributes
34
- const MarkdownLink = ({
35
- href,
36
- children,
37
- }: {
38
- href?: string
39
- children?: React.ReactNode
40
- }) => (
41
- <a
42
- href={href}
43
- target="_blank"
44
- rel="noopener noreferrer"
45
- className="text-primary hover:underline"
46
- >
47
- {children}
48
- </a>
49
- )
50
-
51
34
  // Helper function for approval method icons
52
35
  function getApprovalMethodIcon(
53
36
  type: 'service' | 'personTeam' | 'custom' | 'noAccessRequired' | 'unknown',
@@ -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'
@@ -16,13 +16,14 @@ import {
16
16
  TableHeader,
17
17
  TableRow,
18
18
  } from '~/ui/table'
19
+ import { MarkdownText } from './MarkdownText'
19
20
  import { ScreenshotGallery } from './ScreenshotGallery'
20
21
  import { TierVariantsSection } from './TierVariantsSection'
21
22
  import { SubResourcesSection } from './SubResourcesSection'
22
- import { getSubResourcesForApp } from '~/modules/appCatalog/utils/resolveHelpers'
23
+ import { getChildResources } from '~/modules/appCatalog/utils/resolveHelpers'
23
24
 
24
25
  export interface AppDetailModalProps {
25
- app: AppForCatalog
26
+ app: Resource
26
27
  isOpen: boolean
27
28
  onClose: () => void
28
29
  }
@@ -31,7 +32,7 @@ function getIconUrl(iconName: string): string {
31
32
  return `/api/icons/${iconName}`
32
33
  }
33
34
 
34
- function AppIcon({ app }: { app: AppForCatalog }) {
35
+ function AppIcon({ app }: { app: Resource }) {
35
36
  const [imageError, setImageError] = React.useState(false)
36
37
 
37
38
  if (app.iconName && !imageError) {
@@ -52,7 +53,7 @@ function AppIcon({ app }: { app: AppForCatalog }) {
52
53
  )
53
54
  }
54
55
 
55
- function ScreenshotPreview({ app }: { app: AppForCatalog }) {
56
+ function ScreenshotPreview({ app }: { app: Resource }) {
56
57
  const [imageErrors, setImageErrors] = useState<Set<string>>(() => new Set())
57
58
  const [galleryOpen, setGalleryOpen] = useState(false)
58
59
  const [initialIndex, setInitialIndex] = useState(0)
@@ -124,7 +125,7 @@ function ScreenshotPreview({ app }: { app: AppForCatalog }) {
124
125
  )
125
126
  }
126
127
 
127
- function AccessSection({ app }: { app: AppForCatalog }) {
128
+ function AccessSection({ app }: { app: Resource }) {
128
129
  const { approvalMethods } = useAppCatalogContext()
129
130
  const { accessRequest } = app
130
131
  if (!accessRequest) {
@@ -186,11 +187,11 @@ function AccessSection({ app }: { app: AppForCatalog }) {
186
187
  )
187
188
  }
188
189
 
189
- function TiersAndSubResources({ app }: { app: AppForCatalog }) {
190
- const { subResources } = useAppCatalogContext()
190
+ function TiersAndSubResources({ app }: { app: Resource }) {
191
+ const { resources } = useAppCatalogContext()
191
192
  const appSubResources = useMemo(
192
- () => getSubResourcesForApp(subResources ?? [], app.slug),
193
- [subResources, app.slug],
193
+ () => getChildResources(resources, app.slug),
194
+ [resources, app.slug],
194
195
  )
195
196
 
196
197
  return (
@@ -286,7 +287,9 @@ export function AppDetailModal({ app, isOpen, onClose }: AppDetailModalProps) {
286
287
  {app.description && (
287
288
  <div className="space-y-2">
288
289
  <div className="text-sm font-medium">Description</div>
289
- <p className="text-muted-foreground">{app.description}</p>
290
+ <MarkdownText className="prose prose-sm max-w-none text-muted-foreground [&_p]:m-0">
291
+ {app.description}
292
+ </MarkdownText>
290
293
  </div>
291
294
  )}
292
295
 
@@ -0,0 +1,42 @@
1
+ import type React from 'react'
2
+ import ReactMarkdown from 'react-markdown'
3
+
4
+ /**
5
+ * Renders a markdown link with security attributes (opens in a new tab,
6
+ * no referrer/opener leakage). Shared so all catalog text fields render
7
+ * links identically.
8
+ */
9
+ export const MarkdownLink = ({
10
+ href,
11
+ children,
12
+ }: {
13
+ href?: string
14
+ children?: React.ReactNode
15
+ }) => (
16
+ <a
17
+ href={href}
18
+ target="_blank"
19
+ rel="noopener noreferrer"
20
+ className="text-primary hover:underline"
21
+ >
22
+ {children}
23
+ </a>
24
+ )
25
+
26
+ /**
27
+ * Renders user-facing catalog text (descriptions, comments, prompts) as
28
+ * markdown so links become clickable. Links use {@link MarkdownLink}.
29
+ */
30
+ export function MarkdownText({
31
+ children,
32
+ className,
33
+ }: {
34
+ children: string
35
+ className?: string
36
+ }) {
37
+ return (
38
+ <span className={className}>
39
+ <ReactMarkdown components={{ a: MarkdownLink }}>{children}</ReactMarkdown>
40
+ </span>
41
+ )
42
+ }
@@ -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
  /**