@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.
- package/dist/esm/__tests__/integration/harness/MockBackendVerifier.d.ts +3 -3
- package/dist/esm/__tests__/integration/mock-backend/MockBackendConfigurer.d.ts +4 -4
- package/dist/esm/__tests__/integration/mock-backend/MockDb.d.ts +11 -7
- package/dist/esm/api/infra/trpc.d.ts +3 -3
- package/dist/esm/modules/appCatalog/context/AppCatalogContext.d.ts +2 -3
- package/dist/esm/modules/appCatalog/context/AppCatalogContext.js +2 -4
- package/dist/esm/modules/appCatalog/context/AppCatalogContext.js.map +1 -1
- package/dist/esm/modules/appCatalog/hooks/useAppCounts.d.ts +2 -2
- package/dist/esm/modules/appCatalog/hooks/useAppCounts.js +3 -3
- package/dist/esm/modules/appCatalog/hooks/useAppCounts.js.map +1 -1
- package/dist/esm/modules/appCatalog/hooks/useUpdateApp.d.ts +9 -0
- package/dist/esm/modules/appCatalog/ui/components/AccessRequestSection.d.ts +2 -2
- package/dist/esm/modules/appCatalog/ui/components/AccessRequestSection.js.map +1 -1
- package/dist/esm/modules/appCatalog/ui/components/AppDetailModal.d.ts +2 -2
- package/dist/esm/modules/appCatalog/ui/components/ScreenshotGallery.d.ts +2 -2
- package/dist/esm/modules/appCatalog/ui/components/ScreenshotGallery.js.map +1 -1
- package/dist/esm/modules/appCatalog/ui/components/SubResourcesSection.d.ts +2 -2
- package/dist/esm/modules/appCatalog/ui/components/SubResourcesSection.js +12 -14
- package/dist/esm/modules/appCatalog/ui/components/SubResourcesSection.js.map +1 -1
- package/dist/esm/modules/appCatalog/ui/components/TierVariantsSection.d.ts +2 -2
- package/dist/esm/modules/appCatalog/ui/components/TierVariantsSection.js.map +1 -1
- package/dist/esm/modules/appCatalog/ui/filters/FilterBar.d.ts +2 -2
- package/dist/esm/modules/appCatalog/ui/filters/FilterBar.js.map +1 -1
- package/dist/esm/modules/appCatalog/ui/grid/AppCatalogGrid.d.ts +3 -3
- package/dist/esm/modules/appCatalog/ui/grid/AppCatalogGrid.js +19 -19
- package/dist/esm/modules/appCatalog/ui/grid/AppCatalogGrid.js.map +1 -1
- package/dist/esm/modules/appCatalog/ui/grid/AppCatalogTable.d.ts +2 -2
- package/dist/esm/modules/appCatalog/ui/grid/appCatalogUtils.d.ts +2 -2
- package/dist/esm/modules/appCatalog/ui/hooks/useKeyboardNavigation.d.ts +3 -3
- package/dist/esm/modules/appCatalog/ui/hooks/useKeyboardNavigation.js.map +1 -1
- package/dist/esm/modules/appCatalog/ui/pages/AppCatalogPage.js +20 -12
- package/dist/esm/modules/appCatalog/ui/pages/AppCatalogPage.js.map +1 -1
- package/dist/esm/modules/appCatalog/utils/resolveHelpers.d.ts +5 -2
- package/dist/esm/modules/appCatalog/utils/resolveHelpers.js +4 -4
- package/dist/esm/modules/appCatalog/utils/resolveHelpers.js.map +1 -1
- package/dist/esm/modules/appCatalog/utils/searchApps.d.ts +11 -6
- package/dist/esm/modules/appCatalog/utils/searchApps.js +15 -14
- package/dist/esm/modules/appCatalog/utils/searchApps.js.map +1 -1
- package/package.json +3 -3
- package/src/__tests__/integration/appCatalog.integration.test.ts +3 -3
- package/src/__tests__/integration/harness/MockBackendVerifier.ts +5 -5
- package/src/__tests__/integration/mock-backend/MockBackendConfigurer.ts +16 -12
- package/src/__tests__/integration/mock-backend/MockDb.ts +30 -22
- package/src/__tests__/modules/appCatalog/ScreenshotPreview.test.tsx +4 -5
- package/src/__tests__/modules/appCatalog/utils/searchApps.test.ts +28 -30
- package/src/__tests__/modules/gallery/EscapeDismissal.integration.test.tsx +3 -4
- package/src/modules/appCatalog/context/AppCatalogContext.tsx +4 -8
- package/src/modules/appCatalog/hooks/useAppCounts.ts +5 -5
- package/src/modules/appCatalog/ui/components/AccessRequestSection.tsx +2 -2
- package/src/modules/appCatalog/ui/components/AppDetailModal.tsx +10 -10
- package/src/modules/appCatalog/ui/components/ScreenshotGallery.tsx +2 -2
- package/src/modules/appCatalog/ui/components/SearchAndFilterHeader.tsx +6 -2
- package/src/modules/appCatalog/ui/components/SubResourcesSection.tsx +17 -17
- package/src/modules/appCatalog/ui/components/TierVariantsSection.tsx +2 -2
- package/src/modules/appCatalog/ui/filters/FilterBar.tsx +2 -2
- package/src/modules/appCatalog/ui/grid/AppCatalogGrid.tsx +34 -37
- package/src/modules/appCatalog/ui/grid/AppCatalogTable.tsx +3 -3
- package/src/modules/appCatalog/ui/grid/appCatalogUtils.ts +2 -2
- package/src/modules/appCatalog/ui/hooks/useKeyboardNavigation.ts +3 -3
- package/src/modules/appCatalog/ui/pages/AppCatalogPage.tsx +24 -14
- package/src/modules/appCatalog/utils/resolveHelpers.ts +13 -10
- package/src/modules/appCatalog/utils/searchApps.ts +36 -31
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { useMemo } from 'react'
|
|
2
|
-
import type {
|
|
3
|
-
import {
|
|
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:
|
|
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
|
|
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
|
|
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
|
-
|
|
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:
|
|
29
|
+
app: Resource
|
|
30
30
|
approvalMethods: AppApprovalMethod[]
|
|
31
31
|
}
|
|
32
32
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
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 {
|
|
22
|
+
import { getChildResources } from '~/modules/appCatalog/utils/resolveHelpers'
|
|
23
23
|
|
|
24
24
|
export interface AppDetailModalProps {
|
|
25
|
-
app:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
190
|
-
const {
|
|
189
|
+
function TiersAndSubResources({ app }: { app: Resource }) {
|
|
190
|
+
const { resources } = useAppCatalogContext()
|
|
191
191
|
const appSubResources = useMemo(
|
|
192
|
-
() =>
|
|
193
|
-
[
|
|
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 {
|
|
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:
|
|
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 {
|
|
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 {
|
|
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:
|
|
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.
|
|
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.
|
|
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 =
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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.
|
|
180
|
+
{sr.tier && (
|
|
181
181
|
<Badge
|
|
182
|
-
variant={getTierBadgeVariant(sr.
|
|
183
|
-
className={`text-xs ${getTierBadgeClassName(sr.
|
|
182
|
+
variant={getTierBadgeVariant(sr.tier)}
|
|
183
|
+
className={`text-xs ${getTierBadgeClassName(sr.tier)}`}
|
|
184
184
|
>
|
|
185
|
-
{getTierDisplayLabel(sr.
|
|
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
|
-
|
|
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:
|
|
22
|
+
tiers: TierVariant[]
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
function getTierBadgeVariant(
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
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:
|
|
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 {
|
|
42
|
+
import { getChildResources } from '../../utils/resolveHelpers'
|
|
43
43
|
|
|
44
44
|
export interface AppCatalogGridProps {
|
|
45
|
-
apps:
|
|
45
|
+
apps: Resource[]
|
|
46
46
|
selectedAppSlug?: string
|
|
47
47
|
groupingDefinition?: GroupingTagDefinition
|
|
48
|
-
onAppClick?: (app:
|
|
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:
|
|
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:
|
|
176
|
-
const {
|
|
169
|
+
function TiersAndSubResourcesPanel({ app }: { app: Resource }) {
|
|
170
|
+
const { resources } = useAppCatalogContext()
|
|
177
171
|
const appSubResources = React.useMemo(
|
|
178
|
-
() =>
|
|
179
|
-
[
|
|
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:
|
|
204
|
-
onAppClick?: (app:
|
|
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,
|
|
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
|
-
?
|
|
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:
|
|
618
|
+
apps: Resource[]
|
|
625
619
|
}
|
|
626
620
|
|
|
627
621
|
function groupApps(
|
|
628
|
-
apps:
|
|
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,
|
|
646
|
-
const ungrouped:
|
|
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
|
|
724
|
-
const {
|
|
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() ||
|
|
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
|
|
737
|
-
if (
|
|
738
|
-
|
|
739
|
-
const
|
|
740
|
-
const
|
|
741
|
-
|
|
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(
|
|
741
|
+
map.set(r.parentSlug, r.displayName)
|
|
745
742
|
}
|
|
746
743
|
}
|
|
747
744
|
return map
|
|
748
|
-
}, [searchQuery,
|
|
745
|
+
}, [searchQuery, allResources2])
|
|
749
746
|
|
|
750
747
|
// Define columns
|
|
751
|
-
const columns = React.useMemo<ColumnDef<
|
|
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:
|
|
856
|
+
const handleAppClick = (app: Resource) => {
|
|
860
857
|
onAppClick?.(app)
|
|
861
858
|
}
|
|
862
859
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
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:
|
|
20
|
+
apps: Resource[]
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
export function AppCatalogTable({ apps }: AppCatalogTableProps) {
|
|
24
|
-
const columns = useMemo<ColumnDef<
|
|
24
|
+
const columns = useMemo<ColumnDef<Resource>[]>(
|
|
25
25
|
() => [
|
|
26
26
|
{
|
|
27
27
|
slug: 'name',
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { Resource } from '@igstack/app-catalog-backend-core'
|
|
2
2
|
|
|
3
|
-
export function getAppUrl(app:
|
|
3
|
+
export function getAppUrl(app: Resource): string {
|
|
4
4
|
return app.appUrl || '#'
|
|
5
5
|
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import type {
|
|
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:
|
|
5
|
+
apps: Resource[]
|
|
6
6
|
selectedAppSlug?: string
|
|
7
|
-
onAppClick?: (app:
|
|
7
|
+
onAppClick?: (app: Resource) => void
|
|
8
8
|
}
|
|
9
9
|
|
|
10
10
|
export function useKeyboardNavigation({
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
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 {
|
|
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 {
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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 =
|
|
128
|
+
let count = rootResources.length
|
|
119
129
|
if (!filterState.showDeprecated) {
|
|
120
|
-
count =
|
|
130
|
+
count = rootResources.filter((app) => !app.deprecated).length
|
|
121
131
|
}
|
|
122
132
|
return count
|
|
123
|
-
}, [
|
|
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={
|
|
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
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
):
|
|
25
|
-
return
|
|
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
|