@igstack/app-catalog-frontend-core 0.5.0 → 0.6.0

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 (33) hide show
  1. package/dist/esm/__tests__/integration/appDeepLink.integration.test.d.ts +0 -0
  2. package/dist/esm/__tests__/integration/harness/given.d.ts +5 -1
  3. package/dist/esm/modules/appCatalog/context/AppCatalogContext.js +1 -0
  4. package/dist/esm/modules/appCatalog/context/AppCatalogContext.js.map +1 -1
  5. package/dist/esm/modules/appCatalog/ui/components/PersonBadge.js +1 -0
  6. package/dist/esm/modules/appCatalog/ui/components/PersonBadge.js.map +1 -1
  7. package/dist/esm/modules/appCatalog/ui/components/SubResourcesSection.js +1 -0
  8. package/dist/esm/modules/appCatalog/ui/components/SubResourcesSection.js.map +1 -1
  9. package/dist/esm/modules/appCatalog/ui/components/TierVariantsSection.js +1 -0
  10. package/dist/esm/modules/appCatalog/ui/components/TierVariantsSection.js.map +1 -1
  11. package/dist/esm/modules/appCatalog/ui/grid/AppCatalogGrid.d.ts +3 -1
  12. package/dist/esm/modules/appCatalog/ui/grid/AppCatalogGrid.js +3 -1
  13. package/dist/esm/modules/appCatalog/ui/grid/AppCatalogGrid.js.map +1 -1
  14. package/dist/esm/modules/appCatalog/ui/pages/AppCatalogPage.d.ts +3 -1
  15. package/dist/esm/modules/appCatalog/ui/pages/AppCatalogPage.js +17 -12
  16. package/dist/esm/modules/appCatalog/ui/pages/AppCatalogPage.js.map +1 -1
  17. package/dist/esm/routeTree.gen.d.ts +18 -4
  18. package/dist/esm/routeTree.gen.js +11 -4
  19. package/dist/esm/routeTree.gen.js.map +1 -1
  20. package/dist/esm/routes/_layout/app.$slug.d.ts +3 -0
  21. package/dist/esm/routes/_layout/app._slug.js +21 -0
  22. package/dist/esm/routes/_layout/app._slug.js.map +1 -0
  23. package/dist/esm/routes/_layout/index.d.ts +0 -1
  24. package/dist/esm/routes/_layout/index.js +0 -1
  25. package/dist/esm/routes/_layout/index.js.map +1 -1
  26. package/package.json +3 -3
  27. package/src/__tests__/integration/appDeepLink.integration.test.ts +55 -0
  28. package/src/__tests__/integration/harness/given.tsx +7 -3
  29. package/src/modules/appCatalog/ui/grid/AppCatalogGrid.tsx +4 -0
  30. package/src/modules/appCatalog/ui/pages/AppCatalogPage.tsx +18 -15
  31. package/src/routeTree.gen.ts +21 -2
  32. package/src/routes/_layout/app.$slug.tsx +23 -0
  33. package/src/routes/_layout/index.tsx +0 -1
@@ -1,4 +1,5 @@
1
1
  import { jsx, jsxs } from "react/jsx-runtime";
2
+ import { useNavigate } from "@tanstack/react-router";
2
3
  import { X } from "lucide-react";
3
4
  import { useDeferredValue, useState, useEffect, useMemo } from "react";
4
5
  import { Button } from "../../../../ui/button.js";
@@ -6,20 +7,19 @@ import { Empty, EmptyHeader, EmptyMedia, EmptyTitle, EmptyDescription, EmptyCont
6
7
  import { useAppCatalogContext } from "../../context/AppCatalogContext.js";
7
8
  import { useAppClickHistory } from "../../hooks/useAppClickHistory.js";
8
9
  import { useAppCounts } from "../../hooks/useAppCounts.js";
9
- import { useUrlSyncedState } from "../../hooks/useUrlSyncedState.js";
10
10
  import { searchResources } from "../../utils/searchApps.js";
11
11
  import { OnboardingCard } from "../components/OnboardingCard.js";
12
12
  import { useAppCatalogFilters } from "../context/AppCatalogFiltersContext.js";
13
13
  import { FilterBar } from "../filters/FilterBar.js";
14
14
  import { AppCatalogGrid } from "../grid/AppCatalogGrid.js";
15
- function AppCatalogPage() {
15
+ function AppCatalogPage({
16
+ selectedSlug
17
+ } = {}) {
16
18
  const { resources, isLoadingApps, tagsDefinitions } = useAppCatalogContext();
17
19
  const { state: filterState, actions } = useAppCatalogFilters();
18
20
  const { getTopApps } = useAppClickHistory();
19
- const [selectedAppSlug, setSelectedAppSlug] = useUrlSyncedState({
20
- key: "app",
21
- defaultValue: void 0
22
- });
21
+ const navigate = useNavigate();
22
+ const selectedAppSlug = selectedSlug;
23
23
  const searchValue = filterState.searchValue;
24
24
  const setSearchValue = actions.setSearchValue;
25
25
  const deferredSearchValue = useDeferredValue(searchValue);
@@ -72,17 +72,21 @@ function AppCatalogPage() {
72
72
  searchValue: deferredSearchValue
73
73
  });
74
74
  useEffect(() => {
75
- if (filteredApps.length === 1 && filteredApps[0]) {
76
- setSelectedAppSlug(filteredApps[0].slug);
75
+ if (filteredApps.length === 1 && filteredApps[0] && !selectedAppSlug) {
76
+ void navigate({
77
+ to: "/app/$slug",
78
+ params: { slug: filteredApps[0].slug },
79
+ replace: true
80
+ });
77
81
  }
78
- }, [filteredApps, setSelectedAppSlug]);
82
+ }, [filteredApps, selectedAppSlug, navigate]);
79
83
  const handleAppClick = (app) => {
80
- setSelectedAppSlug(app.slug);
84
+ void navigate({ to: "/app/$slug", params: { slug: app.slug } });
81
85
  };
82
86
  const handleClearFilters = () => {
83
87
  setSearchValue("");
84
88
  actions.clearAllFilters();
85
- setSelectedAppSlug(void 0);
89
+ void navigate({ to: "/" });
86
90
  };
87
91
  const totalAppsCount = useMemo(() => {
88
92
  let count = rootResources.length;
@@ -121,7 +125,7 @@ function AppCatalogPage() {
121
125
  variant: "outline",
122
126
  onClick: () => {
123
127
  setSearchValue("");
124
- setSelectedAppSlug(void 0);
128
+ void navigate({ to: "/" });
125
129
  },
126
130
  className: "gap-2",
127
131
  children: [
@@ -137,6 +141,7 @@ function AppCatalogPage() {
137
141
  selectedAppSlug,
138
142
  groupingDefinition,
139
143
  onAppClick: handleAppClick,
144
+ onClosePanel: () => void navigate({ to: "/" }),
140
145
  hasSearch: !!deferredSearchValue,
141
146
  searchQuery: deferredSearchValue,
142
147
  totalAppsCount,
@@ -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 { 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 { useUrlSyncedState } from '../../hooks/useUrlSyncedState'\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'\n\nexport function AppCatalogPage() {\n const { resources, isLoadingApps, tagsDefinitions } = useAppCatalogContext()\n const { state: filterState, actions } = useAppCatalogFilters()\n const { getTopApps } = useAppClickHistory()\n\n // URL-synced state\n const [selectedAppSlug, setSelectedAppSlug] = useUrlSyncedState<\n string | undefined\n >({\n key: 'app',\n defaultValue: undefined,\n })\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 const filteredApps = useMemo(() => {\n let result = rootResources\n\n // Step 1: Filter deprecated apps (if not showing them)\n if (!filterState.showDeprecated) {\n result = result.filter((app) => !app.deprecated)\n }\n\n // Step 2: 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 // Step 3: Apply search (using deferred value)\n // Pass all resources so children contribute to parent scoring\n const childResources = resources.filter((r) => r.parentSlug)\n result = searchResources(\n [...result, ...childResources],\n deferredSearchValue,\n )\n\n return result\n }, [\n rootResources,\n resources,\n deferredSearchValue,\n filterState.recentMode,\n filterState.tagFilters,\n filterState.showDeprecated,\n topAppSlugs,\n ])\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\n useEffect(() => {\n if (filteredApps.length === 1 && filteredApps[0]) {\n setSelectedAppSlug(filteredApps[0].slug)\n }\n }, [filteredApps, setSelectedAppSlug])\n\n const handleAppClick = (app: Resource) => {\n setSelectedAppSlug(app.slug)\n }\n\n const handleClearFilters = () => {\n setSearchValue('')\n actions.clearAllFilters()\n setSelectedAppSlug(undefined)\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 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 <div className=\"flex-1 min-h-0\">\n {filteredApps.length === 0 ? (\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 setSelectedAppSlug(undefined)\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 hasSearch={!!deferredSearchValue}\n searchQuery={deferredSearchValue}\n totalAppsCount={totalAppsCount}\n onClearFilters={handleClearFilters}\n />\n )}\n </div>\n </div>\n )\n}\n"],"names":[],"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 } 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'\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 const filteredApps = useMemo(() => {\n let result = rootResources\n\n // Step 1: Filter deprecated apps (if not showing them)\n if (!filterState.showDeprecated) {\n result = result.filter((app) => !app.deprecated)\n }\n\n // Step 2: 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 // Step 3: Apply search (using deferred value)\n // Pass all resources so children contribute to parent scoring\n const childResources = resources.filter((r) => r.parentSlug)\n result = searchResources(\n [...result, ...childResources],\n deferredSearchValue,\n )\n\n return result\n }, [\n rootResources,\n resources,\n deferredSearchValue,\n filterState.recentMode,\n filterState.tagFilters,\n filterState.showDeprecated,\n topAppSlugs,\n ])\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\n useEffect(() => {\n if (filteredApps.length === 1 && filteredApps[0] && !selectedAppSlug) {\n void navigate({\n to: '/app/$slug',\n params: { slug: filteredApps[0].slug },\n replace: true,\n })\n }\n }, [filteredApps, selectedAppSlug, navigate])\n\n const handleAppClick = (app: Resource) => {\n void navigate({ to: '/app/$slug', params: { slug: app.slug } })\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 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 <div className=\"flex-1 min-h-0\">\n {filteredApps.length === 0 ? (\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":[],"mappings":"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"}
@@ -3,10 +3,10 @@ import { Route as LayoutRouteImport } from './routes/_layout.js';
3
3
  import { Route as LayoutIndexRouteImport } from './routes/_layout/index.js';
4
4
  import { Route as AuthCallbackRouteImport } from './routes/auth.callback';
5
5
  import { Route as LayoutLoginRouteImport } from './routes/_layout/login.js';
6
+ import { Route as LayoutAppSlugRouteImport } from './routes/_layout/app.$slug';
6
7
  import { Route as LayoutCatalogAppsIndexRouteImport } from './routes/_layout/catalog.apps.index';
7
8
  declare const LayoutRoute: 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>;
8
9
  declare const LayoutIndexRoute: 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>, "/", "/", "/_layout/", "/_layout/", import('zod').ZodObject<{
9
- app: import('zod').ZodOptional<import('zod').ZodString>;
10
10
  filterTag: import('zod').ZodOptional<import('zod').ZodString>;
11
11
  }, import('zod/v4/core').$strip>, import('@tanstack/router-core').ResolveParams<"/">, import('@tanstack/router-core').AnyContext, import('@tanstack/router-core').AnyContext, import('@tanstack/router-core').AnyContext, {}, () => Promise<{
12
12
  appCatalogLoader: import('./modules/appCatalog/routeLoader.js').AppCatalogLoaderReturn;
@@ -16,6 +16,9 @@ declare const AuthCallbackRoute: import('@tanstack/router-core').Route<import('@
16
16
  code: string | undefined;
17
17
  }, import('@tanstack/router-core').ResolveParams<"/auth/callback">, import('@tanstack/router-core').AnyContext, import('@tanstack/router-core').AnyContext, import('@tanstack/router-core').AnyContext, {}, undefined, unknown, unknown, unknown, unknown, undefined>;
18
18
  declare const LayoutLoginRoute: 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>, "/login", "/login", "/_layout/login", "/_layout/login", undefined, import('@tanstack/router-core').ResolveParams<"/login">, import('@tanstack/router-core').AnyContext, import('@tanstack/router-core').AnyContext, import('@tanstack/router-core').AnyContext, {}, undefined, unknown, unknown, unknown, unknown, undefined>;
19
+ declare const LayoutAppSlugRoute: 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>, "/app/$slug", "/app/$slug", "/_layout/app/$slug", "/_layout/app/$slug", undefined, import('@tanstack/router-core').ResolveParams<"/app/$slug">, import('@tanstack/router-core').AnyContext, import('@tanstack/router-core').AnyContext, import('@tanstack/router-core').AnyContext, {}, () => Promise<{
20
+ appCatalogLoader: import('./modules/appCatalog/routeLoader.js').AppCatalogLoaderReturn;
21
+ }>, unknown, unknown, unknown, unknown, undefined>;
19
22
  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<{
20
23
  app: import('zod').ZodOptional<import('zod').ZodString>;
21
24
  q: import('zod').ZodOptional<import('zod').ZodString>;
@@ -24,12 +27,14 @@ export interface FileRoutesByFullPath {
24
27
  '/': typeof LayoutIndexRoute;
25
28
  '/login': typeof LayoutLoginRoute;
26
29
  '/auth/callback': typeof AuthCallbackRoute;
30
+ '/app/$slug': typeof LayoutAppSlugRoute;
27
31
  '/catalog/apps/': typeof LayoutCatalogAppsIndexRoute;
28
32
  }
29
33
  export interface FileRoutesByTo {
30
34
  '/login': typeof LayoutLoginRoute;
31
35
  '/auth/callback': typeof AuthCallbackRoute;
32
36
  '/': typeof LayoutIndexRoute;
37
+ '/app/$slug': typeof LayoutAppSlugRoute;
33
38
  '/catalog/apps': typeof LayoutCatalogAppsIndexRoute;
34
39
  }
35
40
  export interface FileRoutesById {
@@ -38,14 +43,15 @@ export interface FileRoutesById {
38
43
  '/_layout/login': typeof LayoutLoginRoute;
39
44
  '/auth/callback': typeof AuthCallbackRoute;
40
45
  '/_layout/': typeof LayoutIndexRoute;
46
+ '/_layout/app/$slug': typeof LayoutAppSlugRoute;
41
47
  '/_layout/catalog/apps/': typeof LayoutCatalogAppsIndexRoute;
42
48
  }
43
49
  export interface FileRouteTypes {
44
50
  fileRoutesByFullPath: FileRoutesByFullPath;
45
- fullPaths: '/' | '/login' | '/auth/callback' | '/catalog/apps/';
51
+ fullPaths: '/' | '/login' | '/auth/callback' | '/app/$slug' | '/catalog/apps/';
46
52
  fileRoutesByTo: FileRoutesByTo;
47
- to: '/login' | '/auth/callback' | '/' | '/catalog/apps';
48
- id: '__root__' | '/_layout' | '/_layout/login' | '/auth/callback' | '/_layout/' | '/_layout/catalog/apps/';
53
+ to: '/login' | '/auth/callback' | '/' | '/app/$slug' | '/catalog/apps';
54
+ id: '__root__' | '/_layout' | '/_layout/login' | '/auth/callback' | '/_layout/' | '/_layout/app/$slug' | '/_layout/catalog/apps/';
49
55
  fileRoutesById: FileRoutesById;
50
56
  }
51
57
  export interface RootRouteChildren {
@@ -82,6 +88,13 @@ declare module '@tanstack/react-router' {
82
88
  preLoaderRoute: typeof LayoutLoginRouteImport;
83
89
  parentRoute: typeof LayoutRoute;
84
90
  };
91
+ '/_layout/app/$slug': {
92
+ id: '/_layout/app/$slug';
93
+ path: '/app/$slug';
94
+ fullPath: '/app/$slug';
95
+ preLoaderRoute: typeof LayoutAppSlugRouteImport;
96
+ parentRoute: typeof LayoutRoute;
97
+ };
85
98
  '/_layout/catalog/apps/': {
86
99
  id: '/_layout/catalog/apps/';
87
100
  path: '/catalog/apps';
@@ -94,6 +107,7 @@ declare module '@tanstack/react-router' {
94
107
  interface LayoutRouteChildren {
95
108
  LayoutLoginRoute: typeof LayoutLoginRoute;
96
109
  LayoutIndexRoute: typeof LayoutIndexRoute;
110
+ LayoutAppSlugRoute: typeof LayoutAppSlugRoute;
97
111
  LayoutCatalogAppsIndexRoute: typeof LayoutCatalogAppsIndexRoute;
98
112
  }
99
113
  declare const LayoutRouteChildren: LayoutRouteChildren;
@@ -1,14 +1,15 @@
1
1
  import { Route } from "./routes/__root.js";
2
2
  import { Route as Route$2 } from "./routes/_layout.js";
3
- import { Route as Route$4 } from "./routes/_layout/index.js";
3
+ import { Route as Route$5 } from "./routes/_layout/index.js";
4
4
  import { Route as Route$1 } from "./routes/auth.callback.js";
5
- import { Route as Route$5 } from "./routes/_layout/login.js";
5
+ import { Route as Route$6 } from "./routes/_layout/login.js";
6
+ import { Route as Route$4 } from "./routes/_layout/app._slug.js";
6
7
  import { Route as Route$3 } from "./routes/_layout/catalog.apps.index.js";
7
8
  const LayoutRoute = Route$2.update({
8
9
  id: "/_layout",
9
10
  getParentRoute: () => Route
10
11
  });
11
- const LayoutIndexRoute = Route$4.update({
12
+ const LayoutIndexRoute = Route$5.update({
12
13
  id: "/",
13
14
  path: "/",
14
15
  getParentRoute: () => LayoutRoute
@@ -18,11 +19,16 @@ const AuthCallbackRoute = Route$1.update({
18
19
  path: "/auth/callback",
19
20
  getParentRoute: () => Route
20
21
  });
21
- const LayoutLoginRoute = Route$5.update({
22
+ const LayoutLoginRoute = Route$6.update({
22
23
  id: "/login",
23
24
  path: "/login",
24
25
  getParentRoute: () => LayoutRoute
25
26
  });
27
+ const LayoutAppSlugRoute = Route$4.update({
28
+ id: "/app/$slug",
29
+ path: "/app/$slug",
30
+ getParentRoute: () => LayoutRoute
31
+ });
26
32
  const LayoutCatalogAppsIndexRoute = Route$3.update({
27
33
  id: "/catalog/apps/",
28
34
  path: "/catalog/apps/",
@@ -31,6 +37,7 @@ const LayoutCatalogAppsIndexRoute = Route$3.update({
31
37
  const LayoutRouteChildren = {
32
38
  LayoutLoginRoute,
33
39
  LayoutIndexRoute,
40
+ LayoutAppSlugRoute,
34
41
  LayoutCatalogAppsIndexRoute
35
42
  };
36
43
  const LayoutRouteWithChildren = LayoutRoute._addFileChildren(LayoutRouteChildren);
@@ -1 +1 @@
1
- {"version":3,"file":"routeTree.gen.js","sources":["../../src/routeTree.gen.ts"],"sourcesContent":["/* eslint-disable */\n\n// @ts-nocheck\n\n// noinspection JSUnusedGlobalSymbols\n\n// This file was automatically generated by TanStack Router.\n// You should NOT make any changes in this file as it will be overwritten.\n// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.\n\nimport { Route as rootRouteImport } from './routes/__root'\nimport { Route as LayoutRouteImport } from './routes/_layout'\nimport { Route as LayoutIndexRouteImport } from './routes/_layout/index'\nimport { Route as AuthCallbackRouteImport } from './routes/auth.callback'\nimport { Route as LayoutLoginRouteImport } from './routes/_layout/login'\nimport { Route as LayoutCatalogAppsIndexRouteImport } from './routes/_layout/catalog.apps.index'\n\nconst LayoutRoute = LayoutRouteImport.update({\n id: '/_layout',\n getParentRoute: () => rootRouteImport,\n} as any)\nconst LayoutIndexRoute = LayoutIndexRouteImport.update({\n id: '/',\n path: '/',\n getParentRoute: () => LayoutRoute,\n} as any)\nconst AuthCallbackRoute = AuthCallbackRouteImport.update({\n id: '/auth/callback',\n path: '/auth/callback',\n getParentRoute: () => rootRouteImport,\n} as any)\nconst LayoutLoginRoute = LayoutLoginRouteImport.update({\n id: '/login',\n path: '/login',\n getParentRoute: () => LayoutRoute,\n} as any)\nconst LayoutCatalogAppsIndexRoute = LayoutCatalogAppsIndexRouteImport.update({\n id: '/catalog/apps/',\n path: '/catalog/apps/',\n getParentRoute: () => LayoutRoute,\n} as any)\n\nexport interface FileRoutesByFullPath {\n '/': typeof LayoutIndexRoute\n '/login': typeof LayoutLoginRoute\n '/auth/callback': typeof AuthCallbackRoute\n '/catalog/apps/': typeof LayoutCatalogAppsIndexRoute\n}\nexport interface FileRoutesByTo {\n '/login': typeof LayoutLoginRoute\n '/auth/callback': typeof AuthCallbackRoute\n '/': typeof LayoutIndexRoute\n '/catalog/apps': typeof LayoutCatalogAppsIndexRoute\n}\nexport interface FileRoutesById {\n __root__: typeof rootRouteImport\n '/_layout': typeof LayoutRouteWithChildren\n '/_layout/login': typeof LayoutLoginRoute\n '/auth/callback': typeof AuthCallbackRoute\n '/_layout/': typeof LayoutIndexRoute\n '/_layout/catalog/apps/': typeof LayoutCatalogAppsIndexRoute\n}\nexport interface FileRouteTypes {\n fileRoutesByFullPath: FileRoutesByFullPath\n fullPaths: '/' | '/login' | '/auth/callback' | '/catalog/apps/'\n fileRoutesByTo: FileRoutesByTo\n to: '/login' | '/auth/callback' | '/' | '/catalog/apps'\n id:\n | '__root__'\n | '/_layout'\n | '/_layout/login'\n | '/auth/callback'\n | '/_layout/'\n | '/_layout/catalog/apps/'\n fileRoutesById: FileRoutesById\n}\nexport interface RootRouteChildren {\n LayoutRoute: typeof LayoutRouteWithChildren\n AuthCallbackRoute: typeof AuthCallbackRoute\n}\n\ndeclare module '@tanstack/react-router' {\n interface FileRoutesByPath {\n '/_layout': {\n id: '/_layout'\n path: ''\n fullPath: '/'\n preLoaderRoute: typeof LayoutRouteImport\n parentRoute: typeof rootRouteImport\n }\n '/_layout/': {\n id: '/_layout/'\n path: '/'\n fullPath: '/'\n preLoaderRoute: typeof LayoutIndexRouteImport\n parentRoute: typeof LayoutRoute\n }\n '/auth/callback': {\n id: '/auth/callback'\n path: '/auth/callback'\n fullPath: '/auth/callback'\n preLoaderRoute: typeof AuthCallbackRouteImport\n parentRoute: typeof rootRouteImport\n }\n '/_layout/login': {\n id: '/_layout/login'\n path: '/login'\n fullPath: '/login'\n preLoaderRoute: typeof LayoutLoginRouteImport\n parentRoute: typeof LayoutRoute\n }\n '/_layout/catalog/apps/': {\n id: '/_layout/catalog/apps/'\n path: '/catalog/apps'\n fullPath: '/catalog/apps/'\n preLoaderRoute: typeof LayoutCatalogAppsIndexRouteImport\n parentRoute: typeof LayoutRoute\n }\n }\n}\n\ninterface LayoutRouteChildren {\n LayoutLoginRoute: typeof LayoutLoginRoute\n LayoutIndexRoute: typeof LayoutIndexRoute\n LayoutCatalogAppsIndexRoute: typeof LayoutCatalogAppsIndexRoute\n}\n\nconst LayoutRouteChildren: LayoutRouteChildren = {\n LayoutLoginRoute: LayoutLoginRoute,\n LayoutIndexRoute: LayoutIndexRoute,\n LayoutCatalogAppsIndexRoute: LayoutCatalogAppsIndexRoute,\n}\n\nconst LayoutRouteWithChildren =\n LayoutRoute._addFileChildren(LayoutRouteChildren)\n\nconst rootRouteChildren: RootRouteChildren = {\n LayoutRoute: LayoutRouteWithChildren,\n AuthCallbackRoute: AuthCallbackRoute,\n}\nexport const routeTree = rootRouteImport\n ._addFileChildren(rootRouteChildren)\n ._addFileTypes<FileRouteTypes>()\n"],"names":["LayoutRouteImport","rootRouteImport","LayoutIndexRouteImport","AuthCallbackRouteImport","LayoutLoginRouteImport","LayoutCatalogAppsIndexRouteImport"],"mappings":";;;;;;AAiBA,MAAM,cAAcA,QAAkB,OAAO;AAAA,EAC3C,IAAI;AAAA,EACJ,gBAAgB,MAAMC;AACxB,CAAQ;AACR,MAAM,mBAAmBC,QAAuB,OAAO;AAAA,EACrD,IAAI;AAAA,EACJ,MAAM;AAAA,EACN,gBAAgB,MAAM;AACxB,CAAQ;AACR,MAAM,oBAAoBC,QAAwB,OAAO;AAAA,EACvD,IAAI;AAAA,EACJ,MAAM;AAAA,EACN,gBAAgB,MAAMF;AACxB,CAAQ;AACR,MAAM,mBAAmBG,QAAuB,OAAO;AAAA,EACrD,IAAI;AAAA,EACJ,MAAM;AAAA,EACN,gBAAgB,MAAM;AACxB,CAAQ;AACR,MAAM,8BAA8BC,QAAkC,OAAO;AAAA,EAC3E,IAAI;AAAA,EACJ,MAAM;AAAA,EACN,gBAAgB,MAAM;AACxB,CAAQ;AAuFR,MAAM,sBAA2C;AAAA,EAC/C;AAAA,EACA;AAAA,EACA;AACF;AAEA,MAAM,0BACJ,YAAY,iBAAiB,mBAAmB;AAElD,MAAM,oBAAuC;AAAA,EAC3C,aAAa;AAAA,EACb;AACF;AACO,MAAM,YAAYJ,MACtB,iBAAiB,iBAAiB,EAClC,cAAA;"}
1
+ {"version":3,"file":"routeTree.gen.js","sources":["../../src/routeTree.gen.ts"],"sourcesContent":["/* eslint-disable */\n\n// @ts-nocheck\n\n// noinspection JSUnusedGlobalSymbols\n\n// This file was automatically generated by TanStack Router.\n// You should NOT make any changes in this file as it will be overwritten.\n// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.\n\nimport { Route as rootRouteImport } from './routes/__root'\nimport { Route as LayoutRouteImport } from './routes/_layout'\nimport { Route as LayoutIndexRouteImport } from './routes/_layout/index'\nimport { Route as AuthCallbackRouteImport } from './routes/auth.callback'\nimport { Route as LayoutLoginRouteImport } from './routes/_layout/login'\nimport { Route as LayoutAppSlugRouteImport } from './routes/_layout/app.$slug'\nimport { Route as LayoutCatalogAppsIndexRouteImport } from './routes/_layout/catalog.apps.index'\n\nconst LayoutRoute = LayoutRouteImport.update({\n id: '/_layout',\n getParentRoute: () => rootRouteImport,\n} as any)\nconst LayoutIndexRoute = LayoutIndexRouteImport.update({\n id: '/',\n path: '/',\n getParentRoute: () => LayoutRoute,\n} as any)\nconst AuthCallbackRoute = AuthCallbackRouteImport.update({\n id: '/auth/callback',\n path: '/auth/callback',\n getParentRoute: () => rootRouteImport,\n} as any)\nconst LayoutLoginRoute = LayoutLoginRouteImport.update({\n id: '/login',\n path: '/login',\n getParentRoute: () => LayoutRoute,\n} as any)\nconst LayoutAppSlugRoute = LayoutAppSlugRouteImport.update({\n id: '/app/$slug',\n path: '/app/$slug',\n getParentRoute: () => LayoutRoute,\n} as any)\nconst LayoutCatalogAppsIndexRoute = LayoutCatalogAppsIndexRouteImport.update({\n id: '/catalog/apps/',\n path: '/catalog/apps/',\n getParentRoute: () => LayoutRoute,\n} as any)\n\nexport interface FileRoutesByFullPath {\n '/': typeof LayoutIndexRoute\n '/login': typeof LayoutLoginRoute\n '/auth/callback': typeof AuthCallbackRoute\n '/app/$slug': typeof LayoutAppSlugRoute\n '/catalog/apps/': typeof LayoutCatalogAppsIndexRoute\n}\nexport interface FileRoutesByTo {\n '/login': typeof LayoutLoginRoute\n '/auth/callback': typeof AuthCallbackRoute\n '/': typeof LayoutIndexRoute\n '/app/$slug': typeof LayoutAppSlugRoute\n '/catalog/apps': typeof LayoutCatalogAppsIndexRoute\n}\nexport interface FileRoutesById {\n __root__: typeof rootRouteImport\n '/_layout': typeof LayoutRouteWithChildren\n '/_layout/login': typeof LayoutLoginRoute\n '/auth/callback': typeof AuthCallbackRoute\n '/_layout/': typeof LayoutIndexRoute\n '/_layout/app/$slug': typeof LayoutAppSlugRoute\n '/_layout/catalog/apps/': typeof LayoutCatalogAppsIndexRoute\n}\nexport interface FileRouteTypes {\n fileRoutesByFullPath: FileRoutesByFullPath\n fullPaths: '/' | '/login' | '/auth/callback' | '/app/$slug' | '/catalog/apps/'\n fileRoutesByTo: FileRoutesByTo\n to: '/login' | '/auth/callback' | '/' | '/app/$slug' | '/catalog/apps'\n id:\n | '__root__'\n | '/_layout'\n | '/_layout/login'\n | '/auth/callback'\n | '/_layout/'\n | '/_layout/app/$slug'\n | '/_layout/catalog/apps/'\n fileRoutesById: FileRoutesById\n}\nexport interface RootRouteChildren {\n LayoutRoute: typeof LayoutRouteWithChildren\n AuthCallbackRoute: typeof AuthCallbackRoute\n}\n\ndeclare module '@tanstack/react-router' {\n interface FileRoutesByPath {\n '/_layout': {\n id: '/_layout'\n path: ''\n fullPath: '/'\n preLoaderRoute: typeof LayoutRouteImport\n parentRoute: typeof rootRouteImport\n }\n '/_layout/': {\n id: '/_layout/'\n path: '/'\n fullPath: '/'\n preLoaderRoute: typeof LayoutIndexRouteImport\n parentRoute: typeof LayoutRoute\n }\n '/auth/callback': {\n id: '/auth/callback'\n path: '/auth/callback'\n fullPath: '/auth/callback'\n preLoaderRoute: typeof AuthCallbackRouteImport\n parentRoute: typeof rootRouteImport\n }\n '/_layout/login': {\n id: '/_layout/login'\n path: '/login'\n fullPath: '/login'\n preLoaderRoute: typeof LayoutLoginRouteImport\n parentRoute: typeof LayoutRoute\n }\n '/_layout/app/$slug': {\n id: '/_layout/app/$slug'\n path: '/app/$slug'\n fullPath: '/app/$slug'\n preLoaderRoute: typeof LayoutAppSlugRouteImport\n parentRoute: typeof LayoutRoute\n }\n '/_layout/catalog/apps/': {\n id: '/_layout/catalog/apps/'\n path: '/catalog/apps'\n fullPath: '/catalog/apps/'\n preLoaderRoute: typeof LayoutCatalogAppsIndexRouteImport\n parentRoute: typeof LayoutRoute\n }\n }\n}\n\ninterface LayoutRouteChildren {\n LayoutLoginRoute: typeof LayoutLoginRoute\n LayoutIndexRoute: typeof LayoutIndexRoute\n LayoutAppSlugRoute: typeof LayoutAppSlugRoute\n LayoutCatalogAppsIndexRoute: typeof LayoutCatalogAppsIndexRoute\n}\n\nconst LayoutRouteChildren: LayoutRouteChildren = {\n LayoutLoginRoute: LayoutLoginRoute,\n LayoutIndexRoute: LayoutIndexRoute,\n LayoutAppSlugRoute: LayoutAppSlugRoute,\n LayoutCatalogAppsIndexRoute: LayoutCatalogAppsIndexRoute,\n}\n\nconst LayoutRouteWithChildren =\n LayoutRoute._addFileChildren(LayoutRouteChildren)\n\nconst rootRouteChildren: RootRouteChildren = {\n LayoutRoute: LayoutRouteWithChildren,\n AuthCallbackRoute: AuthCallbackRoute,\n}\nexport const routeTree = rootRouteImport\n ._addFileChildren(rootRouteChildren)\n ._addFileTypes<FileRouteTypes>()\n"],"names":["LayoutRouteImport","rootRouteImport","LayoutIndexRouteImport","AuthCallbackRouteImport","LayoutLoginRouteImport","LayoutAppSlugRouteImport","LayoutCatalogAppsIndexRouteImport"],"mappings":";;;;;;;AAkBA,MAAM,cAAcA,QAAkB,OAAO;AAAA,EAC3C,IAAI;AAAA,EACJ,gBAAgB,MAAMC;AACxB,CAAQ;AACR,MAAM,mBAAmBC,QAAuB,OAAO;AAAA,EACrD,IAAI;AAAA,EACJ,MAAM;AAAA,EACN,gBAAgB,MAAM;AACxB,CAAQ;AACR,MAAM,oBAAoBC,QAAwB,OAAO;AAAA,EACvD,IAAI;AAAA,EACJ,MAAM;AAAA,EACN,gBAAgB,MAAMF;AACxB,CAAQ;AACR,MAAM,mBAAmBG,QAAuB,OAAO;AAAA,EACrD,IAAI;AAAA,EACJ,MAAM;AAAA,EACN,gBAAgB,MAAM;AACxB,CAAQ;AACR,MAAM,qBAAqBC,QAAyB,OAAO;AAAA,EACzD,IAAI;AAAA,EACJ,MAAM;AAAA,EACN,gBAAgB,MAAM;AACxB,CAAQ;AACR,MAAM,8BAA8BC,QAAkC,OAAO;AAAA,EAC3E,IAAI;AAAA,EACJ,MAAM;AAAA,EACN,gBAAgB,MAAM;AACxB,CAAQ;AAmGR,MAAM,sBAA2C;AAAA,EAC/C;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAEA,MAAM,0BACJ,YAAY,iBAAiB,mBAAmB;AAElD,MAAM,oBAAuC;AAAA,EAC3C,aAAa;AAAA,EACb;AACF;AACO,MAAM,YAAYL,MACtB,iBAAiB,iBAAiB,EAClC,cAAA;"}
@@ -0,0 +1,3 @@
1
+ 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>, "/app/$slug", "/app/$slug", "/_layout/app/$slug", "/_layout/app/$slug", undefined, import('@tanstack/router-core').ResolveParams<"/app/$slug">, import('@tanstack/router-core').AnyContext, import('@tanstack/router-core').AnyContext, import('@tanstack/router-core').AnyContext, {}, () => Promise<{
2
+ appCatalogLoader: import('~/modules/appCatalog/routeLoader').AppCatalogLoaderReturn;
3
+ }>, unknown, unknown, unknown, unknown, undefined>;
@@ -0,0 +1,21 @@
1
+ import { jsx } from "react/jsx-runtime";
2
+ import { createFileRoute } from "@tanstack/react-router";
3
+ import { appCatalogRouteLoader } from "../../modules/appCatalog/routeLoader.js";
4
+ import { AppCatalogLayout } from "../../modules/appCatalog/ui/layout/AppCatalogLayout.js";
5
+ import { AppCatalogPage } from "../../modules/appCatalog/ui/pages/AppCatalogPage.js";
6
+ const Route = createFileRoute("/_layout/app/$slug")({
7
+ component: RouteComponent,
8
+ async loader() {
9
+ const appCatalogLoader = await appCatalogRouteLoader();
10
+ return { appCatalogLoader };
11
+ }
12
+ });
13
+ function RouteComponent() {
14
+ const { queryClient, trpcClient } = Route.useRouteContext();
15
+ const { slug } = Route.useParams();
16
+ return /* @__PURE__ */ jsx(AppCatalogLayout, { queryClient, trpcClient, children: /* @__PURE__ */ jsx(AppCatalogPage, { selectedSlug: slug }) });
17
+ }
18
+ export {
19
+ Route
20
+ };
21
+ //# sourceMappingURL=app._slug.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"app._slug.js","sources":["../../../../src/routes/_layout/app.$slug.tsx"],"sourcesContent":["import { createFileRoute } from '@tanstack/react-router'\nimport { appCatalogRouteLoader } from '~/modules/appCatalog/routeLoader'\nimport { AppCatalogLayout } from '~/modules/appCatalog/ui/layout/AppCatalogLayout'\nimport { AppCatalogPage } from '~/modules/appCatalog/ui/pages/AppCatalogPage'\n\nexport const Route = createFileRoute('/_layout/app/$slug')({\n component: RouteComponent,\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":[],"mappings":";;;;;AAKO,MAAM,QAAQ,gBAAgB,oBAAoB,EAAE;AAAA,EACzD,WAAW;AAAA,EACX,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;"}
@@ -1,6 +1,5 @@
1
1
  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>, "/", "/", "/_layout/", "/_layout/", z.ZodObject<{
3
- app: z.ZodOptional<z.ZodString>;
4
3
  filterTag: z.ZodOptional<z.ZodString>;
5
4
  }, z.core.$strip>, import('@tanstack/router-core').ResolveParams<"/">, import('@tanstack/router-core').AnyContext, import('@tanstack/router-core').AnyContext, import('@tanstack/router-core').AnyContext, {}, () => Promise<{
6
5
  appCatalogLoader: import('~/modules/appCatalog/routeLoader').AppCatalogLoaderReturn;
@@ -5,7 +5,6 @@ import { AppCatalogLayout } from "../../modules/appCatalog/ui/layout/AppCatalogL
5
5
  import { AppCatalogPage } from "../../modules/appCatalog/ui/pages/AppCatalogPage.js";
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
- app: string().optional(),
9
8
  filterTag: string().optional()
10
9
  });
11
10
  const Route = createFileRoute("/_layout/")({
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sources":["../../../../src/routes/_layout/index.tsx"],"sourcesContent":["import { createFileRoute } 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\nconst searchSchema = z.object({\n app: z.string().optional(),\n filterTag: z.string().optional(),\n})\n\nexport const Route = createFileRoute('/_layout/')({\n component: RouteComponent,\n validateSearch: searchSchema,\n async loader() {\n const appCatalogLoader = await appCatalogRouteLoader()\n return {\n appCatalogLoader,\n }\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":";;;;;;AAMA,MAAM,eAAeA,OAAS;AAAA,EAC5B,KAAKC,OAAE,EAAS,SAAA;AAAA,EAChB,WAAWA,OAAE,EAAS,SAAA;AACxB,CAAC;AAEM,MAAM,QAAQ,gBAAgB,WAAW,EAAE;AAAA,EAChD,WAAW;AAAA,EACX,gBAAgB;AAAA,EAChB,MAAM,SAAS;AACb,UAAM,mBAAmB,MAAM,sBAAA;AAC/B,WAAO;AAAA,MACL;AAAA,IAAA;AAAA,EAEJ;AACF,CAAC;AAED,SAAS,iBAAiB;AACxB,QAAM,EAAE,aAAa,eAAe,MAAM,gBAAA;AAE1C,6BACG,kBAAA,EAAiB,aAA0B,YAC1C,UAAA,oBAAC,kBAAe,GAClB;AAEJ;"}
1
+ {"version":3,"file":"index.js","sources":["../../../../src/routes/_layout/index.tsx"],"sourcesContent":["import { createFileRoute } 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\nconst searchSchema = z.object({\n filterTag: z.string().optional(),\n})\n\nexport const Route = createFileRoute('/_layout/')({\n component: RouteComponent,\n validateSearch: searchSchema,\n async loader() {\n const appCatalogLoader = await appCatalogRouteLoader()\n return {\n appCatalogLoader,\n }\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":";;;;;;AAMA,MAAM,eAAeA,OAAS;AAAA,EAC5B,WAAWC,OAAE,EAAS,SAAA;AACxB,CAAC;AAEM,MAAM,QAAQ,gBAAgB,WAAW,EAAE;AAAA,EAChD,WAAW;AAAA,EACX,gBAAgB;AAAA,EAChB,MAAM,SAAS;AACb,UAAM,mBAAmB,MAAM,sBAAA;AAC/B,WAAO;AAAA,MACL;AAAA,IAAA;AAAA,EAEJ;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.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "Frontend core library for App Catalog",
5
5
  "homepage": "https://github.com/lislon/app-catalog",
6
6
  "repository": {
@@ -134,8 +134,8 @@
134
134
  "vite-plugin-static-copy": "^3.1.4",
135
135
  "vite-plugin-svgr": "^4.2.0",
136
136
  "vitest": "^4.1.2",
137
- "@igstack/app-catalog-backend-core": "0.5.0",
138
- "@igstack/app-catalog-shared-core": "0.5.0"
137
+ "@igstack/app-catalog-backend-core": "0.6.0",
138
+ "@igstack/app-catalog-shared-core": "0.6.0"
139
139
  },
140
140
  "peerDependencies": {
141
141
  "react": "19.1.2",
@@ -0,0 +1,55 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { fireEvent, waitFor } from '@testing-library/react'
3
+ import '@testing-library/jest-dom/vitest'
4
+
5
+ import { given } from './harness/given'
6
+ import { magazine } from './mock-backend/magazines'
7
+
8
+ describe('App deep-link routing', () => {
9
+ it('opens the app detail when navigating directly to /app/<slug>', async () => {
10
+ const { ui } = await given(magazine.full(), { initialRoute: '/app/jira' })
11
+ await waitFor(() => {
12
+ expect(ui.catalog.isDetailPanelOpen()).toBe(true)
13
+ })
14
+ expect(ui.catalog.getTableData().length).toBeGreaterThan(0)
15
+ })
16
+
17
+ it('navigates to /app/<slug> when selecting an app, and pushes history', async () => {
18
+ const { ui, router } = await given(magazine.full(), { initialRoute: '/' })
19
+ await ui.catalog.openApp('Jira')
20
+ await waitFor(() => {
21
+ expect(router.state.location.pathname).toBe('/app/jira')
22
+ })
23
+ // push (not replace): catalog '/' is still in history, Back returns to it
24
+ expect(router.history.canGoBack()).toBe(true)
25
+ })
26
+
27
+ it('renders the catalog with nothing open for an unknown slug', async () => {
28
+ const { ui } = await given(magazine.full(), {
29
+ initialRoute: '/app/does-not-exist',
30
+ })
31
+ await waitFor(() => {
32
+ expect(ui.catalog.getTableData().length).toBeGreaterThan(0)
33
+ })
34
+ expect(ui.catalog.isDetailPanelOpen()).toBe(false)
35
+ })
36
+
37
+ it('returns to / when the detail panel is closed', async () => {
38
+ const { ui, router } = await given(magazine.full(), { initialRoute: '/' })
39
+ await ui.catalog.openApp('Jira')
40
+ await waitFor(() => expect(ui.catalog.isDetailPanelOpen()).toBe(true))
41
+ fireEvent.keyDown(document, { key: 'Escape', code: 'Escape' })
42
+ await waitFor(() => {
43
+ expect(ui.catalog.isDetailPanelOpen()).toBe(false)
44
+ })
45
+ expect(router.state.location.pathname).toBe('/')
46
+ })
47
+
48
+ it('ignores legacy ?app= query param (no detail opens)', async () => {
49
+ const { ui } = await given(magazine.full(), { initialRoute: '/?app=jira' })
50
+ await waitFor(() => {
51
+ expect(ui.catalog.getTableData().length).toBeGreaterThan(0)
52
+ })
53
+ expect(ui.catalog.isDetailPanelOpen()).toBe(false)
54
+ })
55
+ })
@@ -32,6 +32,7 @@ import { GalleryTools } from '../tools/GalleryTools'
32
32
  export interface GivenResult {
33
33
  ui: UiTools
34
34
  backend: MockBackendVerifier
35
+ router: ReturnType<typeof createAcRouter>
35
36
  }
36
37
 
37
38
  export interface UiTools {
@@ -57,7 +58,10 @@ export async function cleanupTestResources(): Promise<void> {
57
58
  }
58
59
  }
59
60
 
60
- export async function given(magazine: Magazine): Promise<GivenResult> {
61
+ export async function given(
62
+ magazine: Magazine,
63
+ opts: { initialRoute?: string } = {},
64
+ ): Promise<GivenResult> {
61
65
  // Clean up any previous resources
62
66
  await cleanupTestResources()
63
67
  resetConfigurerCounter()
@@ -126,7 +130,7 @@ export async function given(magazine: Magazine): Promise<GivenResult> {
126
130
 
127
131
  const router = createAcRouter({
128
132
  history: createMemoryHistory({
129
- initialEntries: ['/catalog/apps'],
133
+ initialEntries: [opts.initialRoute ?? '/catalog/apps'],
130
134
  }),
131
135
  context: {
132
136
  queryClient,
@@ -187,5 +191,5 @@ export async function given(magazine: Magazine): Promise<GivenResult> {
187
191
  globalError: () => getGlobalError(),
188
192
  }
189
193
 
190
- return { ui, backend }
194
+ return { ui, backend, router }
191
195
  }
@@ -55,6 +55,8 @@ export interface AppCatalogGridProps {
55
55
  totalAppsCount?: number
56
56
  /** Callback to clear all filters and search */
57
57
  onClearFilters?: () => void
58
+ /** Called when the user closes the detail panel (Esc or X) — e.g. to navigate back to the catalog URL */
59
+ onClosePanel?: () => void
58
60
  }
59
61
 
60
62
  function getIconUrl(iconName: string): string {
@@ -697,6 +699,7 @@ export function AppCatalogGrid({
697
699
  searchQuery,
698
700
  totalAppsCount,
699
701
  onClearFilters,
702
+ onClosePanel,
700
703
  }: AppCatalogGridProps) {
701
704
  const selectedApp = selectedAppSlug
702
705
  ? apps.find((a) => a.slug === selectedAppSlug)
@@ -862,6 +865,7 @@ export function AppCatalogGrid({
862
865
 
863
866
  const handleClosePanel = () => {
864
867
  setHasUserClosed(true)
868
+ onClosePanel?.()
865
869
  }
866
870
 
867
871
  return (
@@ -1,4 +1,5 @@
1
1
  import type { Resource } from '@igstack/app-catalog-backend-core'
2
+ import { useNavigate } from '@tanstack/react-router'
2
3
  import { X } from 'lucide-react'
3
4
  import { useDeferredValue, useEffect, useMemo, useState } from 'react'
4
5
  import { Button } from '~/ui/button'
@@ -13,25 +14,22 @@ import {
13
14
  import { useAppCatalogContext } from '../../context/AppCatalogContext'
14
15
  import { useAppClickHistory } from '../../hooks/useAppClickHistory'
15
16
  import { useAppCounts } from '../../hooks/useAppCounts'
16
- import { useUrlSyncedState } from '../../hooks/useUrlSyncedState'
17
17
  import { searchResources } from '../../utils/searchApps'
18
18
  import { OnboardingCard } from '../components/OnboardingCard'
19
19
  import { useAppCatalogFilters } from '../context/AppCatalogFiltersContext'
20
20
  import { FilterBar } from '../filters/FilterBar'
21
21
  import { AppCatalogGrid } from '../grid/AppCatalogGrid'
22
22
 
23
- export function AppCatalogPage() {
23
+ export function AppCatalogPage({
24
+ selectedSlug,
25
+ }: { selectedSlug?: string } = {}) {
24
26
  const { resources, isLoadingApps, tagsDefinitions } = useAppCatalogContext()
25
27
  const { state: filterState, actions } = useAppCatalogFilters()
26
28
  const { getTopApps } = useAppClickHistory()
29
+ const navigate = useNavigate()
27
30
 
28
- // URL-synced state
29
- const [selectedAppSlug, setSelectedAppSlug] = useUrlSyncedState<
30
- string | undefined
31
- >({
32
- key: 'app',
33
- defaultValue: undefined,
34
- })
31
+ // Selected app comes from the route path (/app/<slug>); undefined = nothing open
32
+ const selectedAppSlug = selectedSlug
35
33
 
36
34
  // Search value from context (URL-synced in AppCatalogFiltersContext)
37
35
  const searchValue = filterState.searchValue
@@ -108,19 +106,23 @@ export function AppCatalogPage() {
108
106
 
109
107
  // Auto-open details when only 1 result
110
108
  useEffect(() => {
111
- if (filteredApps.length === 1 && filteredApps[0]) {
112
- setSelectedAppSlug(filteredApps[0].slug)
109
+ if (filteredApps.length === 1 && filteredApps[0] && !selectedAppSlug) {
110
+ void navigate({
111
+ to: '/app/$slug',
112
+ params: { slug: filteredApps[0].slug },
113
+ replace: true,
114
+ })
113
115
  }
114
- }, [filteredApps, setSelectedAppSlug])
116
+ }, [filteredApps, selectedAppSlug, navigate])
115
117
 
116
118
  const handleAppClick = (app: Resource) => {
117
- setSelectedAppSlug(app.slug)
119
+ void navigate({ to: '/app/$slug', params: { slug: app.slug } })
118
120
  }
119
121
 
120
122
  const handleClearFilters = () => {
121
123
  setSearchValue('')
122
124
  actions.clearAllFilters()
123
- setSelectedAppSlug(undefined)
125
+ void navigate({ to: '/' })
124
126
  }
125
127
 
126
128
  // Calculate total apps count (respecting showDeprecated setting)
@@ -174,7 +176,7 @@ export function AppCatalogPage() {
174
176
  variant="outline"
175
177
  onClick={() => {
176
178
  setSearchValue('')
177
- setSelectedAppSlug(undefined)
179
+ void navigate({ to: '/' })
178
180
  }}
179
181
  className="gap-2"
180
182
  >
@@ -190,6 +192,7 @@ export function AppCatalogPage() {
190
192
  selectedAppSlug={selectedAppSlug}
191
193
  groupingDefinition={groupingDefinition}
192
194
  onAppClick={handleAppClick}
195
+ onClosePanel={() => void navigate({ to: '/' })}
193
196
  hasSearch={!!deferredSearchValue}
194
197
  searchQuery={deferredSearchValue}
195
198
  totalAppsCount={totalAppsCount}