@igstack/app-catalog-frontend-core 0.16.0 → 0.17.1-alpha-20260821043201
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/mock-backend/magazines.d.ts +1 -1
- package/dist/esm/__tests__/integration/tools/AppDetailTools.d.ts +16 -0
- package/dist/esm/__tests__/integration/tools/CatalogTools.d.ts +12 -0
- package/dist/esm/api/infra/trpc.d.ts +24 -0
- package/dist/esm/modules/appCatalog/hooks/useUpdateApp.d.ts +5 -2
- package/dist/esm/modules/appCatalog/ui/components/AccessPrerequisiteChain.d.ts +1 -1
- package/dist/esm/modules/appCatalog/ui/components/AccessPrerequisiteChain.js.map +1 -1
- package/dist/esm/modules/appCatalog/ui/components/SubResourceDetailPanel.d.ts +9 -0
- package/dist/esm/modules/appCatalog/ui/components/SubResourceDetailPanel.js +90 -0
- package/dist/esm/modules/appCatalog/ui/components/SubResourceDetailPanel.js.map +1 -0
- package/dist/esm/modules/appCatalog/ui/components/SubResourcesSection.d.ts +1 -1
- package/dist/esm/modules/appCatalog/ui/components/SubResourcesSection.js.map +1 -1
- package/dist/esm/modules/appCatalog/ui/grid/AppCatalogGrid.d.ts +7 -1
- package/dist/esm/modules/appCatalog/ui/grid/AppCatalogGrid.js +114 -34
- package/dist/esm/modules/appCatalog/ui/grid/AppCatalogGrid.js.map +1 -1
- package/dist/esm/modules/appCatalog/ui/launcher/LauncherHome.js +8 -8
- package/dist/esm/modules/appCatalog/ui/launcher/LauncherHome.js.map +1 -1
- package/dist/esm/modules/appCatalog/ui/pages/AppCatalogPage.js +31 -4
- package/dist/esm/modules/appCatalog/ui/pages/AppCatalogPage.js.map +1 -1
- package/dist/esm/modules/appCatalog/ui/pages/ServiceDesksPage.js +41 -20
- package/dist/esm/modules/appCatalog/ui/pages/ServiceDesksPage.js.map +1 -1
- package/dist/esm/modules/appCatalog/utils/markdownToPlainText.d.ts +1 -1
- package/dist/esm/modules/appCatalog/utils/markdownToPlainText.js.map +1 -1
- package/dist/esm/routeTree.gen.d.ts +2 -0
- package/dist/esm/routes/_layout/app.$slug.d.ts +1 -0
- package/dist/esm/routes/_layout/app._slug.js +2 -1
- package/dist/esm/routes/_layout/app._slug.js.map +1 -1
- package/dist/esm/routes/_layout/catalog.apps.index.d.ts +1 -0
- package/dist/esm/routes/_layout/catalog.apps.index.js +2 -1
- package/dist/esm/routes/_layout/catalog.apps.index.js.map +1 -1
- package/package.json +5 -7
- package/src/__tests__/integration/appCatalog.integration.test.ts +264 -5
- package/src/__tests__/integration/appDeepLink.integration.test.ts +46 -19
- package/src/__tests__/integration/appFreshness.integration.test.ts +74 -4
- package/src/__tests__/integration/crossRefLink.integration.test.ts +8 -8
- package/src/__tests__/integration/crossRefLinkListView.integration.test.ts +8 -8
- package/src/__tests__/integration/mock-backend/MockBackendConfigurer.ts +2 -0
- package/src/__tests__/integration/mock-backend/magazines.ts +24 -24
- package/src/__tests__/integration/replacementLink.integration.test.ts +2 -2
- package/src/__tests__/integration/searchDeprecatedFallback.integration.test.ts +2 -2
- package/src/__tests__/integration/serviceDesks.integration.test.ts +31 -24
- package/src/__tests__/integration/tools/AppDetailTools.ts +57 -0
- package/src/__tests__/integration/tools/CatalogTools.ts +77 -4
- package/src/__tests__/modules/appCatalog/utils/markdownToPlainText.test.ts +6 -6
- package/src/__tests__/modules/appCatalog/utils/resolveHelpers.test.ts +1 -1
- package/src/__tests__/modules/appCatalog/utils/searchApps.test.ts +11 -7
- package/src/modules/appCatalog/ui/components/AccessPrerequisiteChain.tsx +1 -1
- package/src/modules/appCatalog/ui/components/SubResourceDetailPanel.tsx +124 -0
- package/src/modules/appCatalog/ui/components/SubResourcesSection.tsx +1 -1
- package/src/modules/appCatalog/ui/grid/AppCatalogGrid.tsx +140 -36
- package/src/modules/appCatalog/ui/launcher/LauncherHome.tsx +12 -8
- package/src/modules/appCatalog/ui/pages/AppCatalogPage.tsx +49 -8
- package/src/modules/appCatalog/ui/pages/ServiceDesksPage.tsx +37 -9
- package/src/modules/appCatalog/utils/markdownToPlainText.ts +1 -1
- package/src/routes/_layout/app.$slug.tsx +1 -0
- package/src/routes/_layout/catalog.apps.index.tsx +1 -0
|
@@ -53,6 +53,7 @@ import { markdownToPlainText } from '../../utils/markdownToPlainText'
|
|
|
53
53
|
import { formatRelativeTime } from '../../utils/formatRelativeTime'
|
|
54
54
|
import { TierVariantsSection } from '../components/TierVariantsSection'
|
|
55
55
|
import { SubResourcesSection } from '../components/SubResourcesSection'
|
|
56
|
+
import { SubResourceDetailPanel } from '../components/SubResourceDetailPanel'
|
|
56
57
|
import { getChildResources } from '../../utils/resolveHelpers'
|
|
57
58
|
|
|
58
59
|
export interface AppCatalogGridProps {
|
|
@@ -70,6 +71,12 @@ export interface AppCatalogGridProps {
|
|
|
70
71
|
onClearFilters?: () => void
|
|
71
72
|
/** Called when the user closes the detail panel (Esc or X) — e.g. to navigate back to the catalog URL */
|
|
72
73
|
onClosePanel?: () => void
|
|
74
|
+
/** Currently selected subresource slug */
|
|
75
|
+
selectedSubSlug?: string
|
|
76
|
+
/** Called when user clicks a sub-row */
|
|
77
|
+
onSubClick?: (parentSlug: string, subSlug: string) => void
|
|
78
|
+
/** Called when user clicks Back in sub detail */
|
|
79
|
+
onBackToParent?: () => void
|
|
73
80
|
}
|
|
74
81
|
|
|
75
82
|
function getIconUrl(iconName: string): string {
|
|
@@ -540,10 +547,20 @@ export function AppDetails({
|
|
|
540
547
|
{app.freshness?.lastCheckedAt && (
|
|
541
548
|
<span
|
|
542
549
|
className="inline-flex items-center gap-1 text-xs text-muted-foreground"
|
|
543
|
-
title={
|
|
550
|
+
title={
|
|
551
|
+
app.freshness.lastContentChangeAt
|
|
552
|
+
? `Content last changed ${app.freshness.lastContentChangeAt} · last checked ${app.freshness.lastCheckedAt}`
|
|
553
|
+
: `Last checked ${app.freshness.lastCheckedAt}`
|
|
554
|
+
}
|
|
544
555
|
>
|
|
545
556
|
<Clock className="size-3" />
|
|
546
|
-
Updated
|
|
557
|
+
{/* "Updated" means the content changed, not that a check ran -
|
|
558
|
+
entries predating the field have none, so fall back. */}
|
|
559
|
+
Updated{' '}
|
|
560
|
+
{formatRelativeTime(
|
|
561
|
+
app.freshness.lastContentChangeAt ??
|
|
562
|
+
app.freshness.lastCheckedAt,
|
|
563
|
+
)}
|
|
547
564
|
{app.freshness.isStale && (
|
|
548
565
|
<span className="italic opacity-75">
|
|
549
566
|
{' '}
|
|
@@ -773,16 +790,21 @@ export function AppCatalogGrid({
|
|
|
773
790
|
totalAppsCount,
|
|
774
791
|
onClearFilters,
|
|
775
792
|
onClosePanel,
|
|
793
|
+
selectedSubSlug,
|
|
794
|
+
onSubClick,
|
|
795
|
+
onBackToParent,
|
|
776
796
|
}: AppCatalogGridProps) {
|
|
777
797
|
// Full, unfiltered resource set — the detail panel must resolve the open app
|
|
778
798
|
// from this, not only from the filtered `apps`. Navigating to /app/<slug>
|
|
779
799
|
// (e.g. a deprecated app's "View replacement" link, or any deep link) should
|
|
780
800
|
// render that app like typing the URL in the browser, regardless of the
|
|
781
|
-
// active search/filters (#12).
|
|
782
|
-
const { resources:
|
|
801
|
+
// active search/filters (#12). Also provides approvalMethods for sub-resource panel.
|
|
802
|
+
const { resources: allResources2, approvalMethods } = useAppCatalogContext()
|
|
803
|
+
|
|
783
804
|
const selectedApp = selectedAppSlug
|
|
784
805
|
? (apps.find((a) => a.slug === selectedAppSlug) ??
|
|
785
|
-
|
|
806
|
+
allResources2.find((a) => a.slug === selectedAppSlug && !a.parentSlug) ??
|
|
807
|
+
null)
|
|
786
808
|
: null
|
|
787
809
|
|
|
788
810
|
const groupedApps = groupApps(apps, groupingDefinition, hasSearch)
|
|
@@ -803,10 +825,8 @@ export function AppCatalogGrid({
|
|
|
803
825
|
// Launch history for the secondary "open in new tab" action on each row.
|
|
804
826
|
const { recordClick: recordLaunch } = useAppClickHistory()
|
|
805
827
|
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
const matchedSubResourceMap = React.useMemo(() => {
|
|
809
|
-
const map = new Map<string, string>()
|
|
828
|
+
const matchedSubResourcesMap = React.useMemo(() => {
|
|
829
|
+
const map = new Map<string, Resource[]>()
|
|
810
830
|
if (!searchQuery?.trim() || allResources2.length === 0) return map
|
|
811
831
|
const queryTerms = searchQuery
|
|
812
832
|
.trim()
|
|
@@ -818,7 +838,6 @@ export function AppCatalogGrid({
|
|
|
818
838
|
|
|
819
839
|
for (const r of allResources2) {
|
|
820
840
|
if (!r.parentSlug) continue
|
|
821
|
-
if (map.has(r.parentSlug)) continue
|
|
822
841
|
const nameMatch = allTermsMatch(r.displayName.toLowerCase())
|
|
823
842
|
const aliasMatch = (r.aliases ?? []).some((a) =>
|
|
824
843
|
allTermsMatch(a.toLowerCase()),
|
|
@@ -827,12 +846,23 @@ export function AppCatalogGrid({
|
|
|
827
846
|
? allTermsMatch(r.description.toLowerCase())
|
|
828
847
|
: false
|
|
829
848
|
if (nameMatch || aliasMatch || descMatch) {
|
|
830
|
-
map.
|
|
849
|
+
if (!map.has(r.parentSlug)) {
|
|
850
|
+
map.set(r.parentSlug, [])
|
|
851
|
+
}
|
|
852
|
+
map.get(r.parentSlug)!.push(r)
|
|
831
853
|
}
|
|
832
854
|
}
|
|
833
855
|
return map
|
|
834
856
|
}, [searchQuery, allResources2])
|
|
835
857
|
|
|
858
|
+
// Track expanded parent slugs for sub-resource rows; reset when search changes
|
|
859
|
+
const [expandedParentSlugs, setExpandedParentSlugs] = React.useState<
|
|
860
|
+
Set<string>
|
|
861
|
+
>(() => new Set())
|
|
862
|
+
React.useEffect(() => {
|
|
863
|
+
setExpandedParentSlugs(new Set())
|
|
864
|
+
}, [searchQuery])
|
|
865
|
+
|
|
836
866
|
// Define columns
|
|
837
867
|
const columns = React.useMemo<ColumnDef<Resource>[]>(
|
|
838
868
|
() => [
|
|
@@ -899,12 +929,55 @@ export function AppCatalogGrid({
|
|
|
899
929
|
searchQuery={searchQuery}
|
|
900
930
|
/>
|
|
901
931
|
</span>
|
|
902
|
-
{
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
932
|
+
{/* matched subs: expandable indented rows */}
|
|
933
|
+
{(matchedSubResourcesMap.get(row.original.slug)?.length ?? 0) > 0 &&
|
|
934
|
+
(() => {
|
|
935
|
+
const matchedSubs = matchedSubResourcesMap.get(
|
|
936
|
+
row.original.slug,
|
|
937
|
+
)!
|
|
938
|
+
const COLLAPSED_LIMIT = 5
|
|
939
|
+
const isExpanded = expandedParentSlugs.has(row.original.slug)
|
|
940
|
+
const visibleSubs = isExpanded
|
|
941
|
+
? matchedSubs
|
|
942
|
+
: matchedSubs.slice(0, COLLAPSED_LIMIT)
|
|
943
|
+
const hiddenCount = matchedSubs.length - COLLAPSED_LIMIT
|
|
944
|
+
return (
|
|
945
|
+
<div className="mt-1 space-y-0.5">
|
|
946
|
+
{visibleSubs.map((sub) => (
|
|
947
|
+
<button
|
|
948
|
+
key={sub.slug}
|
|
949
|
+
type="button"
|
|
950
|
+
onClick={(e) => {
|
|
951
|
+
e.stopPropagation()
|
|
952
|
+
onSubClick?.(row.original.slug, sub.slug)
|
|
953
|
+
}}
|
|
954
|
+
className={cn(
|
|
955
|
+
'flex items-center gap-1.5 text-xs pl-3 border-l-2 w-full text-left py-0.5 rounded-sm hover:bg-accent/50 transition-colors',
|
|
956
|
+
selectedSubSlug === sub.slug
|
|
957
|
+
? 'border-primary text-primary font-medium'
|
|
958
|
+
: 'border-muted-foreground/30 text-muted-foreground hover:text-foreground',
|
|
959
|
+
)}
|
|
960
|
+
>
|
|
961
|
+
<span>{sub.displayName}</span>
|
|
962
|
+
</button>
|
|
963
|
+
))}
|
|
964
|
+
{!isExpanded && hiddenCount > 0 && (
|
|
965
|
+
<button
|
|
966
|
+
type="button"
|
|
967
|
+
onClick={(e) => {
|
|
968
|
+
e.stopPropagation()
|
|
969
|
+
setExpandedParentSlugs(
|
|
970
|
+
(prev) => new Set([...prev, row.original.slug]),
|
|
971
|
+
)
|
|
972
|
+
}}
|
|
973
|
+
className="text-xs pl-3 border-l-2 border-muted-foreground/30 text-muted-foreground hover:text-primary w-full text-left py-0.5"
|
|
974
|
+
>
|
|
975
|
+
... {hiddenCount} more
|
|
976
|
+
</button>
|
|
977
|
+
)}
|
|
978
|
+
</div>
|
|
979
|
+
)
|
|
980
|
+
})()}
|
|
908
981
|
</div>
|
|
909
982
|
),
|
|
910
983
|
},
|
|
@@ -937,7 +1010,15 @@ export function AppCatalogGrid({
|
|
|
937
1010
|
},
|
|
938
1011
|
},
|
|
939
1012
|
],
|
|
940
|
-
[
|
|
1013
|
+
[
|
|
1014
|
+
searchQuery,
|
|
1015
|
+
matchedSubResourcesMap,
|
|
1016
|
+
expandedParentSlugs,
|
|
1017
|
+
selectedSubSlug,
|
|
1018
|
+
onSubClick,
|
|
1019
|
+
setExpandedParentSlugs,
|
|
1020
|
+
recordLaunch,
|
|
1021
|
+
],
|
|
941
1022
|
)
|
|
942
1023
|
|
|
943
1024
|
// Create a single table instance with all apps
|
|
@@ -1113,24 +1194,47 @@ export function AppCatalogGrid({
|
|
|
1113
1194
|
className="overflow-hidden"
|
|
1114
1195
|
>
|
|
1115
1196
|
<div className="h-full overflow-y-auto border-l bg-background pl-4">
|
|
1116
|
-
{selectedApp
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1197
|
+
{/* selectedApp is non-null here; isPanelOpen guards this block */}
|
|
1198
|
+
<div className="relative">
|
|
1199
|
+
<Button
|
|
1200
|
+
variant="ghost"
|
|
1201
|
+
size="icon"
|
|
1202
|
+
className="absolute top-4 right-4 z-10 hover:bg-accent"
|
|
1203
|
+
onClick={handleClosePanel}
|
|
1204
|
+
aria-label="Close details panel"
|
|
1205
|
+
>
|
|
1206
|
+
<X className="h-5 w-5" />
|
|
1207
|
+
</Button>
|
|
1208
|
+
{/* eslint-disable-next-line @typescript-eslint/no-unnecessary-condition */}
|
|
1209
|
+
{selectedApp &&
|
|
1210
|
+
(() => {
|
|
1211
|
+
const app = selectedApp
|
|
1212
|
+
const selectedSub = selectedSubSlug
|
|
1213
|
+
? allResources2.find(
|
|
1214
|
+
(r) =>
|
|
1215
|
+
r.slug === selectedSubSlug &&
|
|
1216
|
+
r.parentSlug === app.slug,
|
|
1217
|
+
)
|
|
1218
|
+
: null
|
|
1219
|
+
if (selectedSub != null) {
|
|
1220
|
+
return (
|
|
1221
|
+
<SubResourceDetailPanel
|
|
1222
|
+
subResource={selectedSub}
|
|
1223
|
+
parent={app}
|
|
1224
|
+
approvalMethods={approvalMethods}
|
|
1225
|
+
onBack={onBackToParent ?? (() => undefined)}
|
|
1226
|
+
/>
|
|
1227
|
+
)
|
|
1228
|
+
}
|
|
1229
|
+
return (
|
|
1230
|
+
<AppDetails
|
|
1231
|
+
app={selectedApp}
|
|
1232
|
+
onAppClick={onAppClick}
|
|
1233
|
+
onClosePanel={handleClosePanel}
|
|
1234
|
+
/>
|
|
1235
|
+
)
|
|
1236
|
+
})()}
|
|
1237
|
+
</div>
|
|
1134
1238
|
</div>
|
|
1135
1239
|
</ResizablePanel>
|
|
1136
1240
|
</>
|
|
@@ -370,21 +370,25 @@ export function LauncherHome({
|
|
|
370
370
|
[topSlugs, bySlug],
|
|
371
371
|
)
|
|
372
372
|
|
|
373
|
-
// New this week: apps whose
|
|
373
|
+
// New this week: apps whose content actually changed in the last 7 days, as a
|
|
374
374
|
// proxy for "recently added/updated" (frontend Resource has no createdAt yet;
|
|
375
|
-
// see #38).
|
|
375
|
+
// see #38). A bare re-check is not an update, so prefer the content-change
|
|
376
|
+
// date, falling back to the check date only for entries that predate it.
|
|
377
|
+
// Falls back to empty (section hidden) when no freshness data.
|
|
376
378
|
const fresh = useMemo(() => {
|
|
377
379
|
const weekAgo = Date.now() - 7 * 24 * 60 * 60 * 1000
|
|
380
|
+
const updatedAt = (a: Resource) =>
|
|
381
|
+
a.freshness?.lastContentChangeAt ?? a.freshness?.lastCheckedAt ?? null
|
|
378
382
|
return apps
|
|
379
383
|
.filter((a) => {
|
|
380
|
-
const t = a
|
|
384
|
+
const t = updatedAt(a)
|
|
381
385
|
return t ? new Date(t).getTime() >= weekAgo : false
|
|
382
386
|
})
|
|
383
|
-
.sort(
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
387
|
+
.sort(
|
|
388
|
+
(a, b) =>
|
|
389
|
+
new Date(updatedAt(b) ?? 0).getTime() -
|
|
390
|
+
new Date(updatedAt(a) ?? 0).getTime(),
|
|
391
|
+
)
|
|
388
392
|
.slice(0, 6)
|
|
389
393
|
}, [apps])
|
|
390
394
|
|
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
import type { Resource } from '@igstack/app-catalog-backend-core'
|
|
2
|
-
import { useNavigate } from '@tanstack/react-router'
|
|
2
|
+
import { useNavigate, useSearch } from '@tanstack/react-router'
|
|
3
3
|
import { X } from 'lucide-react'
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
useCallback,
|
|
6
|
+
useDeferredValue,
|
|
7
|
+
useEffect,
|
|
8
|
+
useMemo,
|
|
9
|
+
useState,
|
|
10
|
+
} from 'react'
|
|
5
11
|
import { Button } from '~/ui/button'
|
|
6
12
|
import {
|
|
7
13
|
Empty,
|
|
@@ -34,6 +40,35 @@ export function AppCatalogPage({
|
|
|
34
40
|
const selectedAppSlug = selectedSlug
|
|
35
41
|
|
|
36
42
|
// Search value from context (URL-synced in AppCatalogFiltersContext)
|
|
43
|
+
const search = useSearch({ strict: false })
|
|
44
|
+
const selectedSubSlug = search.sub
|
|
45
|
+
|
|
46
|
+
const handleSubClick = useCallback(
|
|
47
|
+
(parentSlug: string, subSlug: string) => {
|
|
48
|
+
void navigate({
|
|
49
|
+
to: '/app/$slug',
|
|
50
|
+
params: { slug: parentSlug },
|
|
51
|
+
search: (prev) => ({ ...prev, sub: subSlug }),
|
|
52
|
+
})
|
|
53
|
+
},
|
|
54
|
+
[navigate],
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
const handleBackToParent = useCallback(() => {
|
|
58
|
+
// Return to parent app detail, clearing the sub-resource selection.
|
|
59
|
+
// selectedAppSlug is the parent slug (set by the /app/$slug route).
|
|
60
|
+
if (selectedAppSlug) {
|
|
61
|
+
void navigate({
|
|
62
|
+
to: '/app/$slug',
|
|
63
|
+
params: { slug: selectedAppSlug },
|
|
64
|
+
search: (prev) => {
|
|
65
|
+
const { sub: _sub, ...rest } = prev as Record<string, string>
|
|
66
|
+
return rest
|
|
67
|
+
},
|
|
68
|
+
})
|
|
69
|
+
}
|
|
70
|
+
}, [navigate, selectedAppSlug])
|
|
71
|
+
|
|
37
72
|
const searchValue = filterState.searchValue
|
|
38
73
|
const setSearchValue = actions.setSearchValue
|
|
39
74
|
|
|
@@ -214,12 +249,15 @@ export function AppCatalogPage({
|
|
|
214
249
|
}
|
|
215
250
|
|
|
216
251
|
// Adaptive-home launcher (#38): shown whenever no structural filter
|
|
217
|
-
// (recentMode / tagFilters) is active. A selected app no longer
|
|
218
|
-
// old grid — instead the launcher stays as the backdrop and the app
|
|
219
|
-
// renders in a slide-over panel (#38 item B). The legacy grid+split-pane
|
|
220
|
-
//
|
|
252
|
+
// (recentMode / tagFilters / active search) is active. A selected app no longer
|
|
253
|
+
// forces the old grid — instead the launcher stays as the backdrop and the app
|
|
254
|
+
// detail renders in a slide-over panel (#38 item B). The legacy grid+split-pane
|
|
255
|
+
// is used for recent/tag filter views AND for active search (so expandable
|
|
256
|
+
// sub-resource rows and query highlighting in AppCatalogGrid are visible).
|
|
221
257
|
const showLauncherHome =
|
|
222
|
-
!filterState.recentMode &&
|
|
258
|
+
!filterState.recentMode &&
|
|
259
|
+
Object.keys(filterState.tagFilters).length === 0 &&
|
|
260
|
+
!deferredSearchValue.trim()
|
|
223
261
|
|
|
224
262
|
// The app whose detail slide-over is open over the launcher backdrop.
|
|
225
263
|
const launcherSelectedApp = useMemo(
|
|
@@ -346,11 +384,14 @@ export function AppCatalogPage({
|
|
|
346
384
|
<AppCatalogGrid
|
|
347
385
|
apps={filteredApps}
|
|
348
386
|
selectedAppSlug={selectedAppSlug}
|
|
387
|
+
selectedSubSlug={selectedSubSlug}
|
|
349
388
|
groupingDefinition={groupingDefinition}
|
|
350
389
|
onAppClick={handleAppClick}
|
|
351
390
|
onClosePanel={() => void navigate({ to: '/' })}
|
|
391
|
+
onSubClick={handleSubClick}
|
|
392
|
+
onBackToParent={handleBackToParent}
|
|
352
393
|
hasSearch={!!deferredSearchValue}
|
|
353
|
-
searchQuery={
|
|
394
|
+
searchQuery={searchValue}
|
|
354
395
|
totalAppsCount={totalAppsCount}
|
|
355
396
|
onClearFilters={handleClearFilters}
|
|
356
397
|
/>
|
|
@@ -62,31 +62,59 @@ export function ServiceDesksPage() {
|
|
|
62
62
|
const url = desk.config.url
|
|
63
63
|
const description = desk.config.description
|
|
64
64
|
return (
|
|
65
|
-
<TableRow
|
|
65
|
+
<TableRow
|
|
66
|
+
key={desk.slug}
|
|
67
|
+
className={
|
|
68
|
+
url ? 'cursor-pointer hover:bg-muted/50' : undefined
|
|
69
|
+
}
|
|
70
|
+
onClick={
|
|
71
|
+
url
|
|
72
|
+
? () =>
|
|
73
|
+
window.open(url, '_blank', 'noopener,noreferrer')
|
|
74
|
+
: undefined
|
|
75
|
+
}
|
|
76
|
+
>
|
|
66
77
|
<TableCell className="font-medium align-top">
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
78
|
+
{url ? (
|
|
79
|
+
<a
|
|
80
|
+
href={url}
|
|
81
|
+
target="_blank"
|
|
82
|
+
rel="noopener noreferrer"
|
|
83
|
+
className="hover:text-primary hover:underline"
|
|
84
|
+
data-testid="service-desk-name"
|
|
85
|
+
onClick={(e) => e.stopPropagation()}
|
|
86
|
+
>
|
|
87
|
+
{desk.displayName}
|
|
88
|
+
</a>
|
|
89
|
+
) : (
|
|
90
|
+
<span data-testid="service-desk-name">
|
|
91
|
+
{desk.displayName}
|
|
92
|
+
</span>
|
|
93
|
+
)}
|
|
70
94
|
{description && (
|
|
71
95
|
<span className="block text-xs font-normal text-muted-foreground">
|
|
72
96
|
{description}
|
|
73
97
|
</span>
|
|
74
98
|
)}
|
|
75
|
-
|
|
76
|
-
<TableCell>
|
|
77
|
-
{url ? (
|
|
99
|
+
{url && (
|
|
78
100
|
<a
|
|
79
101
|
href={url}
|
|
80
102
|
target="_blank"
|
|
81
103
|
rel="noopener noreferrer"
|
|
82
|
-
className="inline-flex items-center gap-1 text-primary hover:underline
|
|
104
|
+
className="mt-0.5 inline-flex items-center gap-1 text-xs text-primary/70 hover:text-primary hover:underline"
|
|
83
105
|
title={url}
|
|
106
|
+
onClick={(e) => e.stopPropagation()}
|
|
84
107
|
>
|
|
85
108
|
{url.replace(/^https?:\/\//, '')}
|
|
86
109
|
<ExternalLink className="size-3 shrink-0" />
|
|
87
110
|
</a>
|
|
111
|
+
)}
|
|
112
|
+
</TableCell>
|
|
113
|
+
<TableCell className="text-muted-foreground text-xs">
|
|
114
|
+
{url ? (
|
|
115
|
+
<ExternalLink className="size-4 text-primary/50" />
|
|
88
116
|
) : (
|
|
89
|
-
<span
|
|
117
|
+
<span>—</span>
|
|
90
118
|
)}
|
|
91
119
|
</TableCell>
|
|
92
120
|
</TableRow>
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Reduce a short markdown string to its visible plain text.
|
|
3
3
|
*
|
|
4
4
|
* Descriptions may contain markdown — notably cross-reference links like
|
|
5
|
-
* `[
|
|
5
|
+
* `[Team Chat](/app/team-chat)` (#25). The detail panel renders
|
|
6
6
|
* those as interactive links via {@link MarkdownText}, but in compact / clamped
|
|
7
7
|
* surfaces (grid list preview, table row, filter combobox, sub-resources) an
|
|
8
8
|
* interactive link would break `line-clamp` and can't be reliably highlighted
|
|
@@ -9,6 +9,7 @@ import { AppCatalogPage } from '~/modules/appCatalog/ui/pages/AppCatalogPage'
|
|
|
9
9
|
const searchSchema = z.object({
|
|
10
10
|
app: z.string().optional(),
|
|
11
11
|
q: z.string().optional(),
|
|
12
|
+
sub: z.string().optional(),
|
|
12
13
|
})
|
|
13
14
|
|
|
14
15
|
export const Route = createFileRoute('/_layout/catalog/apps/')({
|