@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.
- 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 +1 -13
- 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/MarkdownText.d.ts +18 -0
- package/dist/esm/modules/appCatalog/ui/components/MarkdownText.js +19 -0
- package/dist/esm/modules/appCatalog/ui/components/MarkdownText.js.map +1 -0
- 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 +3 -20
- package/src/modules/appCatalog/ui/components/AppDetailModal.tsx +14 -11
- package/src/modules/appCatalog/ui/components/MarkdownText.tsx +42 -0
- 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,13 +1,8 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest'
|
|
2
|
-
import type {
|
|
3
|
-
|
|
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
|
|
19
|
-
overrides: Partial<
|
|
20
|
-
):
|
|
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('
|
|
30
|
-
const apps:
|
|
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(
|
|
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 =
|
|
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('
|
|
55
|
-
const
|
|
56
|
-
|
|
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
|
-
|
|
55
|
+
parentSlug: 'aws-console',
|
|
60
56
|
aliases: ['043902793406'],
|
|
61
57
|
}),
|
|
62
|
-
|
|
58
|
+
makeChildResource({
|
|
63
59
|
slug: 'aws-natera-infosec-dev',
|
|
64
60
|
displayName: 'natera-infosec-dev',
|
|
65
|
-
|
|
61
|
+
parentSlug: 'aws-console',
|
|
66
62
|
aliases: [],
|
|
67
63
|
}),
|
|
68
64
|
]
|
|
69
65
|
|
|
70
|
-
|
|
71
|
-
|
|
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
|
|
77
|
-
const results =
|
|
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
|
|
83
|
-
const results =
|
|
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
|
|
88
|
-
const results =
|
|
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 {
|
|
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
|
-
|
|
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():
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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?.
|
|
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 {
|
|
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'
|
|
@@ -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:
|
|
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 {
|
|
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 {
|
|
23
|
+
import { getChildResources } from '~/modules/appCatalog/utils/resolveHelpers'
|
|
23
24
|
|
|
24
25
|
export interface AppDetailModalProps {
|
|
25
|
-
app:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
190
|
-
const {
|
|
190
|
+
function TiersAndSubResources({ app }: { app: Resource }) {
|
|
191
|
+
const { resources } = useAppCatalogContext()
|
|
191
192
|
const appSubResources = useMemo(
|
|
192
|
-
() =>
|
|
193
|
-
[
|
|
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
|
-
<
|
|
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 {
|
|
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
|
/**
|