@igstack/app-catalog-frontend-core 0.0.1 → 0.1.1-alpha-20260302025010
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/App.d.ts +1 -1
- package/dist/esm/App.js.map +1 -1
- package/dist/esm/api/infra/createQueryClient.d.ts +1 -1
- package/dist/esm/api/infra/createQueryClient.js.map +1 -1
- package/dist/esm/appPropsFactory.js.map +1 -1
- package/dist/esm/modules/appCatalog/ui/pages/AppCatalogPage.js +10 -14
- package/dist/esm/modules/appCatalog/ui/pages/AppCatalogPage.js.map +1 -1
- package/dist/esm/modules/appCatalog/utils/searchApps.d.ts +10 -0
- package/dist/esm/modules/appCatalog/utils/searchApps.js +48 -0
- package/dist/esm/modules/appCatalog/utils/searchApps.js.map +1 -0
- package/dist/esm/util/createEhRouter.d.ts +2 -2
- package/dist/esm/util/createEhRouter.js.map +1 -1
- package/package.json +3 -3
- package/src/App.tsx +2 -2
- package/src/api/infra/createQueryClient.ts +1 -1
- package/src/appPropsFactory.ts +2 -2
- package/src/modules/appCatalog/ui/pages/AppCatalogPage.tsx +12 -25
- package/src/modules/appCatalog/utils/searchApps.ts +83 -0
- package/src/util/createEhRouter.tsx +2 -2
package/dist/esm/App.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { QueryClient } from '@tanstack/react-query';
|
|
|
2
2
|
import { TRPCRouter } from '@igstack/app-catalog-backend-core';
|
|
3
3
|
import { TRPCClient } from '@trpc/client';
|
|
4
4
|
import { AcDb } from './userDb/AcDb.js';
|
|
5
|
-
import { createEhRouter } from '
|
|
5
|
+
import { createEhRouter } from './util/createEhRouter.js';
|
|
6
6
|
export interface AppProps {
|
|
7
7
|
router: ReturnType<typeof createEhRouter>;
|
|
8
8
|
queryClient: QueryClient;
|
package/dist/esm/App.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"App.js","sources":["../../src/App.tsx"],"sourcesContent":["import { QueryClientProvider } from '@tanstack/react-query'\nimport { RouterProvider } from '@tanstack/react-router'\nimport { DbProvider } from './userDb/DbContext'\nimport type { QueryClient } from '@tanstack/react-query'\nimport type { TRPCRouter } from '@igstack/app-catalog-backend-core'\nimport type { TRPCClient } from '@trpc/client'\nimport type { AcDb } from './userDb/AcDb'\nimport type { createEhRouter } from '
|
|
1
|
+
{"version":3,"file":"App.js","sources":["../../src/App.tsx"],"sourcesContent":["import { QueryClientProvider } from '@tanstack/react-query'\nimport { RouterProvider } from '@tanstack/react-router'\nimport { DbProvider } from './userDb/DbContext'\nimport type { QueryClient } from '@tanstack/react-query'\nimport type { TRPCRouter } from '@igstack/app-catalog-backend-core'\nimport type { TRPCClient } from '@trpc/client'\nimport type { AcDb } from './userDb/AcDb'\nimport type { createEhRouter } from './util/createEhRouter'\nimport { TRPCProvider } from './api/infra/trpc'\n\nexport interface AppProps {\n router: ReturnType<typeof createEhRouter>\n queryClient: QueryClient\n trpcClient: TRPCClient<TRPCRouter>\n db: AcDb\n}\n\nexport function App({ router, queryClient, trpcClient, db }: AppProps) {\n return (\n <QueryClientProvider client={queryClient}>\n <TRPCProvider queryClient={queryClient} trpcClient={trpcClient}>\n <DbProvider db={db}>\n <RouterProvider router={router} />\n </DbProvider>\n </TRPCProvider>\n </QueryClientProvider>\n )\n}\n"],"names":[],"mappings":";;;;;AAiBO,SAAS,IAAI,EAAE,QAAQ,aAAa,YAAY,MAAgB;AACrE,6BACG,qBAAA,EAAoB,QAAQ,aAC3B,UAAA,oBAAC,gBAAa,aAA0B,YACtC,UAAA,oBAAC,YAAA,EAAW,IACV,UAAA,oBAAC,gBAAA,EAAe,OAAA,CAAgB,GAClC,GACF,EAAA,CACF;AAEJ;"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { QueryClient } from '@tanstack/react-query';
|
|
2
2
|
import { TRPCRouter } from '@igstack/app-catalog-backend-core';
|
|
3
3
|
import { TRPCClient } from '@trpc/client';
|
|
4
|
-
import { AcDb } from '
|
|
4
|
+
import { AcDb } from '../../userDb/AcDb.js';
|
|
5
5
|
export interface CreateQueryParams {
|
|
6
6
|
trpcClient: TRPCClient<TRPCRouter>;
|
|
7
7
|
db: AcDb;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"createQueryClient.js","sources":["../../../../src/api/infra/createQueryClient.ts"],"sourcesContent":["import { QueryClient } from '@tanstack/react-query'\nimport type { TRPCRouter } from '@igstack/app-catalog-backend-core'\nimport type { TRPCClient } from '@trpc/client'\nimport type { AcDb } from '
|
|
1
|
+
{"version":3,"file":"createQueryClient.js","sources":["../../../../src/api/infra/createQueryClient.ts"],"sourcesContent":["import { QueryClient } from '@tanstack/react-query'\nimport type { TRPCRouter } from '@igstack/app-catalog-backend-core'\nimport type { TRPCClient } from '@trpc/client'\nimport type { AcDb } from '../../userDb/AcDb'\n\nexport interface CreateQueryParams {\n trpcClient: TRPCClient<TRPCRouter>\n db: AcDb\n}\n\nexport function createQueryClient({ trpcClient, db }: CreateQueryParams) {\n return new QueryClient({\n defaultOptions: {\n queries: {\n gcTime: 1000 * 60 * 60 * 24 * 7, // 7 days\n meta: {\n trpcClient,\n db,\n },\n },\n mutations: {\n meta: {\n trpcClient,\n db,\n },\n },\n },\n })\n}\n"],"names":[],"mappings":";AAUO,SAAS,kBAAkB,EAAE,YAAY,MAAyB;AACvE,SAAO,IAAI,YAAY;AAAA,IACrB,gBAAgB;AAAA,MACd,SAAS;AAAA,QACP,QAAQ,MAAO,KAAK,KAAK,KAAK;AAAA;AAAA,QAC9B,MAAM;AAAA,UACJ;AAAA,UACA;AAAA,QAAA;AAAA,MACF;AAAA,MAEF,WAAW;AAAA,QACT,MAAM;AAAA,UACJ;AAAA,UACA;AAAA,QAAA;AAAA,MACF;AAAA,IACF;AAAA,EACF,CACD;AACH;"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"appPropsFactory.js","sources":["../../src/appPropsFactory.ts"],"sourcesContent":["import { createBrowserHistory } from '@tanstack/react-router'\nimport { createTRPCClient, httpBatchLink } from '@trpc/client'\nimport { AcDb } from './userDb/AcDb'\nimport type { TRPCRouter } from '@igstack/app-catalog-backend-core'\nimport type { AppProps } from './App'\nimport type { EhPlugin } from './modules/pluginCore/types'\nimport { createQueryClient } from '
|
|
1
|
+
{"version":3,"file":"appPropsFactory.js","sources":["../../src/appPropsFactory.ts"],"sourcesContent":["import { createBrowserHistory } from '@tanstack/react-router'\nimport { createTRPCClient, httpBatchLink } from '@trpc/client'\nimport { AcDb } from './userDb/AcDb'\nimport type { TRPCRouter } from '@igstack/app-catalog-backend-core'\nimport type { AppProps } from './App'\nimport type { EhPlugin } from './modules/pluginCore/types'\nimport { createQueryClient } from './api/infra/createQueryClient'\nimport { createEhRouter } from './util/createEhRouter'\n\n// registerSW();\nexport function appPropsFactory(): AppProps {\n const trpcClient = createTRPCClient<TRPCRouter>({\n links: [\n httpBatchLink({\n url: `${window.location.origin}/api/trpc`,\n }),\n ],\n })\n\n const db = new AcDb()\n const queryClient = createQueryClient({ trpcClient, db })\n const plugins: Array<EhPlugin> = [\n // Future plugins can be added here\n ]\n const router = createEhRouter({\n history: createBrowserHistory(),\n context: {\n queryClient,\n trpcClient,\n db,\n plugins,\n boostrapHealth: {},\n },\n })\n\n return { router, queryClient, trpcClient, db }\n}\n"],"names":[],"mappings":";;;;;AAUO,SAAS,kBAA4B;AAC1C,QAAM,aAAa,iBAA6B;AAAA,IAC9C,OAAO;AAAA,MACL,cAAc;AAAA,QACZ,KAAK,GAAG,OAAO,SAAS,MAAM;AAAA,MAAA,CAC/B;AAAA,IAAA;AAAA,EACH,CACD;AAED,QAAM,KAAK,IAAI,KAAA;AACf,QAAM,cAAc,kBAAkB,EAAE,YAAY,IAAI;AACxD,QAAM,UAA2B;AAAA;AAAA,EAAA;AAGjC,QAAM,SAAS,eAAe;AAAA,IAC5B,SAAS,qBAAA;AAAA,IACT,SAAS;AAAA,MACP;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA,gBAAgB,CAAA;AAAA,IAAC;AAAA,EACnB,CACD;AAED,SAAO,EAAE,QAAQ,aAAa,YAAY,GAAA;AAC5C;"}
|
|
@@ -3,6 +3,7 @@ import { useNavigate, useRouter, useSearch } from "@tanstack/react-router";
|
|
|
3
3
|
import { useState, useRef, useEffect, useMemo } from "react";
|
|
4
4
|
import { useAppCatalogContext } from "../../context/AppCatalogContext.js";
|
|
5
5
|
import { AppCatalogGrid } from "../grid/AppCatalogGrid.js";
|
|
6
|
+
import { searchApps } from "../../utils/searchApps.js";
|
|
6
7
|
import { Input } from "../../../../ui/input.js";
|
|
7
8
|
function AppCatalogPage() {
|
|
8
9
|
const navigate = useNavigate();
|
|
@@ -53,21 +54,16 @@ function AppCatalogPage() {
|
|
|
53
54
|
});
|
|
54
55
|
}, [searchValue, navigate, router.state.location.pathname, search]);
|
|
55
56
|
const filteredApps = useMemo(() => {
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
57
|
+
const searchedApps = searchApps(apps, searchValue);
|
|
58
|
+
if (filterTag === void 0) {
|
|
59
|
+
return searchedApps;
|
|
60
|
+
}
|
|
61
|
+
return searchedApps.filter(
|
|
62
|
+
(app) => {
|
|
63
|
+
var _a;
|
|
64
|
+
return (_a = app.tags) == null ? void 0 : _a.some((tag) => tag.toLowerCase() === filterTag.toLowerCase());
|
|
61
65
|
}
|
|
62
|
-
|
|
63
|
-
const slug = app.slug.toLowerCase() || "";
|
|
64
|
-
const description = ((_a = app.description) == null ? void 0 : _a.toLowerCase()) || "";
|
|
65
|
-
const tags = ((_b = app.tags) == null ? void 0 : _b.join(" ").toLowerCase()) || "";
|
|
66
|
-
return name.includes(normalizedSearchValue) || slug.includes(normalizedSearchValue) || description.includes(normalizedSearchValue) || tags.includes(normalizedSearchValue);
|
|
67
|
-
}).filter((app) => {
|
|
68
|
-
var _a;
|
|
69
|
-
return filterTag === void 0 || ((_a = app.tags) == null ? void 0 : _a.some((tag) => tag.toLowerCase() === filterTag.toLowerCase()));
|
|
70
|
-
});
|
|
66
|
+
);
|
|
71
67
|
}, [apps, searchValue, filterTag]);
|
|
72
68
|
const handleAppClick = (app) => {
|
|
73
69
|
setSelectedAppSlug(app.slug);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AppCatalogPage.js","sources":["../../../../../../src/modules/appCatalog/ui/pages/AppCatalogPage.tsx"],"sourcesContent":["import { useNavigate, useRouter, useSearch } from '@tanstack/react-router'\nimport { useEffect, useMemo, useRef, useState } from 'react'\n\nimport type { AppForCatalog } from '@igstack/app-catalog-backend-core'\nimport { useAppCatalogContext } from '../../context/AppCatalogContext'\nimport { AppCatalogGrid } from '../grid/AppCatalogGrid'\n\nimport { Input } from '~/ui/input'\n\nexport function AppCatalogPage() {\n const navigate = useNavigate()\n const router = useRouter()\n const search = useSearch({ strict: false })\n const { apps, isLoadingApps, tagsDefinitions } = useAppCatalogContext()\n const [searchValue, setSearchValue] = useState('')\n\n // Local state for app selection (source of truth)\n const [selectedAppSlug, setSelectedAppSlug] = useState<string | undefined>()\n\n const filterTag = search.filterTag\n\n // Initialize from URL on mount (once only)\n const isInitializedRef = useRef(false)\n useEffect(() => {\n if (!isInitializedRef.current) {\n if (search.app) {\n setSelectedAppSlug(search.app)\n }\n if (search.q) {\n setSearchValue(search.q)\n }\n isInitializedRef.current = true\n }\n }, [search.app, search.q])\n\n // Sync app selection state to URL (async side effect)\n useEffect(() => {\n // Don't sync until after initialization\n if (!isInitializedRef.current) return\n if (selectedAppSlug === search.app) return // Already in sync\n\n const currentPath = router.state.location.pathname\n navigate({\n to: currentPath,\n search: { ...search, app: selectedAppSlug },\n replace: true, // Use replace to avoid polluting history\n })\n }, [selectedAppSlug, navigate, router.state.location.pathname, search])\n\n // Sync search value state to URL (async side effect)\n useEffect(() => {\n // Don't sync until after initialization\n if (!isInitializedRef.current) return\n\n const normalizedSearchValue = searchValue.trim()\n const urlSearchValue = search.q || ''\n\n if (normalizedSearchValue === urlSearchValue) return // Already in sync\n\n const currentPath = router.state.location.pathname\n navigate({\n to: currentPath,\n search: {\n ...search,\n q: normalizedSearchValue || undefined, // Remove param if empty\n },\n replace: true, // Use replace to avoid polluting history\n })\n }, [searchValue, navigate, router.state.location.pathname, search])\n\n const filteredApps = useMemo(() => {\n
|
|
1
|
+
{"version":3,"file":"AppCatalogPage.js","sources":["../../../../../../src/modules/appCatalog/ui/pages/AppCatalogPage.tsx"],"sourcesContent":["import { useNavigate, useRouter, useSearch } from '@tanstack/react-router'\nimport { useEffect, useMemo, useRef, useState } from 'react'\n\nimport type { AppForCatalog } from '@igstack/app-catalog-backend-core'\nimport { useAppCatalogContext } from '../../context/AppCatalogContext'\nimport { AppCatalogGrid } from '../grid/AppCatalogGrid'\nimport { searchApps } from '../../utils/searchApps'\n\nimport { Input } from '~/ui/input'\n\nexport function AppCatalogPage() {\n const navigate = useNavigate()\n const router = useRouter()\n const search = useSearch({ strict: false })\n const { apps, isLoadingApps, tagsDefinitions } = useAppCatalogContext()\n const [searchValue, setSearchValue] = useState('')\n\n // Local state for app selection (source of truth)\n const [selectedAppSlug, setSelectedAppSlug] = useState<string | undefined>()\n\n const filterTag = search.filterTag\n\n // Initialize from URL on mount (once only)\n const isInitializedRef = useRef(false)\n useEffect(() => {\n if (!isInitializedRef.current) {\n if (search.app) {\n setSelectedAppSlug(search.app)\n }\n if (search.q) {\n setSearchValue(search.q)\n }\n isInitializedRef.current = true\n }\n }, [search.app, search.q])\n\n // Sync app selection state to URL (async side effect)\n useEffect(() => {\n // Don't sync until after initialization\n if (!isInitializedRef.current) return\n if (selectedAppSlug === search.app) return // Already in sync\n\n const currentPath = router.state.location.pathname\n navigate({\n to: currentPath,\n search: { ...search, app: selectedAppSlug },\n replace: true, // Use replace to avoid polluting history\n })\n }, [selectedAppSlug, navigate, router.state.location.pathname, search])\n\n // Sync search value state to URL (async side effect)\n useEffect(() => {\n // Don't sync until after initialization\n if (!isInitializedRef.current) return\n\n const normalizedSearchValue = searchValue.trim()\n const urlSearchValue = search.q || ''\n\n if (normalizedSearchValue === urlSearchValue) return // Already in sync\n\n const currentPath = router.state.location.pathname\n navigate({\n to: currentPath,\n search: {\n ...search,\n q: normalizedSearchValue || undefined, // Remove param if empty\n },\n replace: true, // Use replace to avoid polluting history\n })\n }, [searchValue, navigate, router.state.location.pathname, search])\n\n const filteredApps = useMemo(() => {\n // First apply search with smart sorting\n const searchedApps = searchApps(apps, searchValue)\n\n // Then apply tag filter if present\n if (filterTag === undefined) {\n return searchedApps\n }\n\n return searchedApps.filter((app) =>\n app.tags?.some((tag) => tag.toLowerCase() === filterTag.toLowerCase()),\n )\n }, [apps, searchValue, filterTag])\n\n const handleAppClick = (app: AppForCatalog) => {\n setSelectedAppSlug(app.slug)\n }\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=\"pb-4 shrink-0\">\n <div className=\"flex items-start justify-between gap-4 pb-4\">\n <div>\n <div className=\"font-medium text-2xl\">App Catalog</div>\n <div className=\"text-sm text-muted-foreground\">\n {filteredApps.length} apps available\n </div>\n </div>\n </div>\n\n <div className=\"w-full\">\n <Input\n value={searchValue}\n onChange={(e) => setSearchValue(e.target.value)}\n placeholder=\"Search apps by name, description, or tags…\"\n aria-label=\"Search apps\"\n />\n </div>\n </div>\n\n <div className=\"flex-1 min-h-0\">\n <AppCatalogGrid\n apps={filteredApps}\n selectedAppSlug={selectedAppSlug}\n groupingDefinition={groupingDefinition}\n onAppClick={handleAppClick}\n />\n </div>\n </div>\n )\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { AppForCatalog } from '@igstack/app-catalog-backend-core';
|
|
2
|
+
/**
|
|
3
|
+
* Search and sort apps by relevance.
|
|
4
|
+
* Prioritizes prefix matches in display name over other matches.
|
|
5
|
+
*
|
|
6
|
+
* @param apps - Array of apps to search
|
|
7
|
+
* @param searchQuery - Search query string
|
|
8
|
+
* @returns Filtered and sorted array of apps
|
|
9
|
+
*/
|
|
10
|
+
export declare function searchApps(apps: Array<AppForCatalog>, searchQuery: string): Array<AppForCatalog>;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
function searchApps(apps, searchQuery) {
|
|
2
|
+
const normalizedQuery = searchQuery.trim().toLowerCase();
|
|
3
|
+
if (normalizedQuery === "") {
|
|
4
|
+
return apps;
|
|
5
|
+
}
|
|
6
|
+
const scoredApps = apps.map((app) => {
|
|
7
|
+
var _a, _b;
|
|
8
|
+
const name = app.displayName.toLowerCase();
|
|
9
|
+
const slug = app.slug.toLowerCase();
|
|
10
|
+
const description = ((_a = app.description) == null ? void 0 : _a.toLowerCase()) || "";
|
|
11
|
+
const tags = ((_b = app.tags) == null ? void 0 : _b.join(" ").toLowerCase()) || "";
|
|
12
|
+
const nameMatch = name.includes(normalizedQuery);
|
|
13
|
+
const slugMatch = slug.includes(normalizedQuery);
|
|
14
|
+
const descriptionMatch = description.includes(normalizedQuery);
|
|
15
|
+
const tagsMatch = tags.includes(normalizedQuery);
|
|
16
|
+
if (!nameMatch && !slugMatch && !descriptionMatch && !tagsMatch) {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
let score = 0;
|
|
20
|
+
if (name.startsWith(normalizedQuery)) {
|
|
21
|
+
score = 0;
|
|
22
|
+
} else if (slug.startsWith(normalizedQuery)) {
|
|
23
|
+
score = 1;
|
|
24
|
+
} else if (nameMatch) {
|
|
25
|
+
score = 2;
|
|
26
|
+
} else if (slugMatch) {
|
|
27
|
+
score = 3;
|
|
28
|
+
} else if (tagsMatch) {
|
|
29
|
+
score = 4;
|
|
30
|
+
} else if (descriptionMatch) {
|
|
31
|
+
score = 5;
|
|
32
|
+
}
|
|
33
|
+
return { app, score };
|
|
34
|
+
}).filter(
|
|
35
|
+
(item) => item !== null
|
|
36
|
+
);
|
|
37
|
+
scoredApps.sort((a, b) => {
|
|
38
|
+
if (a.score !== b.score) {
|
|
39
|
+
return a.score - b.score;
|
|
40
|
+
}
|
|
41
|
+
return a.app.displayName.localeCompare(b.app.displayName);
|
|
42
|
+
});
|
|
43
|
+
return scoredApps.map((item) => item.app);
|
|
44
|
+
}
|
|
45
|
+
export {
|
|
46
|
+
searchApps
|
|
47
|
+
};
|
|
48
|
+
//# sourceMappingURL=searchApps.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"searchApps.js","sources":["../../../../../src/modules/appCatalog/utils/searchApps.ts"],"sourcesContent":["import type { AppForCatalog } from '@igstack/app-catalog-backend-core'\n\n/**\n * Search and sort apps by relevance.\n * Prioritizes prefix matches in display name over other matches.\n *\n * @param apps - Array of apps to search\n * @param searchQuery - Search query string\n * @returns Filtered and sorted array of apps\n */\nexport function searchApps(\n apps: Array<AppForCatalog>,\n searchQuery: string,\n): Array<AppForCatalog> {\n const normalizedQuery = searchQuery.trim().toLowerCase()\n\n if (normalizedQuery === '') {\n return apps\n }\n\n // Filter and score apps\n const scoredApps = apps\n .map((app) => {\n const name = app.displayName.toLowerCase()\n const slug = app.slug.toLowerCase()\n const description = app.description?.toLowerCase() || ''\n const tags = app.tags?.join(' ').toLowerCase() || ''\n\n // Check if any field matches\n const nameMatch = name.includes(normalizedQuery)\n const slugMatch = slug.includes(normalizedQuery)\n const descriptionMatch = description.includes(normalizedQuery)\n const tagsMatch = tags.includes(normalizedQuery)\n\n if (!nameMatch && !slugMatch && !descriptionMatch && !tagsMatch) {\n return null\n }\n\n // Calculate score (lower is better)\n let score = 0\n\n // Highest priority: prefix match in display name\n if (name.startsWith(normalizedQuery)) {\n score = 0\n }\n // Second priority: prefix match in slug\n else if (slug.startsWith(normalizedQuery)) {\n score = 1\n }\n // Third priority: anywhere in display name\n else if (nameMatch) {\n score = 2\n }\n // Fourth priority: anywhere in slug\n else if (slugMatch) {\n score = 3\n }\n // Fifth priority: tags\n else if (tagsMatch) {\n score = 4\n }\n // Lowest priority: description\n else if (descriptionMatch) {\n score = 5\n }\n\n return { app, score }\n })\n .filter(\n (item): item is { app: AppForCatalog; score: number } => item !== null,\n )\n\n // Sort by score (ascending - lower score = higher priority)\n scoredApps.sort((a, b) => {\n if (a.score !== b.score) {\n return a.score - b.score\n }\n // If same score, sort alphabetically by display name\n return a.app.displayName.localeCompare(b.app.displayName)\n })\n\n return scoredApps.map((item) => item.app)\n}\n"],"names":[],"mappings":"AAUO,SAAS,WACd,MACA,aACsB;AACtB,QAAM,kBAAkB,YAAY,KAAA,EAAO,YAAA;AAE3C,MAAI,oBAAoB,IAAI;AAC1B,WAAO;AAAA,EACT;AAGA,QAAM,aAAa,KAChB,IAAI,CAAC,QAAQ;AAZX;AAaD,UAAM,OAAO,IAAI,YAAY,YAAA;AAC7B,UAAM,OAAO,IAAI,KAAK,YAAA;AACtB,UAAM,gBAAc,SAAI,gBAAJ,mBAAiB,kBAAiB;AACtD,UAAM,SAAO,SAAI,SAAJ,mBAAU,KAAK,KAAK,kBAAiB;AAGlD,UAAM,YAAY,KAAK,SAAS,eAAe;AAC/C,UAAM,YAAY,KAAK,SAAS,eAAe;AAC/C,UAAM,mBAAmB,YAAY,SAAS,eAAe;AAC7D,UAAM,YAAY,KAAK,SAAS,eAAe;AAE/C,QAAI,CAAC,aAAa,CAAC,aAAa,CAAC,oBAAoB,CAAC,WAAW;AAC/D,aAAO;AAAA,IACT;AAGA,QAAI,QAAQ;AAGZ,QAAI,KAAK,WAAW,eAAe,GAAG;AACpC,cAAQ;AAAA,IACV,WAES,KAAK,WAAW,eAAe,GAAG;AACzC,cAAQ;AAAA,IACV,WAES,WAAW;AAClB,cAAQ;AAAA,IACV,WAES,WAAW;AAClB,cAAQ;AAAA,IACV,WAES,WAAW;AAClB,cAAQ;AAAA,IACV,WAES,kBAAkB;AACzB,cAAQ;AAAA,IACV;AAEA,WAAO,EAAE,KAAK,MAAA;AAAA,EAChB,CAAC,EACA;AAAA,IACC,CAAC,SAAwD,SAAS;AAAA,EAAA;AAItE,aAAW,KAAK,CAAC,GAAG,MAAM;AACxB,QAAI,EAAE,UAAU,EAAE,OAAO;AACvB,aAAO,EAAE,QAAQ,EAAE;AAAA,IACrB;AAEA,WAAO,EAAE,IAAI,YAAY,cAAc,EAAE,IAAI,WAAW;AAAA,EAC1D,CAAC;AAED,SAAO,WAAW,IAAI,CAAC,SAAS,KAAK,GAAG;AAC1C;"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { EhRouterInitParams } from '
|
|
2
|
-
export declare function createEhRouter({ context, history }: EhRouterInitParams): import('@tanstack/router-core').RouterCore<import('@tanstack/router-core').Route<import('@tanstack/react-router').Register, any, "/", "/", string, "__root__", undefined, {}, import('
|
|
1
|
+
import { EhRouterInitParams } from '../types/types.js';
|
|
2
|
+
export declare function createEhRouter({ context, history }: EhRouterInitParams): import('@tanstack/router-core').RouterCore<import('@tanstack/router-core').Route<import('@tanstack/react-router').Register, any, "/", "/", string, "__root__", undefined, {}, import('../types/types.js').EhRouterContext, import('@tanstack/router-core').AnyContext, import('@tanstack/router-core').AnyContext, {}, undefined, import('../routeTree.gen').RootRouteChildren, import('../routeTree.gen').FileRouteTypes, unknown, unknown, undefined>, "never", false, import('@tanstack/history').RouterHistory, Record<string, any>>;
|
|
3
3
|
declare module '@tanstack/react-router' {
|
|
4
4
|
interface Register {
|
|
5
5
|
router: ReturnType<typeof createEhRouter>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"createEhRouter.js","sources":["../../../src/util/createEhRouter.tsx"],"sourcesContent":["import { createRouter } from '@tanstack/react-router'\nimport type { EhRouterInitParams } from '
|
|
1
|
+
{"version":3,"file":"createEhRouter.js","sources":["../../../src/util/createEhRouter.tsx"],"sourcesContent":["import { createRouter } from '@tanstack/react-router'\nimport type { EhRouterInitParams } from '../types/types'\nimport { routeTree } from '../routeTree.gen'\n\nexport function createEhRouter({ context, history }: EhRouterInitParams) {\n return createRouter({\n routeTree,\n context,\n history,\n // Since we're using React Query, we don't want loader calls to ever be stale\n // This will ensure that the loader is always called when the route is preloaded or visited\n defaultPreloadStaleTime: 0,\n defaultNotFoundComponent: () => <div>404 - Page Not Found</div>,\n pathParamsAllowedCharacters: ['@'],\n })\n}\n\ndeclare module '@tanstack/react-router' {\n interface Register {\n router: ReturnType<typeof createEhRouter>\n }\n}\n"],"names":[],"mappings":";;;AAIO,SAAS,eAAe,EAAE,SAAS,WAA+B;AACvE,SAAO,aAAa;AAAA,IAClB;AAAA,IACA;AAAA,IACA;AAAA;AAAA;AAAA,IAGA,yBAAyB;AAAA,IACzB,0BAA0B,MAAM,oBAAC,OAAA,EAAI,UAAA,uBAAA,CAAoB;AAAA,IACzD,6BAA6B,CAAC,GAAG;AAAA,EAAA,CAClC;AACH;"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@igstack/app-catalog-frontend-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.1.1-alpha-20260302025010",
|
|
4
4
|
"description": "Frontend core library for App Catalog",
|
|
5
5
|
"homepage": "https://github.com/lislon/app-catalog",
|
|
6
6
|
"repository": {
|
|
@@ -133,8 +133,8 @@
|
|
|
133
133
|
"vite-plugin-static-copy": "^3.1.4",
|
|
134
134
|
"vite-plugin-svgr": "^4.2.0",
|
|
135
135
|
"vitest": "3.2.2",
|
|
136
|
-
"@igstack/app-catalog-backend-core": "0.
|
|
137
|
-
"@igstack/app-catalog-shared-core": "0.
|
|
136
|
+
"@igstack/app-catalog-backend-core": "0.1.1-alpha-20260302025010",
|
|
137
|
+
"@igstack/app-catalog-shared-core": "0.1.1-alpha-20260302025010"
|
|
138
138
|
},
|
|
139
139
|
"peerDependencies": {
|
|
140
140
|
"react": "19.1.2",
|
package/src/App.tsx
CHANGED
|
@@ -5,8 +5,8 @@ import type { QueryClient } from '@tanstack/react-query'
|
|
|
5
5
|
import type { TRPCRouter } from '@igstack/app-catalog-backend-core'
|
|
6
6
|
import type { TRPCClient } from '@trpc/client'
|
|
7
7
|
import type { AcDb } from './userDb/AcDb'
|
|
8
|
-
import type { createEhRouter } from '
|
|
9
|
-
import { TRPCProvider } from '
|
|
8
|
+
import type { createEhRouter } from './util/createEhRouter'
|
|
9
|
+
import { TRPCProvider } from './api/infra/trpc'
|
|
10
10
|
|
|
11
11
|
export interface AppProps {
|
|
12
12
|
router: ReturnType<typeof createEhRouter>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { QueryClient } from '@tanstack/react-query'
|
|
2
2
|
import type { TRPCRouter } from '@igstack/app-catalog-backend-core'
|
|
3
3
|
import type { TRPCClient } from '@trpc/client'
|
|
4
|
-
import type { AcDb } from '
|
|
4
|
+
import type { AcDb } from '../../userDb/AcDb'
|
|
5
5
|
|
|
6
6
|
export interface CreateQueryParams {
|
|
7
7
|
trpcClient: TRPCClient<TRPCRouter>
|
package/src/appPropsFactory.ts
CHANGED
|
@@ -4,8 +4,8 @@ import { AcDb } from './userDb/AcDb'
|
|
|
4
4
|
import type { TRPCRouter } from '@igstack/app-catalog-backend-core'
|
|
5
5
|
import type { AppProps } from './App'
|
|
6
6
|
import type { EhPlugin } from './modules/pluginCore/types'
|
|
7
|
-
import { createQueryClient } from '
|
|
8
|
-
import { createEhRouter } from '
|
|
7
|
+
import { createQueryClient } from './api/infra/createQueryClient'
|
|
8
|
+
import { createEhRouter } from './util/createEhRouter'
|
|
9
9
|
|
|
10
10
|
// registerSW();
|
|
11
11
|
export function appPropsFactory(): AppProps {
|
|
@@ -4,6 +4,7 @@ import { useEffect, useMemo, useRef, useState } from 'react'
|
|
|
4
4
|
import type { AppForCatalog } from '@igstack/app-catalog-backend-core'
|
|
5
5
|
import { useAppCatalogContext } from '../../context/AppCatalogContext'
|
|
6
6
|
import { AppCatalogGrid } from '../grid/AppCatalogGrid'
|
|
7
|
+
import { searchApps } from '../../utils/searchApps'
|
|
7
8
|
|
|
8
9
|
import { Input } from '~/ui/input'
|
|
9
10
|
|
|
@@ -69,31 +70,17 @@ export function AppCatalogPage() {
|
|
|
69
70
|
}, [searchValue, navigate, router.state.location.pathname, search])
|
|
70
71
|
|
|
71
72
|
const filteredApps = useMemo(() => {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
return (
|
|
85
|
-
name.includes(normalizedSearchValue) ||
|
|
86
|
-
slug.includes(normalizedSearchValue) ||
|
|
87
|
-
description.includes(normalizedSearchValue) ||
|
|
88
|
-
tags.includes(normalizedSearchValue)
|
|
89
|
-
)
|
|
90
|
-
})
|
|
91
|
-
.filter((app) => {
|
|
92
|
-
return (
|
|
93
|
-
filterTag === undefined ||
|
|
94
|
-
app.tags?.some((tag) => tag.toLowerCase() === filterTag.toLowerCase())
|
|
95
|
-
)
|
|
96
|
-
})
|
|
73
|
+
// First apply search with smart sorting
|
|
74
|
+
const searchedApps = searchApps(apps, searchValue)
|
|
75
|
+
|
|
76
|
+
// Then apply tag filter if present
|
|
77
|
+
if (filterTag === undefined) {
|
|
78
|
+
return searchedApps
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return searchedApps.filter((app) =>
|
|
82
|
+
app.tags?.some((tag) => tag.toLowerCase() === filterTag.toLowerCase()),
|
|
83
|
+
)
|
|
97
84
|
}, [apps, searchValue, filterTag])
|
|
98
85
|
|
|
99
86
|
const handleAppClick = (app: AppForCatalog) => {
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import type { AppForCatalog } from '@igstack/app-catalog-backend-core'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Search and sort apps by relevance.
|
|
5
|
+
* Prioritizes prefix matches in display name over other matches.
|
|
6
|
+
*
|
|
7
|
+
* @param apps - Array of apps to search
|
|
8
|
+
* @param searchQuery - Search query string
|
|
9
|
+
* @returns Filtered and sorted array of apps
|
|
10
|
+
*/
|
|
11
|
+
export function searchApps(
|
|
12
|
+
apps: Array<AppForCatalog>,
|
|
13
|
+
searchQuery: string,
|
|
14
|
+
): Array<AppForCatalog> {
|
|
15
|
+
const normalizedQuery = searchQuery.trim().toLowerCase()
|
|
16
|
+
|
|
17
|
+
if (normalizedQuery === '') {
|
|
18
|
+
return apps
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Filter and score apps
|
|
22
|
+
const scoredApps = apps
|
|
23
|
+
.map((app) => {
|
|
24
|
+
const name = app.displayName.toLowerCase()
|
|
25
|
+
const slug = app.slug.toLowerCase()
|
|
26
|
+
const description = app.description?.toLowerCase() || ''
|
|
27
|
+
const tags = app.tags?.join(' ').toLowerCase() || ''
|
|
28
|
+
|
|
29
|
+
// Check if any field matches
|
|
30
|
+
const nameMatch = name.includes(normalizedQuery)
|
|
31
|
+
const slugMatch = slug.includes(normalizedQuery)
|
|
32
|
+
const descriptionMatch = description.includes(normalizedQuery)
|
|
33
|
+
const tagsMatch = tags.includes(normalizedQuery)
|
|
34
|
+
|
|
35
|
+
if (!nameMatch && !slugMatch && !descriptionMatch && !tagsMatch) {
|
|
36
|
+
return null
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Calculate score (lower is better)
|
|
40
|
+
let score = 0
|
|
41
|
+
|
|
42
|
+
// Highest priority: prefix match in display name
|
|
43
|
+
if (name.startsWith(normalizedQuery)) {
|
|
44
|
+
score = 0
|
|
45
|
+
}
|
|
46
|
+
// Second priority: prefix match in slug
|
|
47
|
+
else if (slug.startsWith(normalizedQuery)) {
|
|
48
|
+
score = 1
|
|
49
|
+
}
|
|
50
|
+
// Third priority: anywhere in display name
|
|
51
|
+
else if (nameMatch) {
|
|
52
|
+
score = 2
|
|
53
|
+
}
|
|
54
|
+
// Fourth priority: anywhere in slug
|
|
55
|
+
else if (slugMatch) {
|
|
56
|
+
score = 3
|
|
57
|
+
}
|
|
58
|
+
// Fifth priority: tags
|
|
59
|
+
else if (tagsMatch) {
|
|
60
|
+
score = 4
|
|
61
|
+
}
|
|
62
|
+
// Lowest priority: description
|
|
63
|
+
else if (descriptionMatch) {
|
|
64
|
+
score = 5
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return { app, score }
|
|
68
|
+
})
|
|
69
|
+
.filter(
|
|
70
|
+
(item): item is { app: AppForCatalog; score: number } => item !== null,
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
// Sort by score (ascending - lower score = higher priority)
|
|
74
|
+
scoredApps.sort((a, b) => {
|
|
75
|
+
if (a.score !== b.score) {
|
|
76
|
+
return a.score - b.score
|
|
77
|
+
}
|
|
78
|
+
// If same score, sort alphabetically by display name
|
|
79
|
+
return a.app.displayName.localeCompare(b.app.displayName)
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
return scoredApps.map((item) => item.app)
|
|
83
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createRouter } from '@tanstack/react-router'
|
|
2
|
-
import type { EhRouterInitParams } from '
|
|
3
|
-
import { routeTree } from '
|
|
2
|
+
import type { EhRouterInitParams } from '../types/types'
|
|
3
|
+
import { routeTree } from '../routeTree.gen'
|
|
4
4
|
|
|
5
5
|
export function createEhRouter({ context, history }: EhRouterInitParams) {
|
|
6
6
|
return createRouter({
|