@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
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AppCatalogPage.js","sources":["../../../../../../src/modules/appCatalog/ui/pages/AppCatalogPage.tsx"],"sourcesContent":["import type { Resource } from '@igstack/app-catalog-backend-core'\nimport { useNavigate } from '@tanstack/react-router'\nimport { X } from 'lucide-react'\nimport { useDeferredValue, useEffect, useMemo, useState } from 'react'\nimport { Button } from '~/ui/button'\nimport {\n Empty,\n EmptyContent,\n EmptyDescription,\n EmptyHeader,\n EmptyMedia,\n EmptyTitle,\n} from '~/ui/empty'\nimport { useAppCatalogContext } from '../../context/AppCatalogContext'\nimport { useAppClickHistory } from '../../hooks/useAppClickHistory'\nimport { useAppCounts } from '../../hooks/useAppCounts'\nimport { searchResources } from '../../utils/searchApps'\nimport { OnboardingCard } from '../components/OnboardingCard'\nimport { useAppCatalogFilters } from '../context/AppCatalogFiltersContext'\nimport { FilterBar } from '../filters/FilterBar'\nimport { AppCatalogGrid } from '../grid/AppCatalogGrid'\nimport { LauncherHome } from '../launcher/LauncherHome'\nimport { LauncherDetailPanel } from '../launcher/LauncherDetailPanel'\n\nexport function AppCatalogPage({\n selectedSlug,\n}: { selectedSlug?: string } = {}) {\n const { resources, isLoadingApps, tagsDefinitions } = useAppCatalogContext()\n const { state: filterState, actions } = useAppCatalogFilters()\n const { getTopApps } = useAppClickHistory()\n const navigate = useNavigate()\n\n // Selected app comes from the route path (/app/<slug>); undefined = nothing open\n const selectedAppSlug = selectedSlug\n\n // Search value from context (URL-synced in AppCatalogFiltersContext)\n const searchValue = filterState.searchValue\n const setSearchValue = actions.setSearchValue\n\n // Defer the search value used for filtering to avoid blocking the input\n const deferredSearchValue = useDeferredValue(searchValue)\n\n // State for top apps (loaded async)\n const [topAppSlugs, setTopAppSlugs] = useState<string[]>([])\n\n // Load top apps on mount to calculate recent count\n useEffect(() => {\n void getTopApps(10).then(setTopAppSlugs)\n }, [getTopApps])\n\n // Get root resources for filtering (children handled internally by searchResources)\n const rootResources = useMemo(\n () => resources.filter((r) => !r.parentSlug),\n [resources],\n )\n\n // Dev-only skew warning: data arrived (resources > 0) but nothing is top-level\n // (rootResources === 0). That fingerprints a frontend/backend-core version skew\n // or a stale service worker — NOT a normal empty search/filter (those still have\n // rootResources). Guarded to dev so prod users never see it.\n useEffect(() => {\n if (\n import.meta.env.DEV &&\n !isLoadingApps &&\n resources.length > 0 &&\n rootResources.length === 0\n ) {\n console.warn(\n `[app-catalog] Loaded ${resources.length} resources but 0 are top-level — ` +\n `likely a frontend/backend-core version skew or a stale service worker. ` +\n `Check the version footer (be X / fe Y) and hard-reload.`,\n )\n }\n }, [isLoadingApps, resources.length, rootResources.length])\n\n const { filteredApps, didDeprecatedFallback } = useMemo(() => {\n const childResources = resources.filter((r) => r.parentSlug)\n\n // Apply recent mode / tag filters / search over a base set of root apps.\n const runPipeline = (base: typeof rootResources) => {\n let result = base\n\n // Apply recent mode or tag filters\n if (filterState.recentMode) {\n // Filter to top 10 most clicked apps\n result = result.filter((app) => topAppSlugs.includes(app.slug))\n } else if (Object.keys(filterState.tagFilters).length > 0) {\n // Apply tag filters (AND condition)\n result = result.filter((app) => {\n return Object.entries(filterState.tagFilters).every(\n ([prefix, value]) => {\n const fullTag = `${prefix}:${value}`\n return app.tags?.some(\n (tag) => tag.toLowerCase() === fullTag.toLowerCase(),\n )\n },\n )\n })\n }\n\n // Apply search (using deferred value). Pass all resources so children\n // contribute to parent scoring.\n return searchResources(\n [...result, ...childResources],\n deferredSearchValue,\n )\n }\n\n // Matches among non-deprecated (\"active\") apps only.\n const activeMatches = runPipeline(\n rootResources.filter((app) => !app.deprecated),\n )\n\n // #11: derive the fallback purely from the data (not the toggle), so it\n // stays stable after the toggle auto-enables below — otherwise the notice\n // would flicker off as soon as showDeprecated flips to true.\n // Fallback applies when there's a query, no active matches, but deprecated\n // apps DO match.\n const hasQuery = deferredSearchValue.trim() !== ''\n if (hasQuery && activeMatches.length === 0) {\n const allMatches = runPipeline(rootResources)\n if (allMatches.length > 0) {\n return { filteredApps: allMatches, didDeprecatedFallback: true }\n }\n }\n\n // Normal display: include deprecated only when the toggle is on.\n const result = filterState.showDeprecated\n ? runPipeline(rootResources)\n : activeMatches\n return { filteredApps: result, didDeprecatedFallback: false }\n }, [\n rootResources,\n resources,\n deferredSearchValue,\n filterState.recentMode,\n filterState.tagFilters,\n filterState.showDeprecated,\n topAppSlugs,\n ])\n\n // #11: when the search fell back to deprecated results, enable the\n // \"Show Deprecated Apps\" toggle so the state is visible and consistent.\n // Runs as an effect (not in render) to avoid a side effect during useMemo.\n useEffect(() => {\n if (didDeprecatedFallback && !filterState.showDeprecated) {\n actions.setShowDeprecated(true)\n }\n }, [didDeprecatedFallback, filterState.showDeprecated, actions])\n\n // Calculate counts for FilterBar\n const { allCount, recentCount, deprecatedCount } = useAppCounts({\n apps: rootResources,\n topAppSlugs,\n searchValue: deferredSearchValue,\n })\n\n // Auto-open details when only 1 result — ONLY in the legacy grid path\n // (recent/tag filters active). In the launcher (#38) typing shows the\n // search-morph results list; auto-navigating to /app/<slug> on a single\n // match would yank the user out of the launcher into the old grid+panel\n // (reported bug: searching \"biom\" jumped straight to an app-detail page).\n // The morph already renders the single match as a keyboard-selectable row —\n // the user presses ↵ or clicks to open it.\n const legacyGridActive =\n filterState.recentMode || Object.keys(filterState.tagFilters).length > 0\n useEffect(() => {\n if (\n legacyGridActive &&\n filteredApps.length === 1 &&\n filteredApps[0] &&\n !selectedAppSlug\n ) {\n void navigate({\n to: '/app/$slug',\n params: { slug: filteredApps[0].slug },\n search: (prev) => prev,\n replace: true,\n })\n }\n }, [legacyGridActive, filteredApps, selectedAppSlug, navigate])\n\n // #22: alias → canonical redirect. When an app is renamed its old slug is\n // kept in `aliases[]`. If the URL slug matches no canonical slug but does\n // match some app's alias, redirect (replace) to that app's canonical slug so\n // old bookmarks resolve instead of showing a blank catalog. Guard on a real\n // canonical miss so we never fight the normal detail-open path. Note: this is\n // a client-side SPA redirect (replace), not an HTTP 301 — see ig-umbrella#22.\n useEffect(() => {\n if (!selectedAppSlug || resources.length === 0) return\n const canonical = resources.some((r) => r.slug === selectedAppSlug)\n if (canonical) return\n const aliased = resources.find((r) => r.aliases?.includes(selectedAppSlug))\n if (aliased) {\n void navigate({\n to: '/app/$slug',\n params: { slug: aliased.slug },\n search: (prev) => prev,\n replace: true,\n })\n }\n }, [selectedAppSlug, resources, navigate])\n\n const handleAppClick = (app: Resource) => {\n void navigate({\n to: '/app/$slug',\n params: { slug: app.slug },\n search: (prev) => prev,\n })\n }\n\n const handleLaunch = (app: Resource) => {\n if (app.appUrl) window.open(app.appUrl, '_blank', 'noopener,noreferrer')\n }\n\n // Adaptive-home launcher (#38): shown whenever no structural filter\n // (recentMode / tagFilters) is active. A selected app no longer forces the\n // old grid — instead the launcher stays as the backdrop and the app detail\n // renders in a slide-over panel (#38 item B). The legacy grid+split-pane is\n // reserved for the recent/tag filter views.\n const showLauncherHome =\n !filterState.recentMode && Object.keys(filterState.tagFilters).length === 0\n\n // The app whose detail slide-over is open over the launcher backdrop.\n const launcherSelectedApp = useMemo(\n () =>\n selectedAppSlug\n ? (resources.find((r) => r.slug === selectedAppSlug) ?? null)\n : null,\n [selectedAppSlug, resources],\n )\n\n const handleClearFilters = () => {\n setSearchValue('')\n actions.clearAllFilters()\n void navigate({ to: '/' })\n }\n\n // Calculate total apps count (respecting showDeprecated setting)\n const totalAppsCount = useMemo(() => {\n let count = rootResources.length\n if (!filterState.showDeprecated) {\n count = rootResources.filter((app) => !app.deprecated).length\n }\n return count\n }, [rootResources, filterState.showDeprecated])\n\n if (isLoadingApps) {\n return <div className=\"py-6 text-muted-foreground\">Loading…</div>\n }\n\n // Use first tag definition for grouping\n const groupingDefinition = tagsDefinitions[0]\n\n // Adaptive-home launcher view (#38): the discovery spine or the search-morph\n // results list (handled inside LauncherHome). Owns vertical scroll —\n // MainLayout is h-screen/overflow-hidden, so the launcher must scroll\n // internally to reveal the full Browse-all list as the user scrolls.\n if (showLauncherHome) {\n return (\n <div className=\"flex-1 min-h-0 overflow-y-auto\">\n <LauncherHome\n apps={rootResources.filter(\n (a) => filterState.showDeprecated || !a.deprecated,\n )}\n // All resources incl. children — so search matches sub-resources\n // (e.g. an AWS account) and surfaces their parent, preserving the\n // existing cross-sub-resource search behavior.\n allResources={resources}\n searchValue={searchValue}\n onSearchChange={setSearchValue}\n onAppClick={handleAppClick}\n onLaunch={handleLaunch}\n totalCount={totalAppsCount}\n />\n {/* #38 item B: app detail as a slide-over over the launcher backdrop,\n instead of dropping into the old grid + split-pane. */}\n {launcherSelectedApp && (\n <LauncherDetailPanel\n app={launcherSelectedApp}\n onClose={() => void navigate({ to: '/' })}\n onAppClick={handleAppClick}\n />\n )}\n </div>\n )\n }\n\n return (\n <div className=\"flex flex-col flex-1 min-h-0\">\n <div className=\"shrink-0\">\n <OnboardingCard />\n </div>\n\n <div className=\"shrink-0\">\n <FilterBar\n totalCount={allCount}\n recentCount={recentCount}\n deprecatedCount={deprecatedCount}\n apps={rootResources}\n />\n </div>\n\n {didDeprecatedFallback && (\n <div\n role=\"status\"\n className=\"shrink-0 px-1 pb-2 text-sm text-muted-foreground\"\n >\n {`No active apps${\n searchValue ? ` for \"${searchValue}\"` : ''\n } — showing deprecated matches.`}\n </div>\n )}\n\n <div className=\"flex-1 min-h-0\">\n {filteredApps.length === 0 && !selectedAppSlug ? (\n <Empty>\n <EmptyHeader>\n <EmptyMedia variant=\"icon\">\n <X className=\"h-6 w-6\" />\n </EmptyMedia>\n <EmptyTitle>\n No apps found{searchValue && ` for \"${searchValue}\"`}\n </EmptyTitle>\n <EmptyDescription>\n Try adjusting your search or filters\n </EmptyDescription>\n </EmptyHeader>\n <EmptyContent>\n {searchValue && (\n <Button\n variant=\"outline\"\n onClick={() => {\n setSearchValue('')\n void navigate({ to: '/' })\n }}\n className=\"gap-2\"\n >\n <X className=\"h-4 w-4\" />\n Clear search\n </Button>\n )}\n </EmptyContent>\n </Empty>\n ) : (\n <AppCatalogGrid\n apps={filteredApps}\n selectedAppSlug={selectedAppSlug}\n groupingDefinition={groupingDefinition}\n onAppClick={handleAppClick}\n onClosePanel={() => void navigate({ to: '/' })}\n hasSearch={!!deferredSearchValue}\n searchQuery={deferredSearchValue}\n totalAppsCount={totalAppsCount}\n onClearFilters={handleClearFilters}\n />\n )}\n </div>\n </div>\n )\n}\n"],"names":["result"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"AppCatalogPage.js","sources":["../../../../../../src/modules/appCatalog/ui/pages/AppCatalogPage.tsx"],"sourcesContent":["import type { Resource } from '@igstack/app-catalog-backend-core'\nimport { useNavigate, useSearch } from '@tanstack/react-router'\nimport { X } from 'lucide-react'\nimport {\n useCallback,\n useDeferredValue,\n useEffect,\n useMemo,\n useState,\n} from 'react'\nimport { Button } from '~/ui/button'\nimport {\n Empty,\n EmptyContent,\n EmptyDescription,\n EmptyHeader,\n EmptyMedia,\n EmptyTitle,\n} from '~/ui/empty'\nimport { useAppCatalogContext } from '../../context/AppCatalogContext'\nimport { useAppClickHistory } from '../../hooks/useAppClickHistory'\nimport { useAppCounts } from '../../hooks/useAppCounts'\nimport { searchResources } from '../../utils/searchApps'\nimport { OnboardingCard } from '../components/OnboardingCard'\nimport { useAppCatalogFilters } from '../context/AppCatalogFiltersContext'\nimport { FilterBar } from '../filters/FilterBar'\nimport { AppCatalogGrid } from '../grid/AppCatalogGrid'\nimport { LauncherHome } from '../launcher/LauncherHome'\nimport { LauncherDetailPanel } from '../launcher/LauncherDetailPanel'\n\nexport function AppCatalogPage({\n selectedSlug,\n}: { selectedSlug?: string } = {}) {\n const { resources, isLoadingApps, tagsDefinitions } = useAppCatalogContext()\n const { state: filterState, actions } = useAppCatalogFilters()\n const { getTopApps } = useAppClickHistory()\n const navigate = useNavigate()\n\n // Selected app comes from the route path (/app/<slug>); undefined = nothing open\n const selectedAppSlug = selectedSlug\n\n // Search value from context (URL-synced in AppCatalogFiltersContext)\n const search = useSearch({ strict: false })\n const selectedSubSlug = search.sub\n\n const handleSubClick = useCallback(\n (parentSlug: string, subSlug: string) => {\n void navigate({\n to: '/app/$slug',\n params: { slug: parentSlug },\n search: (prev) => ({ ...prev, sub: subSlug }),\n })\n },\n [navigate],\n )\n\n const handleBackToParent = useCallback(() => {\n // Return to parent app detail, clearing the sub-resource selection.\n // selectedAppSlug is the parent slug (set by the /app/$slug route).\n if (selectedAppSlug) {\n void navigate({\n to: '/app/$slug',\n params: { slug: selectedAppSlug },\n search: (prev) => {\n const { sub: _sub, ...rest } = prev as Record<string, string>\n return rest\n },\n })\n }\n }, [navigate, selectedAppSlug])\n\n const searchValue = filterState.searchValue\n const setSearchValue = actions.setSearchValue\n\n // Defer the search value used for filtering to avoid blocking the input\n const deferredSearchValue = useDeferredValue(searchValue)\n\n // State for top apps (loaded async)\n const [topAppSlugs, setTopAppSlugs] = useState<string[]>([])\n\n // Load top apps on mount to calculate recent count\n useEffect(() => {\n void getTopApps(10).then(setTopAppSlugs)\n }, [getTopApps])\n\n // Get root resources for filtering (children handled internally by searchResources)\n const rootResources = useMemo(\n () => resources.filter((r) => !r.parentSlug),\n [resources],\n )\n\n // Dev-only skew warning: data arrived (resources > 0) but nothing is top-level\n // (rootResources === 0). That fingerprints a frontend/backend-core version skew\n // or a stale service worker — NOT a normal empty search/filter (those still have\n // rootResources). Guarded to dev so prod users never see it.\n useEffect(() => {\n if (\n import.meta.env.DEV &&\n !isLoadingApps &&\n resources.length > 0 &&\n rootResources.length === 0\n ) {\n console.warn(\n `[app-catalog] Loaded ${resources.length} resources but 0 are top-level — ` +\n `likely a frontend/backend-core version skew or a stale service worker. ` +\n `Check the version footer (be X / fe Y) and hard-reload.`,\n )\n }\n }, [isLoadingApps, resources.length, rootResources.length])\n\n const { filteredApps, didDeprecatedFallback } = useMemo(() => {\n const childResources = resources.filter((r) => r.parentSlug)\n\n // Apply recent mode / tag filters / search over a base set of root apps.\n const runPipeline = (base: typeof rootResources) => {\n let result = base\n\n // Apply recent mode or tag filters\n if (filterState.recentMode) {\n // Filter to top 10 most clicked apps\n result = result.filter((app) => topAppSlugs.includes(app.slug))\n } else if (Object.keys(filterState.tagFilters).length > 0) {\n // Apply tag filters (AND condition)\n result = result.filter((app) => {\n return Object.entries(filterState.tagFilters).every(\n ([prefix, value]) => {\n const fullTag = `${prefix}:${value}`\n return app.tags?.some(\n (tag) => tag.toLowerCase() === fullTag.toLowerCase(),\n )\n },\n )\n })\n }\n\n // Apply search (using deferred value). Pass all resources so children\n // contribute to parent scoring.\n return searchResources(\n [...result, ...childResources],\n deferredSearchValue,\n )\n }\n\n // Matches among non-deprecated (\"active\") apps only.\n const activeMatches = runPipeline(\n rootResources.filter((app) => !app.deprecated),\n )\n\n // #11: derive the fallback purely from the data (not the toggle), so it\n // stays stable after the toggle auto-enables below — otherwise the notice\n // would flicker off as soon as showDeprecated flips to true.\n // Fallback applies when there's a query, no active matches, but deprecated\n // apps DO match.\n const hasQuery = deferredSearchValue.trim() !== ''\n if (hasQuery && activeMatches.length === 0) {\n const allMatches = runPipeline(rootResources)\n if (allMatches.length > 0) {\n return { filteredApps: allMatches, didDeprecatedFallback: true }\n }\n }\n\n // Normal display: include deprecated only when the toggle is on.\n const result = filterState.showDeprecated\n ? runPipeline(rootResources)\n : activeMatches\n return { filteredApps: result, didDeprecatedFallback: false }\n }, [\n rootResources,\n resources,\n deferredSearchValue,\n filterState.recentMode,\n filterState.tagFilters,\n filterState.showDeprecated,\n topAppSlugs,\n ])\n\n // #11: when the search fell back to deprecated results, enable the\n // \"Show Deprecated Apps\" toggle so the state is visible and consistent.\n // Runs as an effect (not in render) to avoid a side effect during useMemo.\n useEffect(() => {\n if (didDeprecatedFallback && !filterState.showDeprecated) {\n actions.setShowDeprecated(true)\n }\n }, [didDeprecatedFallback, filterState.showDeprecated, actions])\n\n // Calculate counts for FilterBar\n const { allCount, recentCount, deprecatedCount } = useAppCounts({\n apps: rootResources,\n topAppSlugs,\n searchValue: deferredSearchValue,\n })\n\n // Auto-open details when only 1 result — ONLY in the legacy grid path\n // (recent/tag filters active). In the launcher (#38) typing shows the\n // search-morph results list; auto-navigating to /app/<slug> on a single\n // match would yank the user out of the launcher into the old grid+panel\n // (reported bug: searching \"biom\" jumped straight to an app-detail page).\n // The morph already renders the single match as a keyboard-selectable row —\n // the user presses ↵ or clicks to open it.\n const legacyGridActive =\n filterState.recentMode || Object.keys(filterState.tagFilters).length > 0\n useEffect(() => {\n if (\n legacyGridActive &&\n filteredApps.length === 1 &&\n filteredApps[0] &&\n !selectedAppSlug\n ) {\n void navigate({\n to: '/app/$slug',\n params: { slug: filteredApps[0].slug },\n search: (prev) => prev,\n replace: true,\n })\n }\n }, [legacyGridActive, filteredApps, selectedAppSlug, navigate])\n\n // #22: alias → canonical redirect. When an app is renamed its old slug is\n // kept in `aliases[]`. If the URL slug matches no canonical slug but does\n // match some app's alias, redirect (replace) to that app's canonical slug so\n // old bookmarks resolve instead of showing a blank catalog. Guard on a real\n // canonical miss so we never fight the normal detail-open path. Note: this is\n // a client-side SPA redirect (replace), not an HTTP 301 — see ig-umbrella#22.\n useEffect(() => {\n if (!selectedAppSlug || resources.length === 0) return\n const canonical = resources.some((r) => r.slug === selectedAppSlug)\n if (canonical) return\n const aliased = resources.find((r) => r.aliases?.includes(selectedAppSlug))\n if (aliased) {\n void navigate({\n to: '/app/$slug',\n params: { slug: aliased.slug },\n search: (prev) => prev,\n replace: true,\n })\n }\n }, [selectedAppSlug, resources, navigate])\n\n const handleAppClick = (app: Resource) => {\n void navigate({\n to: '/app/$slug',\n params: { slug: app.slug },\n search: (prev) => prev,\n })\n }\n\n const handleLaunch = (app: Resource) => {\n if (app.appUrl) window.open(app.appUrl, '_blank', 'noopener,noreferrer')\n }\n\n // Adaptive-home launcher (#38): shown whenever no structural filter\n // (recentMode / tagFilters / active search) is active. A selected app no longer\n // forces the old grid — instead the launcher stays as the backdrop and the app\n // detail renders in a slide-over panel (#38 item B). The legacy grid+split-pane\n // is used for recent/tag filter views AND for active search (so expandable\n // sub-resource rows and query highlighting in AppCatalogGrid are visible).\n const showLauncherHome =\n !filterState.recentMode &&\n Object.keys(filterState.tagFilters).length === 0 &&\n !deferredSearchValue.trim()\n\n // The app whose detail slide-over is open over the launcher backdrop.\n const launcherSelectedApp = useMemo(\n () =>\n selectedAppSlug\n ? (resources.find((r) => r.slug === selectedAppSlug) ?? null)\n : null,\n [selectedAppSlug, resources],\n )\n\n const handleClearFilters = () => {\n setSearchValue('')\n actions.clearAllFilters()\n void navigate({ to: '/' })\n }\n\n // Calculate total apps count (respecting showDeprecated setting)\n const totalAppsCount = useMemo(() => {\n let count = rootResources.length\n if (!filterState.showDeprecated) {\n count = rootResources.filter((app) => !app.deprecated).length\n }\n return count\n }, [rootResources, filterState.showDeprecated])\n\n if (isLoadingApps) {\n return <div className=\"py-6 text-muted-foreground\">Loading…</div>\n }\n\n // Use first tag definition for grouping\n const groupingDefinition = tagsDefinitions[0]\n\n // Adaptive-home launcher view (#38): the discovery spine or the search-morph\n // results list (handled inside LauncherHome). Owns vertical scroll —\n // MainLayout is h-screen/overflow-hidden, so the launcher must scroll\n // internally to reveal the full Browse-all list as the user scrolls.\n if (showLauncherHome) {\n return (\n <div className=\"flex-1 min-h-0 overflow-y-auto\">\n <LauncherHome\n apps={rootResources.filter(\n (a) => filterState.showDeprecated || !a.deprecated,\n )}\n // All resources incl. children — so search matches sub-resources\n // (e.g. an AWS account) and surfaces their parent, preserving the\n // existing cross-sub-resource search behavior.\n allResources={resources}\n searchValue={searchValue}\n onSearchChange={setSearchValue}\n onAppClick={handleAppClick}\n onLaunch={handleLaunch}\n totalCount={totalAppsCount}\n />\n {/* #38 item B: app detail as a slide-over over the launcher backdrop,\n instead of dropping into the old grid + split-pane. */}\n {launcherSelectedApp && (\n <LauncherDetailPanel\n app={launcherSelectedApp}\n onClose={() => void navigate({ to: '/' })}\n onAppClick={handleAppClick}\n />\n )}\n </div>\n )\n }\n\n return (\n <div className=\"flex flex-col flex-1 min-h-0\">\n <div className=\"shrink-0\">\n <OnboardingCard />\n </div>\n\n <div className=\"shrink-0\">\n <FilterBar\n totalCount={allCount}\n recentCount={recentCount}\n deprecatedCount={deprecatedCount}\n apps={rootResources}\n />\n </div>\n\n {didDeprecatedFallback && (\n <div\n role=\"status\"\n className=\"shrink-0 px-1 pb-2 text-sm text-muted-foreground\"\n >\n {`No active apps${\n searchValue ? ` for \"${searchValue}\"` : ''\n } — showing deprecated matches.`}\n </div>\n )}\n\n <div className=\"flex-1 min-h-0\">\n {filteredApps.length === 0 && !selectedAppSlug ? (\n <Empty>\n <EmptyHeader>\n <EmptyMedia variant=\"icon\">\n <X className=\"h-6 w-6\" />\n </EmptyMedia>\n <EmptyTitle>\n No apps found{searchValue && ` for \"${searchValue}\"`}\n </EmptyTitle>\n <EmptyDescription>\n Try adjusting your search or filters\n </EmptyDescription>\n </EmptyHeader>\n <EmptyContent>\n {searchValue && (\n <Button\n variant=\"outline\"\n onClick={() => {\n setSearchValue('')\n void navigate({ to: '/' })\n }}\n className=\"gap-2\"\n >\n <X className=\"h-4 w-4\" />\n Clear search\n </Button>\n )}\n </EmptyContent>\n </Empty>\n ) : (\n <AppCatalogGrid\n apps={filteredApps}\n selectedAppSlug={selectedAppSlug}\n selectedSubSlug={selectedSubSlug}\n groupingDefinition={groupingDefinition}\n onAppClick={handleAppClick}\n onClosePanel={() => void navigate({ to: '/' })}\n onSubClick={handleSubClick}\n onBackToParent={handleBackToParent}\n hasSearch={!!deferredSearchValue}\n searchQuery={searchValue}\n totalAppsCount={totalAppsCount}\n onClearFilters={handleClearFilters}\n />\n )}\n </div>\n </div>\n )\n}\n"],"names":["result"],"mappings":"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"}
|
|
@@ -39,26 +39,47 @@ function ServiceDesksPage() {
|
|
|
39
39
|
/* @__PURE__ */ jsx(TableBody, { children: desks.map((desk) => {
|
|
40
40
|
const url = desk.config.url;
|
|
41
41
|
const description = desk.config.description;
|
|
42
|
-
return /* @__PURE__ */ jsxs(
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
42
|
+
return /* @__PURE__ */ jsxs(
|
|
43
|
+
TableRow,
|
|
44
|
+
{
|
|
45
|
+
className: url ? "cursor-pointer hover:bg-muted/50" : void 0,
|
|
46
|
+
onClick: url ? () => window.open(url, "_blank", "noopener,noreferrer") : void 0,
|
|
47
|
+
children: [
|
|
48
|
+
/* @__PURE__ */ jsxs(TableCell, { className: "font-medium align-top", children: [
|
|
49
|
+
url ? /* @__PURE__ */ jsx(
|
|
50
|
+
"a",
|
|
51
|
+
{
|
|
52
|
+
href: url,
|
|
53
|
+
target: "_blank",
|
|
54
|
+
rel: "noopener noreferrer",
|
|
55
|
+
className: "hover:text-primary hover:underline",
|
|
56
|
+
"data-testid": "service-desk-name",
|
|
57
|
+
onClick: (e) => e.stopPropagation(),
|
|
58
|
+
children: desk.displayName
|
|
59
|
+
}
|
|
60
|
+
) : /* @__PURE__ */ jsx("span", { "data-testid": "service-desk-name", children: desk.displayName }),
|
|
61
|
+
description && /* @__PURE__ */ jsx("span", { className: "block text-xs font-normal text-muted-foreground", children: description }),
|
|
62
|
+
url && /* @__PURE__ */ jsxs(
|
|
63
|
+
"a",
|
|
64
|
+
{
|
|
65
|
+
href: url,
|
|
66
|
+
target: "_blank",
|
|
67
|
+
rel: "noopener noreferrer",
|
|
68
|
+
className: "mt-0.5 inline-flex items-center gap-1 text-xs text-primary/70 hover:text-primary hover:underline",
|
|
69
|
+
title: url,
|
|
70
|
+
onClick: (e) => e.stopPropagation(),
|
|
71
|
+
children: [
|
|
72
|
+
url.replace(/^https?:\/\//, ""),
|
|
73
|
+
/* @__PURE__ */ jsx(ExternalLink, { className: "size-3 shrink-0" })
|
|
74
|
+
]
|
|
75
|
+
}
|
|
76
|
+
)
|
|
77
|
+
] }),
|
|
78
|
+
/* @__PURE__ */ jsx(TableCell, { className: "text-muted-foreground text-xs", children: url ? /* @__PURE__ */ jsx(ExternalLink, { className: "size-4 text-primary/50" }) : /* @__PURE__ */ jsx("span", { children: "—" }) })
|
|
79
|
+
]
|
|
80
|
+
},
|
|
81
|
+
desk.slug
|
|
82
|
+
);
|
|
62
83
|
}) })
|
|
63
84
|
] }) })
|
|
64
85
|
] });
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ServiceDesksPage.js","sources":["../../../../../../src/modules/appCatalog/ui/pages/ServiceDesksPage.tsx"],"sourcesContent":["import { ExternalLink } from 'lucide-react'\nimport { useMemo, useState } from 'react'\nimport { InputGroup, InputGroupInput } from '~/ui/input-group'\nimport {\n Table,\n TableBody,\n TableCell,\n TableHead,\n TableHeader,\n TableRow,\n} from '~/ui/table'\nimport { useAppCatalogContext } from '../../context/AppCatalogContext'\n\n/**\n * Service Desks view (#9): a searchable table of all service-desk approval\n * methods (type === 'service'), each with a link that opens its portal in a new\n * tab. Data rides in on the existing app-catalog query (context.approvalMethods).\n */\nexport function ServiceDesksPage() {\n const { approvalMethods } = useAppCatalogContext()\n const [search, setSearch] = useState('')\n\n const desks = useMemo(() => {\n const services = approvalMethods.filter((m) => m.type === 'service')\n const q = search.trim().toLowerCase()\n const filtered = q\n ? services.filter((m) => m.displayName.toLowerCase().includes(q))\n : services\n return [...filtered].sort((a, b) =>\n a.displayName.localeCompare(b.displayName),\n )\n }, [approvalMethods, search])\n\n return (\n <div className=\"flex flex-col flex-1 min-h-0 gap-4\">\n <InputGroup className=\"max-w-sm\">\n <InputGroupInput\n autoFocus\n value={search}\n onChange={(e) => setSearch(e.target.value)}\n onFocus={(e) => e.target.select()}\n placeholder=\"Search service desks by name…\"\n aria-label=\"Search service desks\"\n />\n </InputGroup>\n\n <div className=\"flex-1 min-h-0 overflow-y-auto\">\n {desks.length === 0 ? (\n <p className=\"text-sm text-muted-foreground\">\n No service desks found{search && ` for \"${search}\"`}.\n </p>\n ) : (\n <Table>\n <TableHeader>\n <TableRow>\n <TableHead>Service Desk</TableHead>\n <TableHead>Link</TableHead>\n </TableRow>\n </TableHeader>\n <TableBody>\n {desks.map((desk) => {\n const url = desk.config.url\n const description = desk.config.description\n return (\n <TableRow
|
|
1
|
+
{"version":3,"file":"ServiceDesksPage.js","sources":["../../../../../../src/modules/appCatalog/ui/pages/ServiceDesksPage.tsx"],"sourcesContent":["import { ExternalLink } from 'lucide-react'\nimport { useMemo, useState } from 'react'\nimport { InputGroup, InputGroupInput } from '~/ui/input-group'\nimport {\n Table,\n TableBody,\n TableCell,\n TableHead,\n TableHeader,\n TableRow,\n} from '~/ui/table'\nimport { useAppCatalogContext } from '../../context/AppCatalogContext'\n\n/**\n * Service Desks view (#9): a searchable table of all service-desk approval\n * methods (type === 'service'), each with a link that opens its portal in a new\n * tab. Data rides in on the existing app-catalog query (context.approvalMethods).\n */\nexport function ServiceDesksPage() {\n const { approvalMethods } = useAppCatalogContext()\n const [search, setSearch] = useState('')\n\n const desks = useMemo(() => {\n const services = approvalMethods.filter((m) => m.type === 'service')\n const q = search.trim().toLowerCase()\n const filtered = q\n ? services.filter((m) => m.displayName.toLowerCase().includes(q))\n : services\n return [...filtered].sort((a, b) =>\n a.displayName.localeCompare(b.displayName),\n )\n }, [approvalMethods, search])\n\n return (\n <div className=\"flex flex-col flex-1 min-h-0 gap-4\">\n <InputGroup className=\"max-w-sm\">\n <InputGroupInput\n autoFocus\n value={search}\n onChange={(e) => setSearch(e.target.value)}\n onFocus={(e) => e.target.select()}\n placeholder=\"Search service desks by name…\"\n aria-label=\"Search service desks\"\n />\n </InputGroup>\n\n <div className=\"flex-1 min-h-0 overflow-y-auto\">\n {desks.length === 0 ? (\n <p className=\"text-sm text-muted-foreground\">\n No service desks found{search && ` for \"${search}\"`}.\n </p>\n ) : (\n <Table>\n <TableHeader>\n <TableRow>\n <TableHead>Service Desk</TableHead>\n <TableHead>Link</TableHead>\n </TableRow>\n </TableHeader>\n <TableBody>\n {desks.map((desk) => {\n const url = desk.config.url\n const description = desk.config.description\n return (\n <TableRow\n key={desk.slug}\n className={\n url ? 'cursor-pointer hover:bg-muted/50' : undefined\n }\n onClick={\n url\n ? () =>\n window.open(url, '_blank', 'noopener,noreferrer')\n : undefined\n }\n >\n <TableCell className=\"font-medium align-top\">\n {url ? (\n <a\n href={url}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"hover:text-primary hover:underline\"\n data-testid=\"service-desk-name\"\n onClick={(e) => e.stopPropagation()}\n >\n {desk.displayName}\n </a>\n ) : (\n <span data-testid=\"service-desk-name\">\n {desk.displayName}\n </span>\n )}\n {description && (\n <span className=\"block text-xs font-normal text-muted-foreground\">\n {description}\n </span>\n )}\n {url && (\n <a\n href={url}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"mt-0.5 inline-flex items-center gap-1 text-xs text-primary/70 hover:text-primary hover:underline\"\n title={url}\n onClick={(e) => e.stopPropagation()}\n >\n {url.replace(/^https?:\\/\\//, '')}\n <ExternalLink className=\"size-3 shrink-0\" />\n </a>\n )}\n </TableCell>\n <TableCell className=\"text-muted-foreground text-xs\">\n {url ? (\n <ExternalLink className=\"size-4 text-primary/50\" />\n ) : (\n <span>—</span>\n )}\n </TableCell>\n </TableRow>\n )\n })}\n </TableBody>\n </Table>\n )}\n </div>\n </div>\n )\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -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
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"markdownToPlainText.js","sources":["../../../../../src/modules/appCatalog/utils/markdownToPlainText.ts"],"sourcesContent":["/**\n * Reduce a short markdown string to its visible plain text.\n *\n * Descriptions may contain markdown — notably cross-reference links like\n * `[
|
|
1
|
+
{"version":3,"file":"markdownToPlainText.js","sources":["../../../../../src/modules/appCatalog/utils/markdownToPlainText.ts"],"sourcesContent":["/**\n * Reduce a short markdown string to its visible plain text.\n *\n * Descriptions may contain markdown — notably cross-reference links like\n * `[Team Chat](/app/team-chat)` (#25). The detail panel renders\n * those as interactive links via {@link MarkdownText}, but in compact / clamped\n * surfaces (grid list preview, table row, filter combobox, sub-resources) an\n * interactive link would break `line-clamp` and can't be reliably highlighted\n * for search. There we show the *text* only — so `[Name](/app/slug)` shows\n * \"Name\", never the raw bracket/paren syntax.\n *\n * This is deliberately a small, allocation-cheap regex pass (not a full\n * markdown parser): it covers the inline constructs that actually appear in\n * catalog descriptions — links, images, emphasis, and inline code.\n */\nexport function markdownToPlainText(input: string): string {\n if (!input) return ''\n\n return (\n input\n // Images:  → alt (must run before links)\n .replace(/!\\[([^\\]]*)\\]\\([^)]*\\)/g, '$1')\n // Links: [text](url) → text\n .replace(/\\[([^\\]]*)\\]\\([^)]*\\)/g, '$1')\n // Bold / italic markers: **x**, __x__, *x*, _x_ → x\n .replace(/(\\*\\*|__)(.*?)\\1/g, '$2')\n .replace(/(\\*|_)(.*?)\\1/g, '$2')\n // Inline code: `x` → x\n .replace(/`([^`]*)`/g, '$1')\n )\n}\n"],"names":[],"mappings":"AAeO,SAAS,oBAAoB,OAAuB;AACzD,MAAI,CAAC,MAAO,QAAO;AAEnB,SACE,MAEG,QAAQ,2BAA2B,IAAI,EAEvC,QAAQ,0BAA0B,IAAI,EAEtC,QAAQ,qBAAqB,IAAI,EACjC,QAAQ,kBAAkB,IAAI,EAE9B,QAAQ,cAAc,IAAI;AAEjC;"}
|
|
@@ -29,12 +29,14 @@ declare const LayoutAppSlugRoute: import('@tanstack/router-core').Route<import('
|
|
|
29
29
|
filters: import('zod').ZodOptional<import('zod').ZodString>;
|
|
30
30
|
deprecated: import('zod').ZodOptional<import('zod').ZodString>;
|
|
31
31
|
q: import('zod').ZodOptional<import('zod').ZodString>;
|
|
32
|
+
sub: import('zod').ZodOptional<import('zod').ZodString>;
|
|
32
33
|
}, import('zod/v4/core').$strip>, import('@tanstack/router-core').ResolveParams<"/app/$slug">, import('@tanstack/router-core').AnyContext, import('@tanstack/router-core').AnyContext, import('@tanstack/router-core').AnyContext, {}, () => Promise<{
|
|
33
34
|
appCatalogLoader: import('./modules/appCatalog/routeLoader.js').AppCatalogLoaderReturn;
|
|
34
35
|
}>, unknown, unknown, unknown, unknown, undefined>;
|
|
35
36
|
declare const LayoutCatalogAppsIndexRoute: import('@tanstack/router-core').Route<import('@tanstack/react-router').Register, import('@tanstack/router-core').Route<import('@tanstack/react-router').Register, import('@tanstack/react-router').RootRoute<import('@tanstack/react-router').Register, undefined, import('./types/types.js').AcRouterContext, import('@tanstack/router-core').AnyContext, import('@tanstack/router-core').AnyContext, {}, undefined, unknown, unknown, unknown, unknown, undefined>, "", "/", "/_layout", "/_layout", undefined, import('@tanstack/router-core').ResolveParams<"">, import('@tanstack/router-core').AnyContext, import('@tanstack/router-core').AnyContext, import('@tanstack/router-core').AnyContext, {}, undefined, unknown, unknown, unknown, unknown, undefined>, "/catalog/apps", "/catalog/apps/", "/_layout/catalog/apps/", "/_layout/catalog/apps/", import('zod').ZodObject<{
|
|
36
37
|
app: import('zod').ZodOptional<import('zod').ZodString>;
|
|
37
38
|
q: import('zod').ZodOptional<import('zod').ZodString>;
|
|
39
|
+
sub: import('zod').ZodOptional<import('zod').ZodString>;
|
|
38
40
|
}, import('zod/v4/core').$strip>, import('@tanstack/router-core').ResolveParams<"/catalog/apps">, import('@tanstack/router-core').AnyContext, import('@tanstack/router-core').AnyContext, import('@tanstack/router-core').AnyContext, {}, (ctx: import('@tanstack/router-core').LoaderFnContext<import('@tanstack/react-router').Register, import('@tanstack/router-core').Route<import('@tanstack/react-router').Register, import('@tanstack/react-router').RootRoute<import('@tanstack/react-router').Register, undefined, import('./types/types.js').AcRouterContext, import('@tanstack/router-core').AnyContext, import('@tanstack/router-core').AnyContext, {}, undefined, unknown, unknown, unknown, unknown, undefined>, "", "/", "/_layout", "/_layout", undefined, import('@tanstack/router-core').ResolveParams<"">, import('@tanstack/router-core').AnyContext, import('@tanstack/router-core').AnyContext, import('@tanstack/router-core').AnyContext, {}, undefined, unknown, unknown, unknown, unknown, undefined>, "/_layout/catalog/apps/", import('@tanstack/router-core').ResolveParams<"/catalog/apps">, {}, import('@tanstack/router-core').AnyContext, import('@tanstack/router-core').AnyContext, import('@tanstack/router-core').AnyContext, unknown, undefined>) => Promise<void>, unknown, unknown, unknown, unknown, undefined>;
|
|
39
41
|
export interface FileRoutesByFullPath {
|
|
40
42
|
'/': typeof LayoutIndexRoute;
|
|
@@ -5,6 +5,7 @@ export declare const Route: import('@tanstack/router-core').Route<import('@tanst
|
|
|
5
5
|
filters: z.ZodOptional<z.ZodString>;
|
|
6
6
|
deprecated: z.ZodOptional<z.ZodString>;
|
|
7
7
|
q: z.ZodOptional<z.ZodString>;
|
|
8
|
+
sub: z.ZodOptional<z.ZodString>;
|
|
8
9
|
}, z.core.$strip>, import('@tanstack/router-core').ResolveParams<"/app/$slug">, import('@tanstack/router-core').AnyContext, import('@tanstack/router-core').AnyContext, import('@tanstack/router-core').AnyContext, {}, () => Promise<{
|
|
9
10
|
appCatalogLoader: import('~/modules/appCatalog/routeLoader').AppCatalogLoaderReturn;
|
|
10
11
|
}>, unknown, unknown, unknown, unknown, undefined>;
|
|
@@ -9,7 +9,8 @@ const searchSchema = object({
|
|
|
9
9
|
recent: string().optional(),
|
|
10
10
|
filters: string().optional(),
|
|
11
11
|
deprecated: string().optional(),
|
|
12
|
-
q: string().optional()
|
|
12
|
+
q: string().optional(),
|
|
13
|
+
sub: string().optional()
|
|
13
14
|
});
|
|
14
15
|
const Route = createFileRoute("/_layout/app/$slug")({
|
|
15
16
|
component: RouteComponent,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"app._slug.js","sources":["../../../../src/routes/_layout/app.$slug.tsx"],"sourcesContent":["import { createFileRoute, stripSearchParams } from '@tanstack/react-router'\nimport { z } from 'zod'\nimport { appCatalogRouteLoader } from '~/modules/appCatalog/routeLoader'\nimport { AppCatalogLayout } from '~/modules/appCatalog/ui/layout/AppCatalogLayout'\nimport { AppCatalogPage } from '~/modules/appCatalog/ui/pages/AppCatalogPage'\n\n// Preserve the URL-synced filter params (see AppCatalogFiltersContext) on this\n// route so they aren't stripped on navigation. `q` (the search text) is the\n// exception: as of #27 it lives in sessionStorage, not the URL. It stays\n// declared here so an incoming legacy `?q=` still validates, but the\n// `stripSearchParams` middleware removes it so shared app links stay clean.\nconst searchSchema = z.object({\n filterTag: z.string().optional(),\n recent: z.string().optional(),\n filters: z.string().optional(),\n deprecated: z.string().optional(),\n q: z.string().optional(),\n})\n\nexport const Route = createFileRoute('/_layout/app/$slug')({\n component: RouteComponent,\n validateSearch: searchSchema,\n search: { middlewares: [stripSearchParams(['q'])] },\n async loader() {\n const appCatalogLoader = await appCatalogRouteLoader()\n return { appCatalogLoader }\n },\n})\n\nfunction RouteComponent() {\n const { queryClient, trpcClient } = Route.useRouteContext()\n const { slug } = Route.useParams()\n\n return (\n <AppCatalogLayout queryClient={queryClient} trpcClient={trpcClient}>\n <AppCatalogPage selectedSlug={slug} />\n </AppCatalogLayout>\n )\n}\n"],"names":["z.object","z.string"],"mappings":";;;;;;AAWA,MAAM,eAAeA,OAAS;AAAA,EAC5B,WAAWC,OAAE,EAAS,SAAA;AAAA,EACtB,QAAQA,OAAE,EAAS,SAAA;AAAA,EACnB,SAASA,OAAE,EAAS,SAAA;AAAA,EACpB,YAAYA,OAAE,EAAS,SAAA;AAAA,EACvB,GAAGA,OAAE,EAAS,SAAA;
|
|
1
|
+
{"version":3,"file":"app._slug.js","sources":["../../../../src/routes/_layout/app.$slug.tsx"],"sourcesContent":["import { createFileRoute, stripSearchParams } from '@tanstack/react-router'\nimport { z } from 'zod'\nimport { appCatalogRouteLoader } from '~/modules/appCatalog/routeLoader'\nimport { AppCatalogLayout } from '~/modules/appCatalog/ui/layout/AppCatalogLayout'\nimport { AppCatalogPage } from '~/modules/appCatalog/ui/pages/AppCatalogPage'\n\n// Preserve the URL-synced filter params (see AppCatalogFiltersContext) on this\n// route so they aren't stripped on navigation. `q` (the search text) is the\n// exception: as of #27 it lives in sessionStorage, not the URL. It stays\n// declared here so an incoming legacy `?q=` still validates, but the\n// `stripSearchParams` middleware removes it so shared app links stay clean.\nconst searchSchema = z.object({\n filterTag: z.string().optional(),\n recent: z.string().optional(),\n filters: z.string().optional(),\n deprecated: z.string().optional(),\n q: z.string().optional(),\n sub: z.string().optional(),\n})\n\nexport const Route = createFileRoute('/_layout/app/$slug')({\n component: RouteComponent,\n validateSearch: searchSchema,\n search: { middlewares: [stripSearchParams(['q'])] },\n async loader() {\n const appCatalogLoader = await appCatalogRouteLoader()\n return { appCatalogLoader }\n },\n})\n\nfunction RouteComponent() {\n const { queryClient, trpcClient } = Route.useRouteContext()\n const { slug } = Route.useParams()\n\n return (\n <AppCatalogLayout queryClient={queryClient} trpcClient={trpcClient}>\n <AppCatalogPage selectedSlug={slug} />\n </AppCatalogLayout>\n )\n}\n"],"names":["z.object","z.string"],"mappings":";;;;;;AAWA,MAAM,eAAeA,OAAS;AAAA,EAC5B,WAAWC,OAAE,EAAS,SAAA;AAAA,EACtB,QAAQA,OAAE,EAAS,SAAA;AAAA,EACnB,SAASA,OAAE,EAAS,SAAA;AAAA,EACpB,YAAYA,OAAE,EAAS,SAAA;AAAA,EACvB,GAAGA,OAAE,EAAS,SAAA;AAAA,EACd,KAAKA,OAAE,EAAS,SAAA;AAClB,CAAC;AAEM,MAAM,QAAQ,gBAAgB,oBAAoB,EAAE;AAAA,EACzD,WAAW;AAAA,EACX,gBAAgB;AAAA,EAChB,QAAQ,EAAE,aAAa,CAAC,kBAAkB,CAAC,GAAG,CAAC,CAAC,EAAA;AAAA,EAChD,MAAM,SAAS;AACb,UAAM,mBAAmB,MAAM,sBAAA;AAC/B,WAAO,EAAE,iBAAA;AAAA,EACX;AACF,CAAC;AAED,SAAS,iBAAiB;AACxB,QAAM,EAAE,aAAa,eAAe,MAAM,gBAAA;AAC1C,QAAM,EAAE,KAAA,IAAS,MAAM,UAAA;AAEvB,SACE,oBAAC,oBAAiB,aAA0B,YAC1C,8BAAC,gBAAA,EAAe,cAAc,MAAM,EAAA,CACtC;AAEJ;"}
|
|
@@ -2,4 +2,5 @@ import { z } from 'zod';
|
|
|
2
2
|
export declare const Route: import('@tanstack/router-core').Route<import('@tanstack/react-router').Register, import('@tanstack/router-core').Route<import('@tanstack/react-router').Register, import('@tanstack/react-router').RootRoute<import('@tanstack/react-router').Register, undefined, import('../../types/types.js').AcRouterContext, import('@tanstack/router-core').AnyContext, import('@tanstack/router-core').AnyContext, {}, undefined, unknown, unknown, unknown, unknown, undefined>, "", "/", "/_layout", "/_layout", undefined, import('@tanstack/router-core').ResolveParams<"">, import('@tanstack/router-core').AnyContext, import('@tanstack/router-core').AnyContext, import('@tanstack/router-core').AnyContext, {}, undefined, unknown, unknown, unknown, unknown, undefined>, "/catalog/apps", "/catalog/apps/", "/_layout/catalog/apps/", "/_layout/catalog/apps/", z.ZodObject<{
|
|
3
3
|
app: z.ZodOptional<z.ZodString>;
|
|
4
4
|
q: z.ZodOptional<z.ZodString>;
|
|
5
|
+
sub: z.ZodOptional<z.ZodString>;
|
|
5
6
|
}, z.core.$strip>, import('@tanstack/router-core').ResolveParams<"/catalog/apps">, import('@tanstack/router-core').AnyContext, import('@tanstack/router-core').AnyContext, import('@tanstack/router-core').AnyContext, {}, (ctx: import('@tanstack/router-core').LoaderFnContext<import('@tanstack/react-router').Register, import('@tanstack/router-core').Route<import('@tanstack/react-router').Register, import('@tanstack/react-router').RootRoute<import('@tanstack/react-router').Register, undefined, import('../../types/types.js').AcRouterContext, import('@tanstack/router-core').AnyContext, import('@tanstack/router-core').AnyContext, {}, undefined, unknown, unknown, unknown, unknown, undefined>, "", "/", "/_layout", "/_layout", undefined, import('@tanstack/router-core').ResolveParams<"">, import('@tanstack/router-core').AnyContext, import('@tanstack/router-core').AnyContext, import('@tanstack/router-core').AnyContext, {}, undefined, unknown, unknown, unknown, unknown, undefined>, "/_layout/catalog/apps/", import('@tanstack/router-core').ResolveParams<"/catalog/apps">, {}, import('@tanstack/router-core').AnyContext, import('@tanstack/router-core').AnyContext, import('@tanstack/router-core').AnyContext, unknown, undefined>) => Promise<void>, unknown, unknown, unknown, unknown, undefined>;
|
|
@@ -6,7 +6,8 @@ import { AppCatalogPage } from "../../modules/appCatalog/ui/pages/AppCatalogPage
|
|
|
6
6
|
import { object, string } from "../../node_modules/.pnpm/zod@4.3.5/node_modules/zod/v4/classic/schemas.js";
|
|
7
7
|
const searchSchema = object({
|
|
8
8
|
app: string().optional(),
|
|
9
|
-
q: string().optional()
|
|
9
|
+
q: string().optional(),
|
|
10
|
+
sub: string().optional()
|
|
10
11
|
});
|
|
11
12
|
const Route = createFileRoute("/_layout/catalog/apps/")({
|
|
12
13
|
component: RouteComponent,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"catalog.apps.index.js","sources":["../../../../src/routes/_layout/catalog.apps.index.tsx"],"sourcesContent":["import { createFileRoute, stripSearchParams } from '@tanstack/react-router'\nimport { z } from 'zod'\nimport { catalogRouteLoader } from '~/modules/appCatalog/catalogRouteLoader'\nimport { AppCatalogLayout } from '~/modules/appCatalog/ui/layout/AppCatalogLayout'\nimport { AppCatalogPage } from '~/modules/appCatalog/ui/pages/AppCatalogPage'\n\n// #27: `q` kept declared for legacy links but stripped from the URL (search\n// text lives in sessionStorage now).\nconst searchSchema = z.object({\n app: z.string().optional(),\n q: z.string().optional(),\n})\n\nexport const Route = createFileRoute('/_layout/catalog/apps/')({\n component: RouteComponent,\n validateSearch: searchSchema,\n search: { middlewares: [stripSearchParams(['q'])] },\n async loader(ctx) {\n await catalogRouteLoader(ctx)\n },\n})\n\nfunction RouteComponent() {\n const { queryClient, trpcClient } = Route.useRouteContext()\n\n return (\n <AppCatalogLayout queryClient={queryClient} trpcClient={trpcClient}>\n <AppCatalogPage />\n </AppCatalogLayout>\n )\n}\n"],"names":["z.object","z.string"],"mappings":";;;;;;AAQA,MAAM,eAAeA,OAAS;AAAA,EAC5B,KAAKC,OAAE,EAAS,SAAA;AAAA,EAChB,GAAGA,OAAE,EAAS,SAAA;
|
|
1
|
+
{"version":3,"file":"catalog.apps.index.js","sources":["../../../../src/routes/_layout/catalog.apps.index.tsx"],"sourcesContent":["import { createFileRoute, stripSearchParams } from '@tanstack/react-router'\nimport { z } from 'zod'\nimport { catalogRouteLoader } from '~/modules/appCatalog/catalogRouteLoader'\nimport { AppCatalogLayout } from '~/modules/appCatalog/ui/layout/AppCatalogLayout'\nimport { AppCatalogPage } from '~/modules/appCatalog/ui/pages/AppCatalogPage'\n\n// #27: `q` kept declared for legacy links but stripped from the URL (search\n// text lives in sessionStorage now).\nconst searchSchema = z.object({\n app: z.string().optional(),\n q: z.string().optional(),\n sub: z.string().optional(),\n})\n\nexport const Route = createFileRoute('/_layout/catalog/apps/')({\n component: RouteComponent,\n validateSearch: searchSchema,\n search: { middlewares: [stripSearchParams(['q'])] },\n async loader(ctx) {\n await catalogRouteLoader(ctx)\n },\n})\n\nfunction RouteComponent() {\n const { queryClient, trpcClient } = Route.useRouteContext()\n\n return (\n <AppCatalogLayout queryClient={queryClient} trpcClient={trpcClient}>\n <AppCatalogPage />\n </AppCatalogLayout>\n )\n}\n"],"names":["z.object","z.string"],"mappings":";;;;;;AAQA,MAAM,eAAeA,OAAS;AAAA,EAC5B,KAAKC,OAAE,EAAS,SAAA;AAAA,EAChB,GAAGA,OAAE,EAAS,SAAA;AAAA,EACd,KAAKA,OAAE,EAAS,SAAA;AAClB,CAAC;AAEM,MAAM,QAAQ,gBAAgB,wBAAwB,EAAE;AAAA,EAC7D,WAAW;AAAA,EACX,gBAAgB;AAAA,EAChB,QAAQ,EAAE,aAAa,CAAC,kBAAkB,CAAC,GAAG,CAAC,CAAC,EAAA;AAAA,EAChD,MAAM,OAAO,KAAK;AAChB,UAAM,mBAAmB,GAAG;AAAA,EAC9B;AACF,CAAC;AAED,SAAS,iBAAiB;AACxB,QAAM,EAAE,aAAa,eAAe,MAAM,gBAAA;AAE1C,6BACG,kBAAA,EAAiB,aAA0B,YAC1C,UAAA,oBAAC,kBAAe,GAClB;AAEJ;"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@igstack/app-catalog-frontend-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.17.1-alpha-20260821043201",
|
|
4
4
|
"description": "Frontend core library for App Catalog",
|
|
5
5
|
"homepage": "https://github.com/lislon/app-catalog",
|
|
6
6
|
"repository": {
|
|
@@ -39,7 +39,6 @@
|
|
|
39
39
|
"public"
|
|
40
40
|
],
|
|
41
41
|
"dependencies": {
|
|
42
|
-
"@ai-sdk/react": "^3.0.39",
|
|
43
42
|
"@base-ui/react": "^1.0.0",
|
|
44
43
|
"@radix-ui/react-alert-dialog": "^1.1.14",
|
|
45
44
|
"@radix-ui/react-checkbox": "^1.3.3",
|
|
@@ -70,10 +69,9 @@
|
|
|
70
69
|
"@trpc/client": "^11.8.1",
|
|
71
70
|
"@trpc/server": "^11.4.2",
|
|
72
71
|
"@trpc/tanstack-react-query": "^11.8.1",
|
|
73
|
-
"ai": "^6.0.37",
|
|
74
72
|
"autoprefixer": "^10.4.21",
|
|
75
73
|
"autosuggest-highlight": "^3.3.4",
|
|
76
|
-
"better-auth": "
|
|
74
|
+
"better-auth": ">=1.4.18 <1.7.0",
|
|
77
75
|
"class-variance-authority": "^0.7.1",
|
|
78
76
|
"classnames": "^2.5.1",
|
|
79
77
|
"clsx": "^2.1.1",
|
|
@@ -134,8 +132,8 @@
|
|
|
134
132
|
"vite-plugin-static-copy": "^3.1.4",
|
|
135
133
|
"vite-plugin-svgr": "^4.2.0",
|
|
136
134
|
"vitest": "^4.1.2",
|
|
137
|
-
"@igstack/app-catalog-backend-core": "0.
|
|
138
|
-
"@igstack/app-catalog-shared-core": "0.
|
|
135
|
+
"@igstack/app-catalog-backend-core": "0.17.1-alpha-20260821043201",
|
|
136
|
+
"@igstack/app-catalog-shared-core": "0.17.1-alpha-20260821043201"
|
|
139
137
|
},
|
|
140
138
|
"peerDependencies": {
|
|
141
139
|
"react": "19.1.2",
|
|
@@ -144,7 +142,7 @@
|
|
|
144
142
|
"vite": "^6.3.5",
|
|
145
143
|
"vite-plugin-svgr": "^4.2.0"
|
|
146
144
|
},
|
|
147
|
-
"gitHead": "
|
|
145
|
+
"gitHead": "304d9c364de826d155069ac01370baffb0620c78",
|
|
148
146
|
"scripts": {
|
|
149
147
|
"build": "vite build",
|
|
150
148
|
"build:lenient": "vite build --mode lenient",
|