@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.
Files changed (56) hide show
  1. package/dist/esm/__tests__/integration/mock-backend/magazines.d.ts +1 -1
  2. package/dist/esm/__tests__/integration/tools/AppDetailTools.d.ts +16 -0
  3. package/dist/esm/__tests__/integration/tools/CatalogTools.d.ts +12 -0
  4. package/dist/esm/api/infra/trpc.d.ts +24 -0
  5. package/dist/esm/modules/appCatalog/hooks/useUpdateApp.d.ts +5 -2
  6. package/dist/esm/modules/appCatalog/ui/components/AccessPrerequisiteChain.d.ts +1 -1
  7. package/dist/esm/modules/appCatalog/ui/components/AccessPrerequisiteChain.js.map +1 -1
  8. package/dist/esm/modules/appCatalog/ui/components/SubResourceDetailPanel.d.ts +9 -0
  9. package/dist/esm/modules/appCatalog/ui/components/SubResourceDetailPanel.js +90 -0
  10. package/dist/esm/modules/appCatalog/ui/components/SubResourceDetailPanel.js.map +1 -0
  11. package/dist/esm/modules/appCatalog/ui/components/SubResourcesSection.d.ts +1 -1
  12. package/dist/esm/modules/appCatalog/ui/components/SubResourcesSection.js.map +1 -1
  13. package/dist/esm/modules/appCatalog/ui/grid/AppCatalogGrid.d.ts +7 -1
  14. package/dist/esm/modules/appCatalog/ui/grid/AppCatalogGrid.js +114 -34
  15. package/dist/esm/modules/appCatalog/ui/grid/AppCatalogGrid.js.map +1 -1
  16. package/dist/esm/modules/appCatalog/ui/launcher/LauncherHome.js +8 -8
  17. package/dist/esm/modules/appCatalog/ui/launcher/LauncherHome.js.map +1 -1
  18. package/dist/esm/modules/appCatalog/ui/pages/AppCatalogPage.js +31 -4
  19. package/dist/esm/modules/appCatalog/ui/pages/AppCatalogPage.js.map +1 -1
  20. package/dist/esm/modules/appCatalog/ui/pages/ServiceDesksPage.js +41 -20
  21. package/dist/esm/modules/appCatalog/ui/pages/ServiceDesksPage.js.map +1 -1
  22. package/dist/esm/modules/appCatalog/utils/markdownToPlainText.d.ts +1 -1
  23. package/dist/esm/modules/appCatalog/utils/markdownToPlainText.js.map +1 -1
  24. package/dist/esm/routeTree.gen.d.ts +2 -0
  25. package/dist/esm/routes/_layout/app.$slug.d.ts +1 -0
  26. package/dist/esm/routes/_layout/app._slug.js +2 -1
  27. package/dist/esm/routes/_layout/app._slug.js.map +1 -1
  28. package/dist/esm/routes/_layout/catalog.apps.index.d.ts +1 -0
  29. package/dist/esm/routes/_layout/catalog.apps.index.js +2 -1
  30. package/dist/esm/routes/_layout/catalog.apps.index.js.map +1 -1
  31. package/package.json +5 -7
  32. package/src/__tests__/integration/appCatalog.integration.test.ts +264 -5
  33. package/src/__tests__/integration/appDeepLink.integration.test.ts +46 -19
  34. package/src/__tests__/integration/appFreshness.integration.test.ts +74 -4
  35. package/src/__tests__/integration/crossRefLink.integration.test.ts +8 -8
  36. package/src/__tests__/integration/crossRefLinkListView.integration.test.ts +8 -8
  37. package/src/__tests__/integration/mock-backend/MockBackendConfigurer.ts +2 -0
  38. package/src/__tests__/integration/mock-backend/magazines.ts +24 -24
  39. package/src/__tests__/integration/replacementLink.integration.test.ts +2 -2
  40. package/src/__tests__/integration/searchDeprecatedFallback.integration.test.ts +2 -2
  41. package/src/__tests__/integration/serviceDesks.integration.test.ts +31 -24
  42. package/src/__tests__/integration/tools/AppDetailTools.ts +57 -0
  43. package/src/__tests__/integration/tools/CatalogTools.ts +77 -4
  44. package/src/__tests__/modules/appCatalog/utils/markdownToPlainText.test.ts +6 -6
  45. package/src/__tests__/modules/appCatalog/utils/resolveHelpers.test.ts +1 -1
  46. package/src/__tests__/modules/appCatalog/utils/searchApps.test.ts +11 -7
  47. package/src/modules/appCatalog/ui/components/AccessPrerequisiteChain.tsx +1 -1
  48. package/src/modules/appCatalog/ui/components/SubResourceDetailPanel.tsx +124 -0
  49. package/src/modules/appCatalog/ui/components/SubResourcesSection.tsx +1 -1
  50. package/src/modules/appCatalog/ui/grid/AppCatalogGrid.tsx +140 -36
  51. package/src/modules/appCatalog/ui/launcher/LauncherHome.tsx +12 -8
  52. package/src/modules/appCatalog/ui/pages/AppCatalogPage.tsx +49 -8
  53. package/src/modules/appCatalog/ui/pages/ServiceDesksPage.tsx +37 -9
  54. package/src/modules/appCatalog/utils/markdownToPlainText.ts +1 -1
  55. package/src/routes/_layout/app.$slug.tsx +1 -0
  56. 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={app.freshness.lastCheckedAt}
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 {formatRelativeTime(app.freshness.lastCheckedAt)}
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: allResourcesForDetail } = useAppCatalogContext()
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
- allResourcesForDetail.find((a) => a.slug === selectedAppSlug))
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
- // Build a map of parentSlug -> matched child resource displayName for search annotation
807
- const { resources: allResources2 } = useAppCatalogContext()
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.set(r.parentSlug, r.displayName)
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
- {matchedSubResourceMap.get(row.original.slug) && (
903
- <div className="text-xs text-primary mt-0.5">
904
- Matched sub-resource:{' '}
905
- {matchedSubResourceMap.get(row.original.slug)}
906
- </div>
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
- [searchQuery, matchedSubResourceMap, recordLaunch],
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
- <div className="relative">
1118
- <Button
1119
- variant="ghost"
1120
- size="icon"
1121
- className="absolute top-4 right-4 z-10 hover:bg-accent"
1122
- onClick={handleClosePanel}
1123
- aria-label="Close details panel"
1124
- >
1125
- <X className="h-5 w-5" />
1126
- </Button>
1127
- <AppDetails
1128
- app={selectedApp}
1129
- onAppClick={onAppClick}
1130
- onClosePanel={handleClosePanel}
1131
- />
1132
- </div>
1133
- ) : null}
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 sources were checked in the last 7 days, as a
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). Falls back to empty (section hidden) when no freshness data.
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.freshness?.lastCheckedAt
384
+ const t = updatedAt(a)
381
385
  return t ? new Date(t).getTime() >= weekAgo : false
382
386
  })
383
- .sort((a, b) => {
384
- const ta = new Date(a.freshness?.lastCheckedAt ?? 0).getTime()
385
- const tb = new Date(b.freshness?.lastCheckedAt ?? 0).getTime()
386
- return tb - ta
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 { useDeferredValue, useEffect, useMemo, useState } from 'react'
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 forces the
218
- // old grid — instead the launcher stays as the backdrop and the app detail
219
- // renders in a slide-over panel (#38 item B). The legacy grid+split-pane is
220
- // reserved for the recent/tag filter views.
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 && Object.keys(filterState.tagFilters).length === 0
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={deferredSearchValue}
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 key={desk.slug}>
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
- <span data-testid="service-desk-name">
68
- {desk.displayName}
69
- </span>
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
- </TableCell>
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 break-all"
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 className="text-muted-foreground">—</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
- * `[Signatera Portal](/app/signatera-portal)` (#25). The detail panel renders
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
@@ -15,6 +15,7 @@ const searchSchema = z.object({
15
15
  filters: z.string().optional(),
16
16
  deprecated: z.string().optional(),
17
17
  q: z.string().optional(),
18
+ sub: z.string().optional(),
18
19
  })
19
20
 
20
21
  export const Route = createFileRoute('/_layout/app/$slug')({
@@ -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/')({