@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,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
@@ -1,7 +1,4 @@
1
- import type {
2
- AppForCatalog,
3
- SubResource,
4
- } from '@igstack/app-catalog-backend-core'
1
+ import type { Resource } from '@igstack/app-catalog-backend-core'
5
2
 
6
3
  export interface SearchMatch {
7
4
  /** Field where the match occurred */
@@ -19,12 +16,15 @@ export interface SearchMatch {
19
16
  }
20
17
 
21
18
  export interface SearchResult {
22
- app: AppForCatalog
19
+ app: Resource
23
20
  match: SearchMatch
24
21
  }
25
22
 
26
23
  /**
27
- * Search and sort apps by relevance with highlighting support.
24
+ * Search and sort resources by relevance with highlighting support.
25
+ * Only root resources (no parentSlug) are scored and returned.
26
+ * Child resources (with parentSlug) contribute to their parent's score.
27
+ *
28
28
  * Priority order:
29
29
  * 0. Exact match in abbreviation
30
30
  * 1. Exact match in displayName
@@ -41,19 +41,21 @@ export interface SearchResult {
41
41
  * 12. Teams
42
42
  * 13. Description
43
43
  *
44
- * @param apps - Array of apps to search
44
+ * @param resources - Array of all resources (root + children)
45
45
  * @param searchQuery - Search query string
46
- * @returns Filtered and sorted array of apps with search results
46
+ * @returns Filtered and sorted array of root resources
47
47
  */
48
- export function searchApps(
49
- apps: AppForCatalog[],
48
+ export function searchResources(
49
+ resources: Resource[],
50
50
  searchQuery: string,
51
- subResources?: SubResource[],
52
- ): AppForCatalog[] {
51
+ ): Resource[] {
53
52
  const normalizedQuery = searchQuery.trim().toLowerCase()
54
53
 
54
+ // Separate root resources from children
55
+ const rootResources = resources.filter((r) => !r.parentSlug)
56
+
55
57
  if (normalizedQuery === '') {
56
- return apps
58
+ return rootResources
57
59
  }
58
60
 
59
61
  // Split query into terms for multi-word matching (AND logic)
@@ -63,18 +65,18 @@ export function searchApps(
63
65
  const allTermsMatch = (text: string): boolean =>
64
66
  queryTerms.every((term) => text.includes(term))
65
67
 
66
- // Build sub-resource lookup: appSlug -> SubResource[]
67
- const subResourcesByApp = new Map<string, SubResource[]>()
68
- if (subResources) {
69
- for (const sr of subResources) {
70
- const list = subResourcesByApp.get(sr.appSlug) ?? []
71
- list.push(sr)
72
- subResourcesByApp.set(sr.appSlug, list)
68
+ // Build children lookup: parentSlug -> Resource[]
69
+ const childrenByParent = new Map<string, Resource[]>()
70
+ for (const r of resources) {
71
+ if (r.parentSlug) {
72
+ const list = childrenByParent.get(r.parentSlug) ?? []
73
+ list.push(r)
74
+ childrenByParent.set(r.parentSlug, list)
73
75
  }
74
76
  }
75
77
 
76
- // Filter and score apps
77
- const scoredApps = apps
78
+ // Filter and score root resources
79
+ const scoredApps = rootResources
78
80
  .map((app): SearchResult | null => {
79
81
  const name = app.displayName.toLowerCase()
80
82
  const abbreviation = app.abbreviation?.toLowerCase() || ''
@@ -143,15 +145,15 @@ export function searchApps(
143
145
  return { app, match: { field: 'description', type: 'contains' } }
144
146
  }
145
147
 
146
- // Check sub-resources (name, aliases, description) — supports multi-word queries
147
- const appSubResources = subResourcesByApp.get(app.slug)
148
- if (appSubResources) {
149
- const subMatch = appSubResources.some(
150
- (sr) =>
151
- allTermsMatch(sr.displayName.toLowerCase()) ||
152
- sr.aliases.some((a) => allTermsMatch(a.toLowerCase())) ||
153
- (sr.description
154
- ? allTermsMatch(sr.description.toLowerCase())
148
+ // Check child resources (name, aliases, description) — supports multi-word queries
149
+ const children = childrenByParent.get(app.slug)
150
+ if (children) {
151
+ const subMatch = children.some(
152
+ (r) =>
153
+ allTermsMatch(r.displayName.toLowerCase()) ||
154
+ (r.aliases ?? []).some((a) => allTermsMatch(a.toLowerCase())) ||
155
+ (r.description
156
+ ? allTermsMatch(r.description.toLowerCase())
155
157
  : false),
156
158
  )
157
159
  if (subMatch) {
@@ -259,3 +261,6 @@ export function highlightText(
259
261
 
260
262
  return segments
261
263
  }
264
+
265
+ /** @deprecated Use searchResources instead */
266
+ export const searchApps = searchResources