@igstack/app-catalog-frontend-core 0.5.0 → 0.6.1
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/appDeepLink.integration.test.d.ts +0 -0
- package/dist/esm/__tests__/integration/harness/given.d.ts +5 -1
- package/dist/esm/__tests__/modules/appCatalog/utils/mergeFrontendBuildId.test.d.ts +1 -0
- package/dist/esm/__tests__/ui/components/header/VersionDisplay.test.d.ts +0 -0
- package/dist/esm/modules/appCatalog/context/AppCatalogContext.js +6 -8
- package/dist/esm/modules/appCatalog/context/AppCatalogContext.js.map +1 -1
- package/dist/esm/modules/appCatalog/context/mergeFrontendBuildId.d.ts +15 -0
- package/dist/esm/modules/appCatalog/context/mergeFrontendBuildId.js +15 -0
- package/dist/esm/modules/appCatalog/context/mergeFrontendBuildId.js.map +1 -0
- package/dist/esm/modules/appCatalog/ui/components/PersonBadge.js +1 -0
- package/dist/esm/modules/appCatalog/ui/components/PersonBadge.js.map +1 -1
- package/dist/esm/modules/appCatalog/ui/components/SubResourcesSection.js +1 -0
- package/dist/esm/modules/appCatalog/ui/components/SubResourcesSection.js.map +1 -1
- package/dist/esm/modules/appCatalog/ui/components/TierVariantsSection.js +1 -0
- package/dist/esm/modules/appCatalog/ui/components/TierVariantsSection.js.map +1 -1
- package/dist/esm/modules/appCatalog/ui/grid/AppCatalogGrid.d.ts +3 -1
- package/dist/esm/modules/appCatalog/ui/grid/AppCatalogGrid.js +3 -1
- package/dist/esm/modules/appCatalog/ui/grid/AppCatalogGrid.js.map +1 -1
- package/dist/esm/modules/appCatalog/ui/pages/AppCatalogPage.d.ts +3 -1
- package/dist/esm/modules/appCatalog/ui/pages/AppCatalogPage.js +17 -12
- package/dist/esm/modules/appCatalog/ui/pages/AppCatalogPage.js.map +1 -1
- package/dist/esm/routeTree.gen.d.ts +18 -4
- package/dist/esm/routeTree.gen.js +11 -4
- package/dist/esm/routeTree.gen.js.map +1 -1
- package/dist/esm/routes/_layout/app.$slug.d.ts +3 -0
- package/dist/esm/routes/_layout/app._slug.js +21 -0
- package/dist/esm/routes/_layout/app._slug.js.map +1 -0
- package/dist/esm/routes/_layout/index.d.ts +0 -1
- package/dist/esm/routes/_layout/index.js +0 -1
- package/dist/esm/routes/_layout/index.js.map +1 -1
- package/dist/esm/ui/components/header/Header.d.ts +4 -0
- package/dist/esm/ui/components/header/Header.js +40 -17
- package/dist/esm/ui/components/header/Header.js.map +1 -1
- package/package.json +4 -3
- package/src/__tests__/integration/appDeepLink.integration.test.ts +55 -0
- package/src/__tests__/integration/harness/given.tsx +7 -3
- package/src/__tests__/modules/appCatalog/utils/mergeFrontendBuildId.test.ts +42 -0
- package/src/__tests__/ui/components/header/VersionDisplay.test.tsx +38 -0
- package/src/modules/appCatalog/context/AppCatalogContext.tsx +5 -11
- package/src/modules/appCatalog/context/mergeFrontendBuildId.ts +31 -0
- package/src/modules/appCatalog/ui/grid/AppCatalogGrid.tsx +4 -0
- package/src/modules/appCatalog/ui/pages/AppCatalogPage.tsx +18 -15
- package/src/routeTree.gen.ts +21 -2
- package/src/routes/_layout/app.$slug.tsx +23 -0
- package/src/routes/_layout/index.tsx +0 -1
- package/src/ui/components/header/Header.tsx +39 -16
|
@@ -32,6 +32,7 @@ import { GalleryTools } from '../tools/GalleryTools'
|
|
|
32
32
|
export interface GivenResult {
|
|
33
33
|
ui: UiTools
|
|
34
34
|
backend: MockBackendVerifier
|
|
35
|
+
router: ReturnType<typeof createAcRouter>
|
|
35
36
|
}
|
|
36
37
|
|
|
37
38
|
export interface UiTools {
|
|
@@ -57,7 +58,10 @@ export async function cleanupTestResources(): Promise<void> {
|
|
|
57
58
|
}
|
|
58
59
|
}
|
|
59
60
|
|
|
60
|
-
export async function given(
|
|
61
|
+
export async function given(
|
|
62
|
+
magazine: Magazine,
|
|
63
|
+
opts: { initialRoute?: string } = {},
|
|
64
|
+
): Promise<GivenResult> {
|
|
61
65
|
// Clean up any previous resources
|
|
62
66
|
await cleanupTestResources()
|
|
63
67
|
resetConfigurerCounter()
|
|
@@ -126,7 +130,7 @@ export async function given(magazine: Magazine): Promise<GivenResult> {
|
|
|
126
130
|
|
|
127
131
|
const router = createAcRouter({
|
|
128
132
|
history: createMemoryHistory({
|
|
129
|
-
initialEntries: ['/catalog/apps'],
|
|
133
|
+
initialEntries: [opts.initialRoute ?? '/catalog/apps'],
|
|
130
134
|
}),
|
|
131
135
|
context: {
|
|
132
136
|
queryClient,
|
|
@@ -187,5 +191,5 @@ export async function given(magazine: Magazine): Promise<GivenResult> {
|
|
|
187
191
|
globalError: () => getGlobalError(),
|
|
188
192
|
}
|
|
189
193
|
|
|
190
|
-
return { ui, backend }
|
|
194
|
+
return { ui, backend, router }
|
|
191
195
|
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { mergeFrontendBuildId } from '~/modules/appCatalog/context/mergeFrontendBuildId'
|
|
3
|
+
|
|
4
|
+
describe('mergeFrontendBuildId', () => {
|
|
5
|
+
it('returns versions unchanged when no build id', () => {
|
|
6
|
+
const versions = { frontend: { displayName: '0.4.0', sha: 'abc1234' } }
|
|
7
|
+
expect(mergeFrontendBuildId(versions, undefined)).toEqual(versions)
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
it('shows local (dropping SHA) when build id is local', () => {
|
|
11
|
+
const versions = { frontend: { displayName: '0.4.0', sha: 'abc1234' } }
|
|
12
|
+
const out = mergeFrontendBuildId(versions, 'local')
|
|
13
|
+
expect(out.frontend).toEqual({ displayName: 'local' })
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
it('prepends pipeline id while preserving fe-core version + sha + url', () => {
|
|
17
|
+
const versions = {
|
|
18
|
+
frontend: {
|
|
19
|
+
displayName: '0.4.0-alpha-x',
|
|
20
|
+
sha: 'abc1234',
|
|
21
|
+
shaUrl: 'https://github.com/lislon/app-catalog/commit/abc1234',
|
|
22
|
+
},
|
|
23
|
+
}
|
|
24
|
+
const out = mergeFrontendBuildId(versions, '12345')
|
|
25
|
+
expect(out.frontend?.displayName).toBe('#12345 · 0.4.0-alpha-x')
|
|
26
|
+
expect(out.frontend?.sha).toBe('abc1234')
|
|
27
|
+
expect(out.frontend?.shaUrl).toBe(
|
|
28
|
+
'https://github.com/lislon/app-catalog/commit/abc1234',
|
|
29
|
+
)
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it('shows only pipeline id when backend gave no frontend slot', () => {
|
|
33
|
+
const out = mergeFrontendBuildId({}, '12345')
|
|
34
|
+
expect(out.frontend).toEqual({ displayName: '#12345' })
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it('does not mutate the input', () => {
|
|
38
|
+
const versions = { frontend: { displayName: '0.4.0' } }
|
|
39
|
+
mergeFrontendBuildId(versions, '99')
|
|
40
|
+
expect(versions.frontend.displayName).toBe('0.4.0')
|
|
41
|
+
})
|
|
42
|
+
})
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { render, screen } from '@testing-library/react'
|
|
2
|
+
import { describe, expect, it } from 'vitest'
|
|
3
|
+
import '@testing-library/jest-dom/vitest'
|
|
4
|
+
|
|
5
|
+
import { VersionDisplay } from '~/ui/components/header/Header'
|
|
6
|
+
|
|
7
|
+
describe('VersionDisplay footer', () => {
|
|
8
|
+
it('renders the frontend-core version and its short SHA as a commit link', () => {
|
|
9
|
+
render(
|
|
10
|
+
<VersionDisplay
|
|
11
|
+
versions={{
|
|
12
|
+
backend: { displayName: '#12345 (deadbee)' },
|
|
13
|
+
coreVersion: { displayName: 'be 0.4.0 / fe 0.3.1' },
|
|
14
|
+
frontend: {
|
|
15
|
+
displayName: '#12345 · 0.3.1-alpha-x',
|
|
16
|
+
sha: 'abc1234',
|
|
17
|
+
shaUrl: 'https://github.com/lislon/app-catalog/commit/abc1234',
|
|
18
|
+
},
|
|
19
|
+
}}
|
|
20
|
+
/>,
|
|
21
|
+
)
|
|
22
|
+
// FE line shows pipeline id + fe-core version
|
|
23
|
+
expect(screen.getByText(/0\.3\.1-alpha-x/)).toBeInTheDocument()
|
|
24
|
+
// SHA rendered and linked to the commit
|
|
25
|
+
const shaLink = screen.getByText('(abc1234)').closest('a')
|
|
26
|
+
expect(shaLink).toHaveAttribute(
|
|
27
|
+
'href',
|
|
28
|
+
'https://github.com/lislon/app-catalog/commit/abc1234',
|
|
29
|
+
)
|
|
30
|
+
// Backend line carries the natera SHA suffix
|
|
31
|
+
expect(screen.getByText(/#12345 \(deadbee\)/)).toBeInTheDocument()
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it('shows plain "local" for local dev', () => {
|
|
35
|
+
render(<VersionDisplay versions={{ backend: { displayName: 'local' } }} />)
|
|
36
|
+
expect(screen.getByText('local')).toBeInTheDocument()
|
|
37
|
+
})
|
|
38
|
+
})
|
|
@@ -11,6 +11,7 @@ import type { ReactNode } from 'react'
|
|
|
11
11
|
import { createContext, use, useEffect, useMemo } from 'react'
|
|
12
12
|
import { ApiQueryMagazineAppCatalog } from '~/modules/appCatalog'
|
|
13
13
|
import { useUiSettings } from '~/context/UiSettingsContext'
|
|
14
|
+
import { mergeFrontendBuildId } from '~/modules/appCatalog/context/mergeFrontendBuildId'
|
|
14
15
|
|
|
15
16
|
export interface AppCatalogContextIface {
|
|
16
17
|
resources: Resource[]
|
|
@@ -44,17 +45,10 @@ export function AppCatalogProvider({ children }: AppCatalogProviderProps) {
|
|
|
44
45
|
approvalMethods: data?.approvalMethods ?? [],
|
|
45
46
|
persons: data?.persons ?? [],
|
|
46
47
|
groups: data?.groups ?? [],
|
|
47
|
-
versions:
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
displayName:
|
|
52
|
-
uiSettings.frontendBuildId === 'local'
|
|
53
|
-
? 'local'
|
|
54
|
-
: `#${uiSettings.frontendBuildId}`,
|
|
55
|
-
},
|
|
56
|
-
}),
|
|
57
|
-
},
|
|
48
|
+
versions: mergeFrontendBuildId(
|
|
49
|
+
data?.versions ?? {},
|
|
50
|
+
uiSettings.frontendBuildId,
|
|
51
|
+
),
|
|
58
52
|
}),
|
|
59
53
|
[
|
|
60
54
|
data?.approvalMethods,
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { AppVersionInfo } from '@igstack/app-catalog-backend-core'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Merge the (natera-agnostic) frontend build id into the backend-provided
|
|
5
|
+
* version info's `frontend` slot.
|
|
6
|
+
*
|
|
7
|
+
* The build id is baked into the deployed JS bundle at build time (e.g. a CI
|
|
8
|
+
* pipeline id via a Vite env var). We PREPEND it to the resolved frontend-core
|
|
9
|
+
* npm version instead of replacing it, so the footer surfaces both:
|
|
10
|
+
* `#<buildId> · <frontend-core version> (<sha>)`.
|
|
11
|
+
*
|
|
12
|
+
* - No build id → return versions unchanged.
|
|
13
|
+
* - build id === 'local' → show `local` (local dev, no meaningful SHA/version).
|
|
14
|
+
* - otherwise → prepend `#<buildId> · ` and preserve the version + sha + shaUrl.
|
|
15
|
+
*/
|
|
16
|
+
export function mergeFrontendBuildId(
|
|
17
|
+
versions: AppVersionInfo,
|
|
18
|
+
frontendBuildId: string | undefined,
|
|
19
|
+
): AppVersionInfo {
|
|
20
|
+
if (!frontendBuildId) return versions
|
|
21
|
+
if (frontendBuildId === 'local') {
|
|
22
|
+
return { ...versions, frontend: { displayName: 'local' } }
|
|
23
|
+
}
|
|
24
|
+
const be = versions.frontend
|
|
25
|
+
return {
|
|
26
|
+
...versions,
|
|
27
|
+
frontend: be
|
|
28
|
+
? { ...be, displayName: `#${frontendBuildId} · ${be.displayName}` }
|
|
29
|
+
: { displayName: `#${frontendBuildId}` },
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -55,6 +55,8 @@ export interface AppCatalogGridProps {
|
|
|
55
55
|
totalAppsCount?: number
|
|
56
56
|
/** Callback to clear all filters and search */
|
|
57
57
|
onClearFilters?: () => void
|
|
58
|
+
/** Called when the user closes the detail panel (Esc or X) — e.g. to navigate back to the catalog URL */
|
|
59
|
+
onClosePanel?: () => void
|
|
58
60
|
}
|
|
59
61
|
|
|
60
62
|
function getIconUrl(iconName: string): string {
|
|
@@ -697,6 +699,7 @@ export function AppCatalogGrid({
|
|
|
697
699
|
searchQuery,
|
|
698
700
|
totalAppsCount,
|
|
699
701
|
onClearFilters,
|
|
702
|
+
onClosePanel,
|
|
700
703
|
}: AppCatalogGridProps) {
|
|
701
704
|
const selectedApp = selectedAppSlug
|
|
702
705
|
? apps.find((a) => a.slug === selectedAppSlug)
|
|
@@ -862,6 +865,7 @@ export function AppCatalogGrid({
|
|
|
862
865
|
|
|
863
866
|
const handleClosePanel = () => {
|
|
864
867
|
setHasUserClosed(true)
|
|
868
|
+
onClosePanel?.()
|
|
865
869
|
}
|
|
866
870
|
|
|
867
871
|
return (
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Resource } from '@igstack/app-catalog-backend-core'
|
|
2
|
+
import { useNavigate } from '@tanstack/react-router'
|
|
2
3
|
import { X } from 'lucide-react'
|
|
3
4
|
import { useDeferredValue, useEffect, useMemo, useState } from 'react'
|
|
4
5
|
import { Button } from '~/ui/button'
|
|
@@ -13,25 +14,22 @@ import {
|
|
|
13
14
|
import { useAppCatalogContext } from '../../context/AppCatalogContext'
|
|
14
15
|
import { useAppClickHistory } from '../../hooks/useAppClickHistory'
|
|
15
16
|
import { useAppCounts } from '../../hooks/useAppCounts'
|
|
16
|
-
import { useUrlSyncedState } from '../../hooks/useUrlSyncedState'
|
|
17
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
|
-
export function AppCatalogPage(
|
|
23
|
+
export function AppCatalogPage({
|
|
24
|
+
selectedSlug,
|
|
25
|
+
}: { selectedSlug?: string } = {}) {
|
|
24
26
|
const { resources, isLoadingApps, tagsDefinitions } = useAppCatalogContext()
|
|
25
27
|
const { state: filterState, actions } = useAppCatalogFilters()
|
|
26
28
|
const { getTopApps } = useAppClickHistory()
|
|
29
|
+
const navigate = useNavigate()
|
|
27
30
|
|
|
28
|
-
//
|
|
29
|
-
const
|
|
30
|
-
string | undefined
|
|
31
|
-
>({
|
|
32
|
-
key: 'app',
|
|
33
|
-
defaultValue: undefined,
|
|
34
|
-
})
|
|
31
|
+
// Selected app comes from the route path (/app/<slug>); undefined = nothing open
|
|
32
|
+
const selectedAppSlug = selectedSlug
|
|
35
33
|
|
|
36
34
|
// Search value from context (URL-synced in AppCatalogFiltersContext)
|
|
37
35
|
const searchValue = filterState.searchValue
|
|
@@ -108,19 +106,23 @@ export function AppCatalogPage() {
|
|
|
108
106
|
|
|
109
107
|
// Auto-open details when only 1 result
|
|
110
108
|
useEffect(() => {
|
|
111
|
-
if (filteredApps.length === 1 && filteredApps[0]) {
|
|
112
|
-
|
|
109
|
+
if (filteredApps.length === 1 && filteredApps[0] && !selectedAppSlug) {
|
|
110
|
+
void navigate({
|
|
111
|
+
to: '/app/$slug',
|
|
112
|
+
params: { slug: filteredApps[0].slug },
|
|
113
|
+
replace: true,
|
|
114
|
+
})
|
|
113
115
|
}
|
|
114
|
-
}, [filteredApps,
|
|
116
|
+
}, [filteredApps, selectedAppSlug, navigate])
|
|
115
117
|
|
|
116
118
|
const handleAppClick = (app: Resource) => {
|
|
117
|
-
|
|
119
|
+
void navigate({ to: '/app/$slug', params: { slug: app.slug } })
|
|
118
120
|
}
|
|
119
121
|
|
|
120
122
|
const handleClearFilters = () => {
|
|
121
123
|
setSearchValue('')
|
|
122
124
|
actions.clearAllFilters()
|
|
123
|
-
|
|
125
|
+
void navigate({ to: '/' })
|
|
124
126
|
}
|
|
125
127
|
|
|
126
128
|
// Calculate total apps count (respecting showDeprecated setting)
|
|
@@ -174,7 +176,7 @@ export function AppCatalogPage() {
|
|
|
174
176
|
variant="outline"
|
|
175
177
|
onClick={() => {
|
|
176
178
|
setSearchValue('')
|
|
177
|
-
|
|
179
|
+
void navigate({ to: '/' })
|
|
178
180
|
}}
|
|
179
181
|
className="gap-2"
|
|
180
182
|
>
|
|
@@ -190,6 +192,7 @@ export function AppCatalogPage() {
|
|
|
190
192
|
selectedAppSlug={selectedAppSlug}
|
|
191
193
|
groupingDefinition={groupingDefinition}
|
|
192
194
|
onAppClick={handleAppClick}
|
|
195
|
+
onClosePanel={() => void navigate({ to: '/' })}
|
|
193
196
|
hasSearch={!!deferredSearchValue}
|
|
194
197
|
searchQuery={deferredSearchValue}
|
|
195
198
|
totalAppsCount={totalAppsCount}
|
package/src/routeTree.gen.ts
CHANGED
|
@@ -13,6 +13,7 @@ import { Route as LayoutRouteImport } from './routes/_layout'
|
|
|
13
13
|
import { Route as LayoutIndexRouteImport } from './routes/_layout/index'
|
|
14
14
|
import { Route as AuthCallbackRouteImport } from './routes/auth.callback'
|
|
15
15
|
import { Route as LayoutLoginRouteImport } from './routes/_layout/login'
|
|
16
|
+
import { Route as LayoutAppSlugRouteImport } from './routes/_layout/app.$slug'
|
|
16
17
|
import { Route as LayoutCatalogAppsIndexRouteImport } from './routes/_layout/catalog.apps.index'
|
|
17
18
|
|
|
18
19
|
const LayoutRoute = LayoutRouteImport.update({
|
|
@@ -34,6 +35,11 @@ const LayoutLoginRoute = LayoutLoginRouteImport.update({
|
|
|
34
35
|
path: '/login',
|
|
35
36
|
getParentRoute: () => LayoutRoute,
|
|
36
37
|
} as any)
|
|
38
|
+
const LayoutAppSlugRoute = LayoutAppSlugRouteImport.update({
|
|
39
|
+
id: '/app/$slug',
|
|
40
|
+
path: '/app/$slug',
|
|
41
|
+
getParentRoute: () => LayoutRoute,
|
|
42
|
+
} as any)
|
|
37
43
|
const LayoutCatalogAppsIndexRoute = LayoutCatalogAppsIndexRouteImport.update({
|
|
38
44
|
id: '/catalog/apps/',
|
|
39
45
|
path: '/catalog/apps/',
|
|
@@ -44,12 +50,14 @@ export interface FileRoutesByFullPath {
|
|
|
44
50
|
'/': typeof LayoutIndexRoute
|
|
45
51
|
'/login': typeof LayoutLoginRoute
|
|
46
52
|
'/auth/callback': typeof AuthCallbackRoute
|
|
53
|
+
'/app/$slug': typeof LayoutAppSlugRoute
|
|
47
54
|
'/catalog/apps/': typeof LayoutCatalogAppsIndexRoute
|
|
48
55
|
}
|
|
49
56
|
export interface FileRoutesByTo {
|
|
50
57
|
'/login': typeof LayoutLoginRoute
|
|
51
58
|
'/auth/callback': typeof AuthCallbackRoute
|
|
52
59
|
'/': typeof LayoutIndexRoute
|
|
60
|
+
'/app/$slug': typeof LayoutAppSlugRoute
|
|
53
61
|
'/catalog/apps': typeof LayoutCatalogAppsIndexRoute
|
|
54
62
|
}
|
|
55
63
|
export interface FileRoutesById {
|
|
@@ -58,19 +66,21 @@ export interface FileRoutesById {
|
|
|
58
66
|
'/_layout/login': typeof LayoutLoginRoute
|
|
59
67
|
'/auth/callback': typeof AuthCallbackRoute
|
|
60
68
|
'/_layout/': typeof LayoutIndexRoute
|
|
69
|
+
'/_layout/app/$slug': typeof LayoutAppSlugRoute
|
|
61
70
|
'/_layout/catalog/apps/': typeof LayoutCatalogAppsIndexRoute
|
|
62
71
|
}
|
|
63
72
|
export interface FileRouteTypes {
|
|
64
73
|
fileRoutesByFullPath: FileRoutesByFullPath
|
|
65
|
-
fullPaths: '/' | '/login' | '/auth/callback' | '/catalog/apps/'
|
|
74
|
+
fullPaths: '/' | '/login' | '/auth/callback' | '/app/$slug' | '/catalog/apps/'
|
|
66
75
|
fileRoutesByTo: FileRoutesByTo
|
|
67
|
-
to: '/login' | '/auth/callback' | '/' | '/catalog/apps'
|
|
76
|
+
to: '/login' | '/auth/callback' | '/' | '/app/$slug' | '/catalog/apps'
|
|
68
77
|
id:
|
|
69
78
|
| '__root__'
|
|
70
79
|
| '/_layout'
|
|
71
80
|
| '/_layout/login'
|
|
72
81
|
| '/auth/callback'
|
|
73
82
|
| '/_layout/'
|
|
83
|
+
| '/_layout/app/$slug'
|
|
74
84
|
| '/_layout/catalog/apps/'
|
|
75
85
|
fileRoutesById: FileRoutesById
|
|
76
86
|
}
|
|
@@ -109,6 +119,13 @@ declare module '@tanstack/react-router' {
|
|
|
109
119
|
preLoaderRoute: typeof LayoutLoginRouteImport
|
|
110
120
|
parentRoute: typeof LayoutRoute
|
|
111
121
|
}
|
|
122
|
+
'/_layout/app/$slug': {
|
|
123
|
+
id: '/_layout/app/$slug'
|
|
124
|
+
path: '/app/$slug'
|
|
125
|
+
fullPath: '/app/$slug'
|
|
126
|
+
preLoaderRoute: typeof LayoutAppSlugRouteImport
|
|
127
|
+
parentRoute: typeof LayoutRoute
|
|
128
|
+
}
|
|
112
129
|
'/_layout/catalog/apps/': {
|
|
113
130
|
id: '/_layout/catalog/apps/'
|
|
114
131
|
path: '/catalog/apps'
|
|
@@ -122,12 +139,14 @@ declare module '@tanstack/react-router' {
|
|
|
122
139
|
interface LayoutRouteChildren {
|
|
123
140
|
LayoutLoginRoute: typeof LayoutLoginRoute
|
|
124
141
|
LayoutIndexRoute: typeof LayoutIndexRoute
|
|
142
|
+
LayoutAppSlugRoute: typeof LayoutAppSlugRoute
|
|
125
143
|
LayoutCatalogAppsIndexRoute: typeof LayoutCatalogAppsIndexRoute
|
|
126
144
|
}
|
|
127
145
|
|
|
128
146
|
const LayoutRouteChildren: LayoutRouteChildren = {
|
|
129
147
|
LayoutLoginRoute: LayoutLoginRoute,
|
|
130
148
|
LayoutIndexRoute: LayoutIndexRoute,
|
|
149
|
+
LayoutAppSlugRoute: LayoutAppSlugRoute,
|
|
131
150
|
LayoutCatalogAppsIndexRoute: LayoutCatalogAppsIndexRoute,
|
|
132
151
|
}
|
|
133
152
|
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { createFileRoute } from '@tanstack/react-router'
|
|
2
|
+
import { appCatalogRouteLoader } from '~/modules/appCatalog/routeLoader'
|
|
3
|
+
import { AppCatalogLayout } from '~/modules/appCatalog/ui/layout/AppCatalogLayout'
|
|
4
|
+
import { AppCatalogPage } from '~/modules/appCatalog/ui/pages/AppCatalogPage'
|
|
5
|
+
|
|
6
|
+
export const Route = createFileRoute('/_layout/app/$slug')({
|
|
7
|
+
component: RouteComponent,
|
|
8
|
+
async loader() {
|
|
9
|
+
const appCatalogLoader = await appCatalogRouteLoader()
|
|
10
|
+
return { appCatalogLoader }
|
|
11
|
+
},
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
function RouteComponent() {
|
|
15
|
+
const { queryClient, trpcClient } = Route.useRouteContext()
|
|
16
|
+
const { slug } = Route.useParams()
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<AppCatalogLayout queryClient={queryClient} trpcClient={trpcClient}>
|
|
20
|
+
<AppCatalogPage selectedSlug={slug} />
|
|
21
|
+
</AppCatalogLayout>
|
|
22
|
+
)
|
|
23
|
+
}
|
|
@@ -5,7 +5,6 @@ import { AppCatalogLayout } from '~/modules/appCatalog/ui/layout/AppCatalogLayou
|
|
|
5
5
|
import { AppCatalogPage } from '~/modules/appCatalog/ui/pages/AppCatalogPage'
|
|
6
6
|
|
|
7
7
|
const searchSchema = z.object({
|
|
8
|
-
app: z.string().optional(),
|
|
9
8
|
filterTag: z.string().optional(),
|
|
10
9
|
})
|
|
11
10
|
|
|
@@ -27,6 +27,27 @@ import {
|
|
|
27
27
|
} from '~/ui/dropdown-menu'
|
|
28
28
|
import { Skeleton } from '~/ui/skeleton'
|
|
29
29
|
|
|
30
|
+
function ShaSuffix({ version }: { version: VersionInfo }) {
|
|
31
|
+
if (!version.sha) return null
|
|
32
|
+
if (version.shaUrl) {
|
|
33
|
+
return (
|
|
34
|
+
<>
|
|
35
|
+
{' '}
|
|
36
|
+
<a
|
|
37
|
+
href={version.shaUrl}
|
|
38
|
+
target="_blank"
|
|
39
|
+
rel="noopener noreferrer"
|
|
40
|
+
className="text-muted-foreground hover:text-primary"
|
|
41
|
+
onClick={(e) => e.stopPropagation()}
|
|
42
|
+
>
|
|
43
|
+
({version.sha})
|
|
44
|
+
</a>
|
|
45
|
+
</>
|
|
46
|
+
)
|
|
47
|
+
}
|
|
48
|
+
return <span className="text-muted-foreground"> ({version.sha})</span>
|
|
49
|
+
}
|
|
50
|
+
|
|
30
51
|
function VersionItem({
|
|
31
52
|
label,
|
|
32
53
|
version,
|
|
@@ -34,28 +55,30 @@ function VersionItem({
|
|
|
34
55
|
label: string
|
|
35
56
|
version: VersionInfo
|
|
36
57
|
}) {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
<a
|
|
40
|
-
href={version.url}
|
|
41
|
-
target="_blank"
|
|
42
|
-
rel="noopener noreferrer"
|
|
43
|
-
className="text-xs text-primary hover:text-primary/80 transition-colors font-medium"
|
|
44
|
-
onClick={(e) => e.stopPropagation()}
|
|
45
|
-
title={label}
|
|
46
|
-
>
|
|
47
|
-
{label}: {version.displayName}
|
|
48
|
-
</a>
|
|
49
|
-
)
|
|
50
|
-
}
|
|
58
|
+
// Outer element is a <span> (not <a>) so the optional SHA link can be a
|
|
59
|
+
// sibling anchor without producing invalid nested <a> markup.
|
|
51
60
|
return (
|
|
52
61
|
<span className="text-xs text-muted-foreground" title={label}>
|
|
53
|
-
{label}:
|
|
62
|
+
{label}:{' '}
|
|
63
|
+
{version.url ? (
|
|
64
|
+
<a
|
|
65
|
+
href={version.url}
|
|
66
|
+
target="_blank"
|
|
67
|
+
rel="noopener noreferrer"
|
|
68
|
+
className="text-primary hover:text-primary/80 transition-colors font-medium"
|
|
69
|
+
onClick={(e) => e.stopPropagation()}
|
|
70
|
+
>
|
|
71
|
+
{version.displayName}
|
|
72
|
+
</a>
|
|
73
|
+
) : (
|
|
74
|
+
version.displayName
|
|
75
|
+
)}
|
|
76
|
+
<ShaSuffix version={version} />
|
|
54
77
|
</span>
|
|
55
78
|
)
|
|
56
79
|
}
|
|
57
80
|
|
|
58
|
-
function VersionDisplay({ versions }: { versions: AppVersionInfo }) {
|
|
81
|
+
export function VersionDisplay({ versions }: { versions: AppVersionInfo }) {
|
|
59
82
|
const isLocal =
|
|
60
83
|
versions.backend?.displayName === 'local' && !versions.coreVersion
|
|
61
84
|
|