@igstack/app-catalog-frontend-core 0.2.0 → 0.3.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.
- package/dist/esm/api/infra/trpc.d.ts +0 -1491
- package/dist/esm/modules/appCatalog/context/AppCatalogContext.js +1 -0
- package/dist/esm/modules/appCatalog/context/AppCatalogContext.js.map +1 -1
- package/dist/esm/modules/appCatalog/ui/filters/FilterBar.js +23 -11
- package/dist/esm/modules/appCatalog/ui/filters/FilterBar.js.map +1 -1
- package/dist/esm/modules/appCatalog/ui/grid/AppCatalogGrid.d.ts +5 -1
- package/dist/esm/modules/appCatalog/ui/grid/AppCatalogGrid.js +146 -56
- package/dist/esm/modules/appCatalog/ui/grid/AppCatalogGrid.js.map +1 -1
- package/dist/esm/modules/appCatalog/ui/pages/AppCatalogPage.js +20 -1
- package/dist/esm/modules/appCatalog/ui/pages/AppCatalogPage.js.map +1 -1
- package/dist/esm/modules/auth/AuthContext.js +1 -1
- package/dist/esm/modules/auth/AuthModalContext.js +1 -1
- package/dist/esm/modules/auth/authClient.d.ts +2 -2
- package/dist/esm/node_modules/.pnpm/zod@4.3.5/node_modules/zod/v4/classic/schemas.js +4 -37
- package/dist/esm/node_modules/.pnpm/zod@4.3.5/node_modules/zod/v4/classic/schemas.js.map +1 -1
- package/dist/esm/node_modules/.pnpm/zod@4.3.5/node_modules/zod/v4/core/api.js +2 -10
- package/dist/esm/node_modules/.pnpm/zod@4.3.5/node_modules/zod/v4/core/api.js.map +1 -1
- package/dist/esm/node_modules/.pnpm/zod@4.3.5/node_modules/zod/v4/core/checks.js +1 -1
- package/dist/esm/node_modules/.pnpm/zod@4.3.5/node_modules/zod/v4/core/json-schema-processors.js +0 -44
- package/dist/esm/node_modules/.pnpm/zod@4.3.5/node_modules/zod/v4/core/json-schema-processors.js.map +1 -1
- package/dist/esm/node_modules/.pnpm/zod@4.3.5/node_modules/zod/v4/core/parse.js +0 -4
- package/dist/esm/node_modules/.pnpm/zod@4.3.5/node_modules/zod/v4/core/parse.js.map +1 -1
- package/dist/esm/node_modules/.pnpm/zod@4.3.5/node_modules/zod/v4/core/regexes.js +0 -2
- package/dist/esm/node_modules/.pnpm/zod@4.3.5/node_modules/zod/v4/core/regexes.js.map +1 -1
- package/dist/esm/node_modules/.pnpm/zod@4.3.5/node_modules/zod/v4/core/schemas.js +4 -49
- package/dist/esm/node_modules/.pnpm/zod@4.3.5/node_modules/zod/v4/core/schemas.js.map +1 -1
- package/dist/esm/routeTree.gen.d.ts +3 -164
- package/dist/esm/routeTree.gen.js +8 -80
- package/dist/esm/routeTree.gen.js.map +1 -1
- package/dist/esm/ui/button.d.ts +1 -1
- package/dist/esm/ui/card.js +1 -48
- package/dist/esm/ui/card.js.map +1 -1
- package/dist/esm/ui/command.js +1 -15
- package/dist/esm/ui/command.js.map +1 -1
- package/dist/esm/ui/components/header/Header.js +2 -11
- package/dist/esm/ui/components/header/Header.js.map +1 -1
- package/dist/esm/ui/input-group.js +125 -0
- package/dist/esm/ui/input-group.js.map +1 -0
- package/package.json +3 -3
- package/src/modules/appCatalog/ui/components/AppDetailModal.tsx +2 -21
- package/src/routeTree.gen.ts +2 -220
- package/src/ui/components/header/Header.tsx +2 -12
- package/dist/esm/components/IconPickerDialog.d.ts +0 -8
- package/dist/esm/components/IconPickerDialog.js +0 -98
- package/dist/esm/components/IconPickerDialog.js.map +0 -1
- package/dist/esm/components/IconPickerField.d.ts +0 -9
- package/dist/esm/components/IconPickerField.js +0 -76
- package/dist/esm/components/IconPickerField.js.map +0 -1
- package/dist/esm/modules/admin-base/components/AdminChat.d.ts +0 -1
- package/dist/esm/modules/admin-base/components/AdminChat.js +0 -82
- package/dist/esm/modules/admin-base/components/AdminChat.js.map +0 -1
- package/dist/esm/modules/admin-base/components/AdminLayout.d.ts +0 -5
- package/dist/esm/modules/admin-base/components/AdminLayout.js +0 -83
- package/dist/esm/modules/admin-base/components/AdminLayout.js.map +0 -1
- package/dist/esm/modules/admin-base/components/AdminWelcome.d.ts +0 -1
- package/dist/esm/modules/admin-base/components/AdminWelcome.js +0 -37
- package/dist/esm/modules/admin-base/components/AdminWelcome.js.map +0 -1
- package/dist/esm/modules/admin-base/context/AdminConfigContext.d.ts +0 -8
- package/dist/esm/modules/admin-base/context/AdminConfigContext.js +0 -27
- package/dist/esm/modules/admin-base/context/AdminConfigContext.js.map +0 -1
- package/dist/esm/modules/admin-base/index.d.ts +0 -5
- package/dist/esm/modules/admin-base/types/adminTypes.d.ts +0 -10
- package/dist/esm/modules/appCatalog/AppCatalogAdminPage.d.ts +0 -1
- package/dist/esm/modules/appCatalog/AppCatalogAdminPage.js +0 -196
- package/dist/esm/modules/appCatalog/AppCatalogAdminPage.js.map +0 -1
- package/dist/esm/modules/appCatalog/ScreenshotItem.js +0 -57
- package/dist/esm/modules/appCatalog/ScreenshotItem.js.map +0 -1
- package/dist/esm/modules/appCatalog/ScreenshotManager.js +0 -155
- package/dist/esm/modules/appCatalog/ScreenshotManager.js.map +0 -1
- package/dist/esm/modules/approvalMethod/AccessRequestFormFields.d.ts +0 -7
- package/dist/esm/modules/approvalMethod/AccessRequestFormFields.js +0 -323
- package/dist/esm/modules/approvalMethod/AccessRequestFormFields.js.map +0 -1
- package/dist/esm/modules/approvalMethod/ApprovalMethodForm.d.ts +0 -14
- package/dist/esm/modules/approvalMethod/ApprovalMethodForm.js +0 -227
- package/dist/esm/modules/approvalMethod/ApprovalMethodForm.js.map +0 -1
- package/dist/esm/modules/approvalMethod/ApprovalMethodSelector.d.ts +0 -7
- package/dist/esm/modules/approvalMethod/ApprovalMethodSelector.js +0 -124
- package/dist/esm/modules/approvalMethod/ApprovalMethodSelector.js.map +0 -1
- package/dist/esm/modules/approvalMethod/api/ApiQueryMagazineApprovalMethod.d.ts +0 -381
- package/dist/esm/modules/approvalMethod/api/ApiQueryMagazineApprovalMethod.js +0 -26
- package/dist/esm/modules/approvalMethod/api/ApiQueryMagazineApprovalMethod.js.map +0 -1
- package/dist/esm/modules/auth/authUtils.js +0 -25
- package/dist/esm/modules/auth/authUtils.js.map +0 -1
- package/dist/esm/modules/icons/IconManagementPage.d.ts +0 -1
- package/dist/esm/modules/icons/IconManagementPage.js +0 -177
- package/dist/esm/modules/icons/IconManagementPage.js.map +0 -1
- package/dist/esm/node_modules/.pnpm/@dnd-kit_accessibility@3.1.1_react@19.1.2/node_modules/@dnd-kit/accessibility/dist/accessibility.esm.js +0 -60
- package/dist/esm/node_modules/.pnpm/@dnd-kit_accessibility@3.1.1_react@19.1.2/node_modules/@dnd-kit/accessibility/dist/accessibility.esm.js.map +0 -1
- package/dist/esm/node_modules/.pnpm/@dnd-kit_core@6.3.1_react-dom@19.1.2_react@19.1.2__react@19.1.2/node_modules/@dnd-kit/core/dist/core.esm.js +0 -3055
- package/dist/esm/node_modules/.pnpm/@dnd-kit_core@6.3.1_react-dom@19.1.2_react@19.1.2__react@19.1.2/node_modules/@dnd-kit/core/dist/core.esm.js.map +0 -1
- package/dist/esm/node_modules/.pnpm/@dnd-kit_sortable@10.0.0_@dnd-kit_core@6.3.1_react-dom@19.1.2_react@19.1.2__react@19.1.2__react@19.1.2/node_modules/@dnd-kit/sortable/dist/sortable.esm.js +0 -593
- package/dist/esm/node_modules/.pnpm/@dnd-kit_sortable@10.0.0_@dnd-kit_core@6.3.1_react-dom@19.1.2_react@19.1.2__react@19.1.2__react@19.1.2/node_modules/@dnd-kit/sortable/dist/sortable.esm.js.map +0 -1
- package/dist/esm/node_modules/.pnpm/@dnd-kit_utilities@3.2.2_react@19.1.2/node_modules/@dnd-kit/utilities/dist/utilities.esm.js +0 -302
- package/dist/esm/node_modules/.pnpm/@dnd-kit_utilities@3.2.2_react@19.1.2/node_modules/@dnd-kit/utilities/dist/utilities.esm.js.map +0 -1
- package/dist/esm/node_modules/.pnpm/@hookform_resolvers@5.2.2_react-hook-form@7.71.1_react@19.1.2_/node_modules/@hookform/resolvers/dist/resolvers.js +0 -34
- package/dist/esm/node_modules/.pnpm/@hookform_resolvers@5.2.2_react-hook-form@7.71.1_react@19.1.2_/node_modules/@hookform/resolvers/dist/resolvers.js.map +0 -1
- package/dist/esm/node_modules/.pnpm/@hookform_resolvers@5.2.2_react-hook-form@7.71.1_react@19.1.2_/node_modules/@hookform/resolvers/zod/dist/zod.js +0 -94
- package/dist/esm/node_modules/.pnpm/@hookform_resolvers@5.2.2_react-hook-form@7.71.1_react@19.1.2_/node_modules/@hookform/resolvers/zod/dist/zod.js.map +0 -1
- package/dist/esm/node_modules/.pnpm/react-hook-form@7.71.1_react@19.1.2/node_modules/react-hook-form/dist/index.esm.js +0 -1894
- package/dist/esm/node_modules/.pnpm/react-hook-form@7.71.1_react@19.1.2/node_modules/react-hook-form/dist/index.esm.js.map +0 -1
- package/dist/esm/routes/admin/app-for-catalog/$id.d.ts +0 -5
- package/dist/esm/routes/admin/app-for-catalog/_id.js +0 -67
- package/dist/esm/routes/admin/app-for-catalog/_id.js.map +0 -1
- package/dist/esm/routes/admin/app-for-catalog/_id2.js +0 -321
- package/dist/esm/routes/admin/app-for-catalog/_id2.js.map +0 -1
- package/dist/esm/routes/admin/app-for-catalog/index.d.ts +0 -1
- package/dist/esm/routes/admin/app-for-catalog/index.js +0 -9
- package/dist/esm/routes/admin/app-for-catalog/index.js.map +0 -1
- package/dist/esm/routes/admin/app-for-catalog/index2.js +0 -12
- package/dist/esm/routes/admin/app-for-catalog/index2.js.map +0 -1
- package/dist/esm/routes/admin/app-for-catalog.d.ts +0 -1
- package/dist/esm/routes/admin/app-for-catalog.js +0 -14
- package/dist/esm/routes/admin/app-for-catalog.js.map +0 -1
- package/dist/esm/routes/admin/app-for-catalog2.js +0 -9
- package/dist/esm/routes/admin/app-for-catalog2.js.map +0 -1
- package/dist/esm/routes/admin/approval-methods/index.d.ts +0 -32
- package/dist/esm/routes/admin/approval-methods/index.js +0 -24
- package/dist/esm/routes/admin/approval-methods/index.js.map +0 -1
- package/dist/esm/routes/admin/approval-methods/index2.js +0 -100
- package/dist/esm/routes/admin/approval-methods/index2.js.map +0 -1
- package/dist/esm/routes/admin/approval-methods.d.ts +0 -1
- package/dist/esm/routes/admin/approval-methods.js +0 -14
- package/dist/esm/routes/admin/approval-methods.js.map +0 -1
- package/dist/esm/routes/admin/approval-methods2.js +0 -7
- package/dist/esm/routes/admin/approval-methods2.js.map +0 -1
- package/dist/esm/routes/admin/chat.d.ts +0 -1
- package/dist/esm/routes/admin/chat.js +0 -14
- package/dist/esm/routes/admin/chat.js.map +0 -1
- package/dist/esm/routes/admin/chat2.js +0 -9
- package/dist/esm/routes/admin/chat2.js.map +0 -1
- package/dist/esm/routes/admin/icons.d.ts +0 -1
- package/dist/esm/routes/admin/icons.js +0 -14
- package/dist/esm/routes/admin/icons.js.map +0 -1
- package/dist/esm/routes/admin/icons2.js +0 -12
- package/dist/esm/routes/admin/icons2.js.map +0 -1
- package/dist/esm/routes/admin/index.d.ts +0 -1
- package/dist/esm/routes/admin/index.js +0 -9
- package/dist/esm/routes/admin/index.js.map +0 -1
- package/dist/esm/routes/admin/index2.js +0 -9
- package/dist/esm/routes/admin/index2.js.map +0 -1
- package/dist/esm/routes/admin.d.ts +0 -1
- package/dist/esm/routes/admin.js +0 -37
- package/dist/esm/routes/admin.js.map +0 -1
- package/dist/esm/routes/admin2.js +0 -18
- package/dist/esm/routes/admin2.js.map +0 -1
- package/dist/esm/ui/alert-dialog.js +0 -141
- package/dist/esm/ui/alert-dialog.js.map +0 -1
- package/dist/esm/ui/breadcrumb.js +0 -84
- package/dist/esm/ui/breadcrumb.js.map +0 -1
- package/dist/esm/ui/components/Breadcrumbs.js +0 -36
- package/dist/esm/ui/components/Breadcrumbs.js.map +0 -1
- package/dist/esm/ui/crud-list/CrudList.js +0 -189
- package/dist/esm/ui/crud-list/CrudList.js.map +0 -1
- package/dist/esm/ui/editable-list/EditableListField.js +0 -130
- package/dist/esm/ui/editable-list/EditableListField.js.map +0 -1
- package/dist/esm/ui/form.js +0 -134
- package/dist/esm/ui/form.js.map +0 -1
- package/dist/esm/ui/linkExternal.js +0 -26
- package/dist/esm/ui/linkExternal.js.map +0 -1
- package/dist/esm/ui/markdown-editor/MarkdownEditor.js +0 -116
- package/dist/esm/ui/markdown-editor/MarkdownEditor.js.map +0 -1
- package/dist/esm/ui/markdown-editor/MarkdownToolbar.js +0 -99
- package/dist/esm/ui/markdown-editor/MarkdownToolbar.js.map +0 -1
- package/dist/esm/ui/scroll-area.js +0 -62
- package/dist/esm/ui/scroll-area.js.map +0 -1
- package/dist/esm/ui/select.js +0 -138
- package/dist/esm/ui/select.js.map +0 -1
- package/dist/esm/ui/textarea.js +0 -19
- package/dist/esm/ui/textarea.js.map +0 -1
- package/src/components/IconPickerDialog.tsx +0 -136
- package/src/components/IconPickerField.tsx +0 -88
- package/src/modules/admin-base/components/AdminChat.tsx +0 -122
- package/src/modules/admin-base/components/AdminLayout.tsx +0 -111
- package/src/modules/admin-base/components/AdminWelcome.tsx +0 -52
- package/src/modules/admin-base/context/AdminConfigContext.tsx +0 -36
- package/src/modules/admin-base/index.ts +0 -16
- package/src/modules/admin-base/types/adminTypes.ts +0 -11
- package/src/modules/appCatalog/AppCatalogAdminPage.tsx +0 -274
- package/src/modules/approvalMethod/AccessRequestFormFields.tsx +0 -393
- package/src/modules/approvalMethod/ApprovalMethodForm.tsx +0 -323
- package/src/modules/approvalMethod/ApprovalMethodSelector.tsx +0 -150
- package/src/modules/approvalMethod/api/ApiQueryMagazineApprovalMethod.ts +0 -34
- package/src/modules/icons/IconManagementPage.tsx +0 -245
- package/src/routes/admin/app-for-catalog/$id.tsx +0 -571
- package/src/routes/admin/app-for-catalog/index.tsx +0 -19
- package/src/routes/admin/app-for-catalog.tsx +0 -12
- package/src/routes/admin/approval-methods/index.tsx +0 -161
- package/src/routes/admin/approval-methods.tsx +0 -10
- package/src/routes/admin/chat.tsx +0 -13
- package/src/routes/admin/icons.tsx +0 -22
- package/src/routes/admin/index.tsx +0 -9
- package/src/routes/admin.tsx +0 -60
|
@@ -2,6 +2,7 @@ import { jsx } from "react/jsx-runtime";
|
|
|
2
2
|
import { useQuery } from "@tanstack/react-query";
|
|
3
3
|
import { use, createContext, useMemo, useEffect } from "react";
|
|
4
4
|
import { ApiQueryMagazineAppCatalog } from "../api/ApiQueryMagazineAppCatalog.js";
|
|
5
|
+
import "../ui/context/AppCatalogFiltersContext.js";
|
|
5
6
|
import "@tanstack/react-query-devtools";
|
|
6
7
|
import "next-themes";
|
|
7
8
|
import "@radix-ui/react-dialog";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AppCatalogContext.js","sources":["../../../../../src/modules/appCatalog/context/AppCatalogContext.tsx"],"sourcesContent":["import type {\n AppApprovalMethod,\n AppForCatalog,\n GroupingTagDefinition,\n} from '@igstack/app-catalog-backend-core'\nimport { useQuery } from '@tanstack/react-query'\nimport type { ReactNode } from 'react'\nimport { createContext, use, useEffect, useMemo } from 'react'\nimport { ApiQueryMagazineAppCatalog } from '~/modules/appCatalog'\n\nexport interface AppCatalogContextIface {\n apps: Array<AppForCatalog>\n isLoadingApps: boolean\n tagsDefinitions: Array<GroupingTagDefinition>\n approvalMethods: Array<AppApprovalMethod>\n appVersion?: { displayName: string; url?: string }\n}\n\nexport const AppCatalogContext = createContext<\n AppCatalogContextIface | undefined\n>(undefined)\n\ninterface AppCatalogProviderProps {\n children: ReactNode\n}\n\nexport function AppCatalogProvider({ children }: AppCatalogProviderProps) {\n const { data, isLoading: isLoadingApps } = useQuery(\n ApiQueryMagazineAppCatalog.getAppCatalog(),\n )\n\n const contextValue = useMemo<AppCatalogContextIface>(\n () => ({\n apps: data?.apps ?? [],\n isLoadingApps,\n tagsDefinitions: data?.tagsDefinitions ?? [],\n approvalMethods: data?.approvalMethods ?? [],\n appVersion: data?.appVersion,\n }),\n [\n data?.approvalMethods,\n data?.apps,\n data?.tagsDefinitions,\n data?.appVersion,\n isLoadingApps,\n ],\n )\n\n // Update document title based on app version\n useEffect(() => {\n if (data?.appVersion?.displayName === 'local') {\n document.title = 'Local'\n } else {\n document.title = 'App Catalog'\n }\n }, [data?.appVersion?.displayName])\n\n return <AppCatalogContext value={contextValue}>{children}</AppCatalogContext>\n}\n\nexport function useAppCatalogContext(): AppCatalogContextIface {\n const context = use(AppCatalogContext)\n if (!context) {\n throw new Error(\n 'useAppCatalogContext must be used within AppCatalogProvider',\n )\n }\n return context\n}\n"],"names":["_a"],"mappings":"
|
|
1
|
+
{"version":3,"file":"AppCatalogContext.js","sources":["../../../../../src/modules/appCatalog/context/AppCatalogContext.tsx"],"sourcesContent":["import type {\n AppApprovalMethod,\n AppForCatalog,\n GroupingTagDefinition,\n} from '@igstack/app-catalog-backend-core'\nimport { useQuery } from '@tanstack/react-query'\nimport type { ReactNode } from 'react'\nimport { createContext, use, useEffect, useMemo } from 'react'\nimport { ApiQueryMagazineAppCatalog } from '~/modules/appCatalog'\n\nexport interface AppCatalogContextIface {\n apps: Array<AppForCatalog>\n isLoadingApps: boolean\n tagsDefinitions: Array<GroupingTagDefinition>\n approvalMethods: Array<AppApprovalMethod>\n appVersion?: { displayName: string; url?: string }\n}\n\nexport const AppCatalogContext = createContext<\n AppCatalogContextIface | undefined\n>(undefined)\n\ninterface AppCatalogProviderProps {\n children: ReactNode\n}\n\nexport function AppCatalogProvider({ children }: AppCatalogProviderProps) {\n const { data, isLoading: isLoadingApps } = useQuery(\n ApiQueryMagazineAppCatalog.getAppCatalog(),\n )\n\n const contextValue = useMemo<AppCatalogContextIface>(\n () => ({\n apps: data?.apps ?? [],\n isLoadingApps,\n tagsDefinitions: data?.tagsDefinitions ?? [],\n approvalMethods: data?.approvalMethods ?? [],\n appVersion: data?.appVersion,\n }),\n [\n data?.approvalMethods,\n data?.apps,\n data?.tagsDefinitions,\n data?.appVersion,\n isLoadingApps,\n ],\n )\n\n // Update document title based on app version\n useEffect(() => {\n if (data?.appVersion?.displayName === 'local') {\n document.title = 'Local'\n } else {\n document.title = 'App Catalog'\n }\n }, [data?.appVersion?.displayName])\n\n return <AppCatalogContext value={contextValue}>{children}</AppCatalogContext>\n}\n\nexport function useAppCatalogContext(): AppCatalogContextIface {\n const context = use(AppCatalogContext)\n if (!context) {\n throw new Error(\n 'useAppCatalogContext must be used within AppCatalogProvider',\n )\n }\n return context\n}\n"],"names":["_a"],"mappings":";;;;;;;;;;;;;;;;AAkBO,MAAM,oBAAoB,cAE/B,MAAS;AAMJ,SAAS,mBAAmB,EAAE,YAAqC;;AACxE,QAAM,EAAE,MAAM,WAAW,cAAA,IAAkB;AAAA,IACzC,2BAA2B,cAAA;AAAA,EAAc;AAG3C,QAAM,eAAe;AAAA,IACnB,OAAO;AAAA,MACL,OAAM,6BAAM,SAAQ,CAAA;AAAA,MACpB;AAAA,MACA,kBAAiB,6BAAM,oBAAmB,CAAA;AAAA,MAC1C,kBAAiB,6BAAM,oBAAmB,CAAA;AAAA,MAC1C,YAAY,6BAAM;AAAA,IAAA;AAAA,IAEpB;AAAA,MACE,6BAAM;AAAA,MACN,6BAAM;AAAA,MACN,6BAAM;AAAA,MACN,6BAAM;AAAA,MACN;AAAA,IAAA;AAAA,EACF;AAIF,YAAU,MAAM;;AACd,UAAIA,MAAA,6BAAM,eAAN,gBAAAA,IAAkB,iBAAgB,SAAS;AAC7C,eAAS,QAAQ;AAAA,IACnB,OAAO;AACL,eAAS,QAAQ;AAAA,IACnB;AAAA,EACF,GAAG,EAAC,kCAAM,eAAN,mBAAkB,WAAW,CAAC;AAElC,SAAO,oBAAC,mBAAA,EAAkB,OAAO,cAAe,SAAA,CAAS;AAC3D;AAEO,SAAS,uBAA+C;AAC7D,QAAM,UAAU,IAAI,iBAAiB;AACrC,MAAI,CAAC,SAAS;AACZ,UAAM,IAAI;AAAA,MACR;AAAA,IAAA;AAAA,EAEJ;AACA,SAAO;AACT;"}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { jsxs, jsx } from "react/jsx-runtime";
|
|
2
|
+
import { X } from "lucide-react";
|
|
2
3
|
import { useMemo } from "react";
|
|
3
4
|
import { Button } from "../../../../ui/button.js";
|
|
4
5
|
import { Checkbox } from "../../../../ui/checkbox.js";
|
|
5
|
-
import {
|
|
6
|
+
import { InputGroup, InputGroupInput, InputGroupAddon, InputGroupButton } from "../../../../ui/input-group.js";
|
|
6
7
|
import { Label } from "../../../../ui/label.js";
|
|
7
8
|
import { useAppCatalogFilters } from "../context/AppCatalogFiltersContext.js";
|
|
8
9
|
import { FilterCombobox } from "./FilterCombobox.js";
|
|
@@ -40,16 +41,27 @@ function FilterBar({
|
|
|
40
41
|
data.availableTagsByPrefix
|
|
41
42
|
]);
|
|
42
43
|
return /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3 mb-4", children: [
|
|
43
|
-
/* @__PURE__ */
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
44
|
+
/* @__PURE__ */ jsxs(InputGroup, { className: "max-w-sm", children: [
|
|
45
|
+
/* @__PURE__ */ jsx(
|
|
46
|
+
InputGroupInput,
|
|
47
|
+
{
|
|
48
|
+
value: state.searchValue,
|
|
49
|
+
onChange: (e) => actions.setSearchValue(e.target.value),
|
|
50
|
+
onFocus: (e) => e.target.select(),
|
|
51
|
+
placeholder: "Search apps by name, description, or tags…",
|
|
52
|
+
"aria-label": "Search apps"
|
|
53
|
+
}
|
|
54
|
+
),
|
|
55
|
+
state.searchValue && /* @__PURE__ */ jsx(InputGroupAddon, { align: "inline-end", children: /* @__PURE__ */ jsx(
|
|
56
|
+
InputGroupButton,
|
|
57
|
+
{
|
|
58
|
+
size: "icon-xs",
|
|
59
|
+
onClick: () => actions.setSearchValue(""),
|
|
60
|
+
"aria-label": "Clear search",
|
|
61
|
+
children: /* @__PURE__ */ jsx(X, {})
|
|
62
|
+
}
|
|
63
|
+
) })
|
|
64
|
+
] }),
|
|
53
65
|
/* @__PURE__ */ jsx("div", { className: "h-8 w-px bg-border" }),
|
|
54
66
|
/* @__PURE__ */ jsxs("div", { className: "flex items-center rounded-md border", children: [
|
|
55
67
|
/* @__PURE__ */ jsxs(
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"FilterBar.js","sources":["../../../../../../src/modules/appCatalog/ui/filters/FilterBar.tsx"],"sourcesContent":["import type { AppForCatalog } from '@igstack/app-catalog-backend-core'\nimport { useMemo } from 'react'\nimport { Button } from '~/ui/button'\nimport { Checkbox } from '~/ui/checkbox'\nimport {
|
|
1
|
+
{"version":3,"file":"FilterBar.js","sources":["../../../../../../src/modules/appCatalog/ui/filters/FilterBar.tsx"],"sourcesContent":["import type { AppForCatalog } from '@igstack/app-catalog-backend-core'\nimport { X } from 'lucide-react'\nimport { useMemo } from 'react'\nimport { Button } from '~/ui/button'\nimport { Checkbox } from '~/ui/checkbox'\nimport {\n InputGroup,\n InputGroupAddon,\n InputGroupButton,\n InputGroupInput,\n} from '~/ui/input-group'\nimport { Label } from '~/ui/label'\nimport { useAppCatalogFilters } from '../context/AppCatalogFiltersContext'\nimport { FilterCombobox } from './FilterCombobox'\n\ninterface FilterBarProps {\n /** Total number of apps (respecting deprecated filter) */\n totalCount: number\n /** Number of apps in \"My Recent\" */\n recentCount: number\n /** Number of deprecated apps (total) */\n deprecatedCount: number\n /** All apps for counting filter options */\n apps: Array<AppForCatalog>\n}\n\n/**\n * Horizontal filter bar with All/My Recent toggle, dynamic tag filter comboboxes, and search.\n * Filters are mutually exclusive: Recent clears tag filters, tag filters clear Recent.\n * All discovery controls are grouped together.\n */\nexport function FilterBar({\n totalCount,\n recentCount,\n deprecatedCount,\n apps,\n}: FilterBarProps) {\n const { state, data, actions } = useAppCatalogFilters()\n\n // Check if \"Show All\" mode is truly active (no filters at all)\n const isShowAllActive =\n !state.recentMode && Object.keys(state.tagFilters).length === 0\n\n // Calculate counts for each filter option (respecting showDeprecated)\n const filterOptionCounts = useMemo(() => {\n const counts: Record<string, Record<string, number>> = {}\n\n // Filter apps by deprecated setting first\n const baseApps = state.showDeprecated\n ? apps\n : apps.filter((app) => !app.deprecated)\n\n state.filterableTagPrefixes.forEach((prefix) => {\n const prefixCounts: Record<string, number> = {}\n const options = data.availableTagsByPrefix[prefix] || []\n\n options.forEach((option) => {\n const fullTag = `${prefix}:${option.value}`\n const count = baseApps.filter((app) =>\n app.tags?.some((tag) => tag.toLowerCase() === fullTag.toLowerCase()),\n ).length\n prefixCounts[option.value] = count\n })\n\n counts[prefix] = prefixCounts\n })\n\n return counts\n }, [\n apps,\n state.showDeprecated,\n state.filterableTagPrefixes,\n data.availableTagsByPrefix,\n ])\n\n return (\n <div className=\"flex items-center gap-3 mb-4\">\n {/* Search input */}\n <InputGroup className=\"max-w-sm\">\n <InputGroupInput\n value={state.searchValue}\n onChange={(e) => actions.setSearchValue(e.target.value)}\n onFocus={(e) => e.target.select()}\n placeholder=\"Search apps by name, description, or tags…\"\n aria-label=\"Search apps\"\n />\n {state.searchValue && (\n <InputGroupAddon align=\"inline-end\">\n <InputGroupButton\n size=\"icon-xs\"\n onClick={() => actions.setSearchValue('')}\n aria-label=\"Clear search\"\n >\n <X />\n </InputGroupButton>\n </InputGroupAddon>\n )}\n </InputGroup>\n\n {/* Vertical divider */}\n <div className=\"h-8 w-px bg-border\" />\n\n {/* Show All / My Recent toggle group */}\n <div className=\"flex items-center rounded-md border\">\n <Button\n variant={isShowAllActive ? 'default' : 'ghost'}\n size=\"sm\"\n onClick={() => actions.setRecentMode(false)}\n className=\"rounded-r-none border-r\"\n >\n Show All ({totalCount})\n </Button>\n <Button\n variant={state.recentMode ? 'default' : 'ghost'}\n size=\"sm\"\n onClick={() => actions.setRecentMode(true)}\n className=\"rounded-l-none\"\n disabled={recentCount === 0}\n >\n My Recent ({recentCount})\n </Button>\n </div>\n\n {/* Vertical divider */}\n {state.filterableTagPrefixes.length > 0 && (\n <div className=\"h-8 w-px bg-border\" />\n )}\n\n {/* Dynamic tag filter comboboxes */}\n {state.filterableTagPrefixes.map((prefix) => {\n const options = data.availableTagsByPrefix[prefix] || []\n const value = state.tagFilters[prefix]\n const counts = filterOptionCounts[prefix] || {}\n\n // Create \"Filter By <Name>\" label\n const displayName =\n prefix.charAt(0).toUpperCase() + prefix.slice(1).replace(/-/g, ' ')\n const label = `Filter By ${displayName}`\n\n return (\n <FilterCombobox\n key={prefix}\n prefix={prefix}\n label={label}\n options={options}\n value={value}\n counts={counts}\n onValueChange={(newValue) => actions.setTagFilter(prefix, newValue)}\n />\n )\n })}\n\n {/* Vertical divider before deprecated checkbox */}\n <div className=\"h-8 w-px bg-border\" />\n\n {/* Show Deprecated Apps checkbox */}\n <div className=\"flex items-center gap-2\">\n <Checkbox\n id=\"show-deprecated\"\n checked={state.showDeprecated}\n onCheckedChange={(checked) =>\n actions.setShowDeprecated(checked === true)\n }\n />\n <Label\n htmlFor=\"show-deprecated\"\n className=\"text-sm font-normal cursor-pointer\"\n >\n Show Deprecated Apps ({deprecatedCount})\n </Label>\n </div>\n </div>\n )\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -6,5 +6,9 @@ export interface AppCatalogGridProps {
|
|
|
6
6
|
onAppClick?: (app: AppForCatalog) => void;
|
|
7
7
|
/** Whether search is active (affects group sorting) */
|
|
8
8
|
hasSearch?: boolean;
|
|
9
|
+
/** Total count of apps before filtering */
|
|
10
|
+
totalAppsCount?: number;
|
|
11
|
+
/** Callback to clear all filters and search */
|
|
12
|
+
onClearFilters?: () => void;
|
|
9
13
|
}
|
|
10
|
-
export declare function AppCatalogGrid({ apps, selectedAppSlug, groupingDefinition, onAppClick, hasSearch, }: AppCatalogGridProps): import("react/jsx-runtime").JSX.Element;
|
|
14
|
+
export declare function AppCatalogGrid({ apps, selectedAppSlug, groupingDefinition, onAppClick, hasSearch, totalAppsCount, onClearFilters, }: AppCatalogGridProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -2,6 +2,7 @@ import { jsxs, jsx, Fragment } from "react/jsx-runtime";
|
|
|
2
2
|
import { useReactTable, getCoreRowModel, flexRender } from "@tanstack/react-table";
|
|
3
3
|
import { X, AppWindow, ExternalLink } from "lucide-react";
|
|
4
4
|
import React__default, { useState, useEffect } from "react";
|
|
5
|
+
import { useHotkeys } from "react-hotkeys-hook";
|
|
5
6
|
import { cn } from "../../../../lib/utils.js";
|
|
6
7
|
import { Badge } from "../../../../ui/badge.js";
|
|
7
8
|
import { Button } from "../../../../ui/button.js";
|
|
@@ -70,13 +71,37 @@ function AppScreenshot({ app }) {
|
|
|
70
71
|
}
|
|
71
72
|
function AppDetails({
|
|
72
73
|
app,
|
|
73
|
-
onAppClick
|
|
74
|
+
onAppClick,
|
|
75
|
+
onClosePanel
|
|
74
76
|
}) {
|
|
75
77
|
var _a;
|
|
76
78
|
const [isGalleryOpen, setIsGalleryOpen] = React__default.useState(false);
|
|
77
79
|
const [galleryInitialIndex, setGalleryInitialIndex] = React__default.useState(0);
|
|
78
80
|
const { approvalMethods, apps } = useAppCatalogContext();
|
|
79
81
|
const { recordClick } = useAppClickHistory();
|
|
82
|
+
useHotkeys(
|
|
83
|
+
"enter",
|
|
84
|
+
() => {
|
|
85
|
+
var _a2;
|
|
86
|
+
const tag = (_a2 = document.activeElement) == null ? void 0 : _a2.tagName;
|
|
87
|
+
if (tag === "BUTTON" || tag === "A" || tag === "INPUT" || tag === "SELECT" || tag === "TEXTAREA")
|
|
88
|
+
return;
|
|
89
|
+
if (app.screenshotIds && app.screenshotIds.length > 0) {
|
|
90
|
+
setGalleryInitialIndex(0);
|
|
91
|
+
setIsGalleryOpen(true);
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
{ enabled: !isGalleryOpen },
|
|
95
|
+
[app, isGalleryOpen]
|
|
96
|
+
);
|
|
97
|
+
useHotkeys(
|
|
98
|
+
"escape",
|
|
99
|
+
() => {
|
|
100
|
+
onClosePanel();
|
|
101
|
+
},
|
|
102
|
+
{ enabled: !isGalleryOpen },
|
|
103
|
+
[isGalleryOpen, onClosePanel]
|
|
104
|
+
);
|
|
80
105
|
const handleScreenshotClick = (index) => {
|
|
81
106
|
setGalleryInitialIndex(index);
|
|
82
107
|
setIsGalleryOpen(true);
|
|
@@ -87,10 +112,10 @@ function AppDetails({
|
|
|
87
112
|
}) : null;
|
|
88
113
|
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
89
114
|
/* @__PURE__ */ jsxs("div", { className: "flex h-full flex-col p-6", children: [
|
|
90
|
-
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-
|
|
115
|
+
/* @__PURE__ */ jsx("div", { className: "border-b pb-6", children: /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [
|
|
91
116
|
/* @__PURE__ */ jsx(AppIcon, { app, className: "size-16" }),
|
|
92
|
-
/* @__PURE__ */ jsxs("div", { children: [
|
|
93
|
-
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
117
|
+
/* @__PURE__ */ jsxs("div", { className: "-mx-3", children: [
|
|
118
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 px-3", children: [
|
|
94
119
|
/* @__PURE__ */ jsx("h2", { className: "text-2xl font-semibold", children: app.displayName }),
|
|
95
120
|
app.deprecated && (() => {
|
|
96
121
|
const deprecationType = app.deprecated.type || "deprecated";
|
|
@@ -110,15 +135,15 @@ function AppDetails({
|
|
|
110
135
|
target: "_blank",
|
|
111
136
|
rel: "noopener noreferrer",
|
|
112
137
|
onClick: () => recordClick(app.slug),
|
|
113
|
-
className: "mt-1 inline-flex items-center gap-1 text-sm text-
|
|
138
|
+
className: "mt-1 inline-flex items-center gap-1 rounded-md px-3 py-1 text-sm text-blue-600 hover:bg-accent/30 hover:underline dark:text-blue-400 transition-all group",
|
|
114
139
|
children: [
|
|
115
140
|
app.appUrl.replaceAll(/https?:\/\//g, ""),
|
|
116
|
-
/* @__PURE__ */ jsx(ExternalLink, { className: "size-3" })
|
|
141
|
+
/* @__PURE__ */ jsx(ExternalLink, { className: "size-3.5 shrink-0 opacity-40 group-hover:opacity-100 transition-opacity" })
|
|
117
142
|
]
|
|
118
143
|
}
|
|
119
144
|
)
|
|
120
145
|
] })
|
|
121
|
-
] }),
|
|
146
|
+
] }) }),
|
|
122
147
|
app.deprecated && (() => {
|
|
123
148
|
const deprecationType = app.deprecated.type || "deprecated";
|
|
124
149
|
const isDiscouraged = deprecationType === "discouraged";
|
|
@@ -178,6 +203,23 @@ function AppDetails({
|
|
|
178
203
|
)
|
|
179
204
|
] }),
|
|
180
205
|
/* @__PURE__ */ jsx(AccessRequestSection, { app, approvalMethods }),
|
|
206
|
+
app.links && app.links.length > 0 && /* @__PURE__ */ jsxs("div", { className: "mt-4", children: [
|
|
207
|
+
/* @__PURE__ */ jsx("h3", { className: "mb-1 text-xs font-medium text-muted-foreground", children: "Links" }),
|
|
208
|
+
/* @__PURE__ */ jsx("div", { className: "space-y-0.5", children: app.links.map((link) => /* @__PURE__ */ jsxs(
|
|
209
|
+
"a",
|
|
210
|
+
{
|
|
211
|
+
href: link.url,
|
|
212
|
+
target: "_blank",
|
|
213
|
+
rel: "noopener noreferrer",
|
|
214
|
+
className: "flex items-center gap-1 text-xs text-muted-foreground hover:text-primary truncate",
|
|
215
|
+
children: [
|
|
216
|
+
/* @__PURE__ */ jsx(ExternalLink, { className: "size-3 shrink-0" }),
|
|
217
|
+
link.title || link.url.replaceAll(/https?:\/\//g, "")
|
|
218
|
+
]
|
|
219
|
+
},
|
|
220
|
+
link.url
|
|
221
|
+
)) })
|
|
222
|
+
] }),
|
|
181
223
|
app.tags && app.tags.length > 0 && /* @__PURE__ */ jsxs("div", { className: "mt-6", children: [
|
|
182
224
|
/* @__PURE__ */ jsx("h3", { className: "mb-2 text-sm font-medium", children: "Tags" }),
|
|
183
225
|
/* @__PURE__ */ jsx("div", { className: "flex flex-wrap gap-2", children: app.tags.map((tag) => /* @__PURE__ */ jsx(Badge, { variant: "secondary", className: "text-xs", children: tag }, tag)) })
|
|
@@ -185,6 +227,22 @@ function AppDetails({
|
|
|
185
227
|
app.teams && app.teams.length > 0 && /* @__PURE__ */ jsxs("div", { className: "mt-6", children: [
|
|
186
228
|
/* @__PURE__ */ jsx("h3", { className: "mb-2 text-sm font-medium", children: "Teams" }),
|
|
187
229
|
/* @__PURE__ */ jsx("div", { className: "flex flex-wrap gap-2", children: app.teams.map((team) => /* @__PURE__ */ jsx(Badge, { variant: "outline", className: "text-xs", children: team }, team)) })
|
|
230
|
+
] }),
|
|
231
|
+
app.sources && app.sources.length > 0 && /* @__PURE__ */ jsxs("div", { className: "mt-6", children: [
|
|
232
|
+
/* @__PURE__ */ jsx("h3", { className: "mb-2 text-sm font-medium", children: "Sources" }),
|
|
233
|
+
/* @__PURE__ */ jsx("ol", { className: "list-decimal list-inside space-y-1", children: app.sources.map((source, index) => /* @__PURE__ */ jsx("li", { className: "text-xs text-muted-foreground", children: /* @__PURE__ */ jsxs(
|
|
234
|
+
"a",
|
|
235
|
+
{
|
|
236
|
+
href: source,
|
|
237
|
+
target: "_blank",
|
|
238
|
+
rel: "noopener noreferrer",
|
|
239
|
+
className: "hover:text-primary inline-flex items-center gap-1",
|
|
240
|
+
children: [
|
|
241
|
+
source.replaceAll(/https?:\/\//g, ""),
|
|
242
|
+
/* @__PURE__ */ jsx(ExternalLink, { className: "size-3 shrink-0" })
|
|
243
|
+
]
|
|
244
|
+
}
|
|
245
|
+
) }, index)) })
|
|
188
246
|
] })
|
|
189
247
|
] }),
|
|
190
248
|
/* @__PURE__ */ jsx(
|
|
@@ -253,7 +311,9 @@ function AppCatalogGrid({
|
|
|
253
311
|
selectedAppSlug,
|
|
254
312
|
groupingDefinition,
|
|
255
313
|
onAppClick,
|
|
256
|
-
hasSearch = false
|
|
314
|
+
hasSearch = false,
|
|
315
|
+
totalAppsCount,
|
|
316
|
+
onClearFilters
|
|
257
317
|
}) {
|
|
258
318
|
const selectedApp = selectedAppSlug ? apps.find((a) => a.slug === selectedAppSlug) : null;
|
|
259
319
|
const groupedApps = groupApps(apps, groupingDefinition, hasSearch);
|
|
@@ -350,55 +410,78 @@ function AppCatalogGrid({
|
|
|
350
410
|
header.id
|
|
351
411
|
);
|
|
352
412
|
}) }, headerGroup.id)) }),
|
|
353
|
-
/* @__PURE__ */
|
|
354
|
-
/* @__PURE__ */
|
|
413
|
+
/* @__PURE__ */ jsxs(TableBody, { children: [
|
|
414
|
+
groupedApps.map((group) => /* @__PURE__ */ jsxs(React__default.Fragment, { children: [
|
|
415
|
+
/* @__PURE__ */ jsx(TableRow, { className: "bg-muted/50 hover:bg-muted/50", children: /* @__PURE__ */ jsx(
|
|
416
|
+
TableCell,
|
|
417
|
+
{
|
|
418
|
+
colSpan: columns.length,
|
|
419
|
+
className: "px-4 py-6 sticky top-[49px] bg-muted/90 backdrop-blur z-10",
|
|
420
|
+
children: /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center", children: /* @__PURE__ */ jsx("span", { className: "font-bold text-lg tracking-widest uppercase leading-loose text-muted-foreground", children: group.groupName }) })
|
|
421
|
+
}
|
|
422
|
+
) }),
|
|
423
|
+
group.apps.map((app) => {
|
|
424
|
+
const row = table.getRowModel().rows.find((r) => r.id === app.id);
|
|
425
|
+
if (!row) return null;
|
|
426
|
+
return /* @__PURE__ */ jsx(
|
|
427
|
+
TableRow,
|
|
428
|
+
{
|
|
429
|
+
ref: (el) => {
|
|
430
|
+
if (el && row.original.slug) {
|
|
431
|
+
rowRefs.current.set(row.original.slug, el);
|
|
432
|
+
} else if (row.original.slug) {
|
|
433
|
+
rowRefs.current.delete(row.original.slug);
|
|
434
|
+
}
|
|
435
|
+
},
|
|
436
|
+
onClick: () => handleAppClick(row.original),
|
|
437
|
+
className: cn(
|
|
438
|
+
"border-b cursor-pointer transition-colors",
|
|
439
|
+
(selectedApp == null ? void 0 : selectedApp.id) === row.original.id ? "bg-blue-100 dark:bg-blue-950 hover:bg-blue-200 dark:hover:bg-blue-900" : "hover:bg-muted/30"
|
|
440
|
+
),
|
|
441
|
+
children: row.getVisibleCells().map((cell) => {
|
|
442
|
+
var _a;
|
|
443
|
+
return /* @__PURE__ */ jsx(
|
|
444
|
+
TableCell,
|
|
445
|
+
{
|
|
446
|
+
className: cn(
|
|
447
|
+
"px-4 py-4",
|
|
448
|
+
(_a = cell.column.columnDef.meta) == null ? void 0 : _a.className
|
|
449
|
+
),
|
|
450
|
+
children: flexRender(
|
|
451
|
+
cell.column.columnDef.cell,
|
|
452
|
+
cell.getContext()
|
|
453
|
+
)
|
|
454
|
+
},
|
|
455
|
+
cell.id
|
|
456
|
+
);
|
|
457
|
+
})
|
|
458
|
+
},
|
|
459
|
+
row.id
|
|
460
|
+
);
|
|
461
|
+
})
|
|
462
|
+
] }, group.groupName)),
|
|
463
|
+
totalAppsCount && totalAppsCount > apps.length && onClearFilters && /* @__PURE__ */ jsx(TableRow, { children: /* @__PURE__ */ jsx(
|
|
355
464
|
TableCell,
|
|
356
465
|
{
|
|
357
466
|
colSpan: columns.length,
|
|
358
|
-
className: "px-4 py-
|
|
359
|
-
children: /* @__PURE__ */
|
|
467
|
+
className: "px-4 py-8 text-center",
|
|
468
|
+
children: /* @__PURE__ */ jsxs(
|
|
469
|
+
Button,
|
|
470
|
+
{
|
|
471
|
+
variant: "outline",
|
|
472
|
+
onClick: onClearFilters,
|
|
473
|
+
className: "gap-2",
|
|
474
|
+
children: [
|
|
475
|
+
/* @__PURE__ */ jsx(X, { className: "h-4 w-4" }),
|
|
476
|
+
"Clear filters to show all apps (",
|
|
477
|
+
totalAppsCount,
|
|
478
|
+
")"
|
|
479
|
+
]
|
|
480
|
+
}
|
|
481
|
+
)
|
|
360
482
|
}
|
|
361
|
-
) })
|
|
362
|
-
|
|
363
|
-
const row = table.getRowModel().rows.find((r) => r.id === app.id);
|
|
364
|
-
if (!row) return null;
|
|
365
|
-
return /* @__PURE__ */ jsx(
|
|
366
|
-
TableRow,
|
|
367
|
-
{
|
|
368
|
-
ref: (el) => {
|
|
369
|
-
if (el && row.original.slug) {
|
|
370
|
-
rowRefs.current.set(row.original.slug, el);
|
|
371
|
-
} else if (row.original.slug) {
|
|
372
|
-
rowRefs.current.delete(row.original.slug);
|
|
373
|
-
}
|
|
374
|
-
},
|
|
375
|
-
onClick: () => handleAppClick(row.original),
|
|
376
|
-
className: cn(
|
|
377
|
-
"border-b cursor-pointer transition-colors",
|
|
378
|
-
(selectedApp == null ? void 0 : selectedApp.id) === row.original.id ? "bg-blue-100 dark:bg-blue-950 hover:bg-blue-200 dark:hover:bg-blue-900" : "hover:bg-muted/30"
|
|
379
|
-
),
|
|
380
|
-
children: row.getVisibleCells().map((cell) => {
|
|
381
|
-
var _a;
|
|
382
|
-
return /* @__PURE__ */ jsx(
|
|
383
|
-
TableCell,
|
|
384
|
-
{
|
|
385
|
-
className: cn(
|
|
386
|
-
"px-4 py-4",
|
|
387
|
-
(_a = cell.column.columnDef.meta) == null ? void 0 : _a.className
|
|
388
|
-
),
|
|
389
|
-
children: flexRender(
|
|
390
|
-
cell.column.columnDef.cell,
|
|
391
|
-
cell.getContext()
|
|
392
|
-
)
|
|
393
|
-
},
|
|
394
|
-
cell.id
|
|
395
|
-
);
|
|
396
|
-
})
|
|
397
|
-
},
|
|
398
|
-
row.id
|
|
399
|
-
);
|
|
400
|
-
})
|
|
401
|
-
] }, group.groupName)) })
|
|
483
|
+
) })
|
|
484
|
+
] })
|
|
402
485
|
] }) })
|
|
403
486
|
}
|
|
404
487
|
),
|
|
@@ -416,13 +499,20 @@ function AppCatalogGrid({
|
|
|
416
499
|
{
|
|
417
500
|
variant: "ghost",
|
|
418
501
|
size: "icon",
|
|
419
|
-
className: "absolute top-
|
|
502
|
+
className: "absolute top-4 right-4 z-10 hover:bg-accent",
|
|
420
503
|
onClick: handleClosePanel,
|
|
421
504
|
"aria-label": "Close details panel",
|
|
422
|
-
children: /* @__PURE__ */ jsx(X, { className: "h-
|
|
505
|
+
children: /* @__PURE__ */ jsx(X, { className: "h-5 w-5" })
|
|
423
506
|
}
|
|
424
507
|
),
|
|
425
|
-
/* @__PURE__ */ jsx(
|
|
508
|
+
/* @__PURE__ */ jsx(
|
|
509
|
+
AppDetails,
|
|
510
|
+
{
|
|
511
|
+
app: selectedApp,
|
|
512
|
+
onAppClick,
|
|
513
|
+
onClosePanel: handleClosePanel
|
|
514
|
+
}
|
|
515
|
+
)
|
|
426
516
|
] }) : null })
|
|
427
517
|
}
|
|
428
518
|
)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AppCatalogGrid.js","sources":["../../../../../../src/modules/appCatalog/ui/grid/AppCatalogGrid.tsx"],"sourcesContent":["import type {\n AppForCatalog,\n GroupingTagDefinition,\n} from '@igstack/app-catalog-backend-core'\nimport type { ColumnDef } from '@tanstack/react-table'\nimport {\n flexRender,\n getCoreRowModel,\n useReactTable,\n} from '@tanstack/react-table'\nimport { AppWindow, ExternalLink, X } from 'lucide-react'\nimport React, { useEffect, useState } from 'react'\n\nimport { cn } from '~/lib/utils'\nimport type {} from '~/types/table'\nimport { Badge } from '~/ui/badge'\nimport { Button } from '~/ui/button'\nimport {\n ResizableHandle,\n ResizablePanel,\n ResizablePanelGroup,\n} from '~/ui/resizable'\nimport {\n Table,\n TableBody,\n TableCell,\n TableHead,\n TableHeader,\n TableRow,\n} from '~/ui/table'\nimport { AccessRequestSection } from '../components/AccessRequestSection'\nimport { ScreenshotGallery } from '../components/ScreenshotGallery'\nimport { useAppCatalogContext } from '../../context/AppCatalogContext'\nimport { useAppClickHistory } from '../../hooks/useAppClickHistory'\nimport { useKeyboardNavigation } from '../hooks/useKeyboardNavigation'\n\nexport interface AppCatalogGridProps {\n apps: Array<AppForCatalog>\n selectedAppSlug?: string\n groupingDefinition?: GroupingTagDefinition\n onAppClick?: (app: AppForCatalog) => void\n /** Whether search is active (affects group sorting) */\n hasSearch?: boolean\n}\n\nfunction getIconUrl(iconName: string): string {\n return `/api/icons/${iconName}`\n}\n\nfunction AppIcon({\n app,\n className,\n}: {\n app: AppForCatalog\n className?: string\n}) {\n const [imageError, setImageError] = React.useState(false)\n\n // Use iconName from backend if available\n if (app.iconName && !imageError) {\n return (\n <div className={cn('size-12 shrink-0', className)}>\n <img\n src={getIconUrl(app.iconName)}\n alt={`${app.displayName} icon`}\n className=\"size-12 rounded-lg object-contain\"\n onError={() => setImageError(true)}\n />\n </div>\n )\n }\n\n // Fallback icon\n return (\n <div\n className={cn(\n 'flex items-center justify-center rounded-lg bg-primary/10 text-primary size-12 shrink-0',\n className,\n )}\n >\n <AppWindow className=\"size-6\" />\n </div>\n )\n}\n\nfunction AppScreenshot({ app }: { app: AppForCatalog }) {\n const [imageError, setImageError] = React.useState(false)\n const [isLoadingImage, setIsLoadingImage] = React.useState(true)\n\n // Check if app has screenshots\n const screenshotId = app.screenshotIds?.[0]\n if (!screenshotId) {\n return (\n <div className=\"w-full bg-muted/50 rounded-lg overflow-hidden flex items-center justify-center min-h-64\">\n <div className=\"w-full h-64 bg-muted/30 flex items-center justify-center text-muted-foreground text-sm\">\n No screenshot available\n </div>\n </div>\n )\n }\n\n const screenshotImageUrl = `/api/screenshots/${screenshotId}?size=512`\n\n return (\n <div className=\"w-full flex justify-center\">\n <div className=\"rounded-lg overflow-hidden inline-flex items-center justify-center min-h-64\">\n {!imageError ? (\n <img\n src={screenshotImageUrl}\n alt={`${app.displayName} screenshot`}\n className=\"h-64 object-contain\"\n onError={() => {\n setImageError(true)\n setIsLoadingImage(false)\n }}\n onLoad={() => setIsLoadingImage(false)}\n />\n ) : null}\n {(imageError || isLoadingImage) && (\n <div className=\"w-full h-64 bg-muted/30 flex items-center justify-center text-muted-foreground text-sm\">\n {isLoadingImage\n ? 'Loading screenshot...'\n : 'No screenshot available'}\n </div>\n )}\n </div>\n </div>\n )\n}\n\nfunction AppDetails({\n app,\n onAppClick,\n}: {\n app: AppForCatalog\n onAppClick?: (app: AppForCatalog) => void\n}) {\n const [isGalleryOpen, setIsGalleryOpen] = React.useState(false)\n const [galleryInitialIndex, setGalleryInitialIndex] = React.useState(0)\n const { approvalMethods, apps } = useAppCatalogContext()\n const { recordClick } = useAppClickHistory()\n\n const handleScreenshotClick = (index: number) => {\n setGalleryInitialIndex(index)\n setIsGalleryOpen(true)\n }\n\n // Find replacement app if deprecated\n const replacementApp = app.deprecated?.replacementSlug\n ? apps.find((a) => a.slug === app.deprecated?.replacementSlug)\n : null\n\n return (\n <>\n <div className=\"flex h-full flex-col p-6\">\n {/* Icon and Title */}\n <div className=\"flex items-center gap-4 border-b pb-6\">\n <AppIcon app={app} className=\"size-16\" />\n <div>\n <div className=\"flex items-center gap-2\">\n <h2 className=\"text-2xl font-semibold\">{app.displayName}</h2>\n {app.deprecated &&\n (() => {\n const deprecationType = app.deprecated.type || 'deprecated'\n return (\n <Badge\n variant={\n deprecationType === 'discouraged'\n ? 'secondary'\n : 'destructive'\n }\n >\n {deprecationType === 'discouraged'\n ? 'Discouraged'\n : 'Deprecated'}\n </Badge>\n )\n })()}\n </div>\n {app.appUrl && (\n <a\n href={app.appUrl}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n onClick={() => recordClick(app.slug)}\n className=\"mt-1 inline-flex items-center gap-1 text-sm text-muted-foreground hover:text-primary\"\n >\n {app.appUrl.replaceAll(/https?:\\/\\//g, '')}\n <ExternalLink className=\"size-3\" />\n </a>\n )}\n </div>\n </div>\n\n {/* Deprecation/Discouraged Warning */}\n {app.deprecated &&\n (() => {\n const deprecationType = app.deprecated.type || 'deprecated'\n const isDiscouraged = deprecationType === 'discouraged'\n return (\n <div\n className={\n isDiscouraged\n ? 'mt-6 p-4 border border-yellow-500/50 rounded-lg bg-yellow-50 dark:bg-yellow-950/20'\n : 'mt-6 p-4 border border-destructive/50 rounded-lg bg-destructive/10'\n }\n >\n <h3\n className={\n isDiscouraged\n ? 'text-sm font-semibold text-yellow-700 dark:text-yellow-500 mb-2'\n : 'text-sm font-semibold text-destructive mb-2'\n }\n >\n {isDiscouraged\n ? 'Usage discouraged'\n : 'This application is deprecated'}\n </h3>\n <p className=\"text-sm text-muted-foreground mb-3\">\n {app.deprecated.comment}\n </p>\n {replacementApp && (\n <button\n onClick={() => onAppClick?.(replacementApp)}\n className=\"text-sm font-medium text-primary hover:underline inline-flex items-center gap-1\"\n >\n View replacement: {replacementApp.displayName}\n <ExternalLink className=\"size-3\" />\n </button>\n )}\n </div>\n )\n })()}\n\n {/* Description */}\n {app.description && (\n <div className=\"mt-6\">\n <h3 className=\"mb-2 text-sm font-medium\">Description</h3>\n <p className=\"text-sm text-muted-foreground\">{app.description}</p>\n </div>\n )}\n\n {/* Screenshots - Clickable preview */}\n {app.screenshotIds && app.screenshotIds.length > 0 && (\n <div className=\"mt-6\">\n <h3 className=\"mb-2 text-sm font-medium\">\n Screenshots ({app.screenshotIds.length})\n </h3>\n <div\n className=\"cursor-pointer hover:opacity-80 transition-opacity\"\n onClick={() => handleScreenshotClick(0)}\n >\n <AppScreenshot app={app} />\n {app.screenshotIds.length > 1 && (\n <p className=\"text-xs text-muted-foreground mt-2 text-center\">\n Click to view all {app.screenshotIds.length} screenshots\n </p>\n )}\n </div>\n </div>\n )}\n\n {/* Access Request Section */}\n <AccessRequestSection app={app} approvalMethods={approvalMethods} />\n\n {/* Tags */}\n {app.tags && app.tags.length > 0 && (\n <div className=\"mt-6\">\n <h3 className=\"mb-2 text-sm font-medium\">Tags</h3>\n <div className=\"flex flex-wrap gap-2\">\n {app.tags.map((tag) => (\n <Badge key={tag} variant=\"secondary\" className=\"text-xs\">\n {tag}\n </Badge>\n ))}\n </div>\n </div>\n )}\n\n {/* Teams */}\n {app.teams && app.teams.length > 0 && (\n <div className=\"mt-6\">\n <h3 className=\"mb-2 text-sm font-medium\">Teams</h3>\n <div className=\"flex flex-wrap gap-2\">\n {app.teams.map((team) => (\n <Badge key={team} variant=\"outline\" className=\"text-xs\">\n {team}\n </Badge>\n ))}\n </div>\n </div>\n )}\n </div>\n\n {/* Screenshot Gallery Dialog */}\n <ScreenshotGallery\n app={app}\n screenshotIds={app.screenshotIds || []}\n open={isGalleryOpen}\n onOpenChange={setIsGalleryOpen}\n initialIndex={galleryInitialIndex}\n title={`${app.displayName} - Screenshots`}\n />\n </>\n )\n}\n\ninterface GroupedApps {\n groupName: string\n apps: Array<AppForCatalog>\n}\n\nfunction groupApps(\n apps: Array<AppForCatalog>,\n groupingDef?: GroupingTagDefinition,\n hasSearch?: boolean,\n): Array<GroupedApps> {\n if (!groupingDef) {\n const sortedApps = [...apps].sort((a, b) =>\n a.displayName.localeCompare(b.displayName),\n )\n return [{ groupName: 'All Apps', apps: sortedApps }]\n }\n\n const grouped = new Map<string, Array<AppForCatalog>>()\n const ungrouped: Array<AppForCatalog> = []\n\n for (const app of apps) {\n const matchingTag = app.tags?.find((tag) =>\n tag.startsWith(`${groupingDef.prefix}:`),\n )\n\n if (matchingTag) {\n const value = matchingTag.split(':')[1]\n if (value) {\n const tagValue = groupingDef.values.find((v) => v.value === value)\n const displayName = tagValue?.displayName || value\n\n if (!grouped.has(displayName)) {\n grouped.set(displayName, [])\n }\n grouped.get(displayName)!.push(app)\n } else {\n ungrouped.push(app)\n }\n } else {\n ungrouped.push(app)\n }\n }\n\n const result: Array<GroupedApps> = []\n for (const [groupName, appsInGroup] of grouped) {\n // Sort apps alphabetically within each group\n const sortedGroupApps = appsInGroup.sort((a, b) =>\n a.displayName.localeCompare(b.displayName),\n )\n result.push({ groupName, apps: sortedGroupApps })\n }\n\n if (ungrouped.length > 0) {\n // Sort ungrouped apps alphabetically\n const sortedUngrouped = ungrouped.sort((a, b) =>\n a.displayName.localeCompare(b.displayName),\n )\n result.push({ groupName: 'Other', apps: sortedUngrouped })\n }\n\n // Sort groups: when no search, sort by app count descending\n // When search is active, keep the order based on app relevance\n if (!hasSearch) {\n result.sort((a, b) => b.apps.length - a.apps.length)\n }\n\n return result\n}\n\nexport function AppCatalogGrid({\n apps,\n selectedAppSlug,\n groupingDefinition,\n onAppClick,\n hasSearch = false,\n}: AppCatalogGridProps) {\n const selectedApp = selectedAppSlug\n ? apps.find((a) => a.slug === selectedAppSlug)\n : null\n\n const groupedApps = groupApps(apps, groupingDefinition, hasSearch)\n\n // Flatten grouped apps to get display order for keyboard navigation\n const appsInDisplayOrder = React.useMemo(\n () => groupedApps.flatMap((group) => group.apps),\n [groupedApps],\n )\n\n // Use keyboard navigation hook with apps in display order\n const { rowRefs } = useKeyboardNavigation({\n apps: appsInDisplayOrder,\n selectedAppSlug,\n onAppClick,\n })\n\n // Define columns\n const columns = React.useMemo<Array<ColumnDef<AppForCatalog>>>(\n () => [\n {\n id: 'application',\n header: 'Application',\n cell: ({ row }) => (\n <div className=\"flex items-center gap-3\">\n <AppIcon app={row.original} className=\"size-6\" />\n <div className=\"flex items-center gap-2\">\n <span className=\"font-medium\">\n {row.original.displayName || 'Unnamed App'}\n </span>\n {row.original.deprecated &&\n (() => {\n const deprecationType =\n row.original.deprecated.type || 'deprecated'\n return (\n <span className=\"text-[0.7rem] text-muted-foreground\">\n (\n {deprecationType === 'discouraged'\n ? 'Discouraged'\n : 'Deprecated'}\n )\n </span>\n )\n })()}\n </div>\n </div>\n ),\n meta: {\n className: 'w-[300px]',\n },\n },\n {\n id: 'description',\n header: 'Description',\n cell: ({ row }) => (\n <span className=\"text-sm text-muted-foreground line-clamp-2\">\n {row.original.description || '—'}\n </span>\n ),\n },\n ],\n [],\n )\n\n // Create a single table instance with all apps\n const table = useReactTable({\n data: apps,\n columns,\n getCoreRowModel: getCoreRowModel(),\n getRowId: (row) => row.id,\n })\n\n // Panel visibility state - default to closed\n const [isPanelOpen, setIsPanelOpen] = useState(false)\n\n // Open panel when app is selected\n useEffect(() => {\n if (selectedApp) {\n setIsPanelOpen(true)\n }\n }, [selectedApp])\n\n // Auto-scroll to selected app (only on initial load)\n const hasScrolledRef = React.useRef(false)\n React.useEffect(() => {\n // Only scroll once on initial load if there's a selection\n if (selectedAppSlug && !hasScrolledRef.current) {\n const rowElement = rowRefs.current.get(selectedAppSlug)\n if (rowElement) {\n rowElement.scrollIntoView({ behavior: 'smooth', block: 'center' })\n }\n hasScrolledRef.current = true\n }\n }, [selectedAppSlug, rowRefs])\n\n const handleAppClick = (app: AppForCatalog) => {\n onAppClick?.(app)\n }\n\n const handleClosePanel = () => {\n setIsPanelOpen(false)\n }\n\n return (\n <ResizablePanelGroup orientation=\"horizontal\" className=\"h-full w-full\">\n {/* Left Panel - Table */}\n <ResizablePanel\n defaultSize={isPanelOpen ? 60 : 100}\n minSize={30}\n className=\"overflow-hidden\"\n >\n <div className=\"h-full overflow-y-auto pr-2 pb-6 [scrollbar-gutter:stable]\">\n <Table>\n <TableHeader className=\"sticky top-0 border-b bg-background z-10\">\n {table.getHeaderGroups().map((headerGroup) => (\n <TableRow key={headerGroup.id}>\n {headerGroup.headers.map((header) => (\n <TableHead\n key={header.id}\n className={cn(\n 'px-4 py-3 text-left font-medium text-sm',\n header.column.columnDef.meta?.className,\n )}\n >\n {header.isPlaceholder\n ? null\n : flexRender(\n header.column.columnDef.header,\n header.getContext(),\n )}\n </TableHead>\n ))}\n </TableRow>\n ))}\n </TableHeader>\n\n <TableBody>\n {groupedApps.map((group) => (\n <React.Fragment key={group.groupName}>\n {/* Group Header Row */}\n <TableRow className=\"bg-muted/50 hover:bg-muted/50\">\n <TableCell\n colSpan={columns.length}\n className=\"px-4 py-6 sticky top-[49px] bg-muted/90 backdrop-blur z-10\"\n >\n <div className=\"flex items-center justify-center\">\n <span className=\"font-bold text-lg tracking-widest uppercase leading-loose text-muted-foreground\">\n {group.groupName}\n </span>\n </div>\n </TableCell>\n </TableRow>\n\n {/* Group Apps */}\n {group.apps.map((app) => {\n const row = table\n .getRowModel()\n .rows.find((r) => r.id === app.id)\n if (!row) return null\n\n return (\n <TableRow\n key={row.id}\n ref={(el) => {\n if (el && row.original.slug) {\n rowRefs.current.set(row.original.slug, el)\n } else if (row.original.slug) {\n rowRefs.current.delete(row.original.slug)\n }\n }}\n onClick={() => handleAppClick(row.original)}\n className={cn(\n 'border-b cursor-pointer transition-colors',\n selectedApp?.id === row.original.id\n ? 'bg-blue-100 dark:bg-blue-950 hover:bg-blue-200 dark:hover:bg-blue-900'\n : 'hover:bg-muted/30',\n )}\n >\n {row.getVisibleCells().map((cell) => (\n <TableCell\n key={cell.id}\n className={cn(\n 'px-4 py-4',\n cell.column.columnDef.meta?.className,\n )}\n >\n {flexRender(\n cell.column.columnDef.cell,\n cell.getContext(),\n )}\n </TableCell>\n ))}\n </TableRow>\n )\n })}\n </React.Fragment>\n ))}\n </TableBody>\n </Table>\n </div>\n </ResizablePanel>\n\n {/* Right Panel - Details (only render when panel is open) */}\n {isPanelOpen && (\n <>\n {/* Resizable Handle */}\n <ResizableHandle withHandle />\n\n <ResizablePanel\n defaultSize={40}\n minSize={25}\n className=\"overflow-hidden\"\n >\n <div className=\"h-full overflow-y-auto border-l bg-background pl-4\">\n {selectedApp ? (\n <div className=\"relative\">\n <Button\n variant=\"ghost\"\n size=\"icon\"\n className=\"absolute top-2 right-2 z-10\"\n onClick={handleClosePanel}\n aria-label=\"Close details panel\"\n >\n <X className=\"h-4 w-4\" />\n </Button>\n <AppDetails app={selectedApp} onAppClick={onAppClick} />\n </div>\n ) : null}\n </div>\n </ResizablePanel>\n </>\n )}\n </ResizablePanelGroup>\n )\n}\n"],"names":["React","_a"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"AppCatalogGrid.js","sources":["../../../../../../src/modules/appCatalog/ui/grid/AppCatalogGrid.tsx"],"sourcesContent":["import type {\n AppForCatalog,\n GroupingTagDefinition,\n} from '@igstack/app-catalog-backend-core'\nimport type { ColumnDef } from '@tanstack/react-table'\nimport {\n flexRender,\n getCoreRowModel,\n useReactTable,\n} from '@tanstack/react-table'\nimport { AppWindow, ExternalLink, X } from 'lucide-react'\nimport React, { useEffect, useState } from 'react'\nimport { useHotkeys } from 'react-hotkeys-hook'\n\nimport { cn } from '~/lib/utils'\nimport type {} from '~/types/table'\nimport { Badge } from '~/ui/badge'\nimport { Button } from '~/ui/button'\nimport {\n ResizableHandle,\n ResizablePanel,\n ResizablePanelGroup,\n} from '~/ui/resizable'\nimport {\n Table,\n TableBody,\n TableCell,\n TableHead,\n TableHeader,\n TableRow,\n} from '~/ui/table'\nimport { AccessRequestSection } from '../components/AccessRequestSection'\nimport { ScreenshotGallery } from '../components/ScreenshotGallery'\nimport { useAppCatalogContext } from '../../context/AppCatalogContext'\nimport { useAppClickHistory } from '../../hooks/useAppClickHistory'\nimport { useKeyboardNavigation } from '../hooks/useKeyboardNavigation'\n\nexport interface AppCatalogGridProps {\n apps: Array<AppForCatalog>\n selectedAppSlug?: string\n groupingDefinition?: GroupingTagDefinition\n onAppClick?: (app: AppForCatalog) => void\n /** Whether search is active (affects group sorting) */\n hasSearch?: boolean\n /** Total count of apps before filtering */\n totalAppsCount?: number\n /** Callback to clear all filters and search */\n onClearFilters?: () => void\n}\n\nfunction getIconUrl(iconName: string): string {\n return `/api/icons/${iconName}`\n}\n\nfunction AppIcon({\n app,\n className,\n}: {\n app: AppForCatalog\n className?: string\n}) {\n const [imageError, setImageError] = React.useState(false)\n\n // Use iconName from backend if available\n if (app.iconName && !imageError) {\n return (\n <div className={cn('size-12 shrink-0', className)}>\n <img\n src={getIconUrl(app.iconName)}\n alt={`${app.displayName} icon`}\n className=\"size-12 rounded-lg object-contain\"\n onError={() => setImageError(true)}\n />\n </div>\n )\n }\n\n // Fallback icon\n return (\n <div\n className={cn(\n 'flex items-center justify-center rounded-lg bg-primary/10 text-primary size-12 shrink-0',\n className,\n )}\n >\n <AppWindow className=\"size-6\" />\n </div>\n )\n}\n\nfunction AppScreenshot({ app }: { app: AppForCatalog }) {\n const [imageError, setImageError] = React.useState(false)\n const [isLoadingImage, setIsLoadingImage] = React.useState(true)\n\n // Check if app has screenshots\n const screenshotId = app.screenshotIds?.[0]\n if (!screenshotId) {\n return (\n <div className=\"w-full bg-muted/50 rounded-lg overflow-hidden flex items-center justify-center min-h-64\">\n <div className=\"w-full h-64 bg-muted/30 flex items-center justify-center text-muted-foreground text-sm\">\n No screenshot available\n </div>\n </div>\n )\n }\n\n const screenshotImageUrl = `/api/screenshots/${screenshotId}?size=512`\n\n return (\n <div className=\"w-full flex justify-center\">\n <div className=\"rounded-lg overflow-hidden inline-flex items-center justify-center min-h-64\">\n {!imageError ? (\n <img\n src={screenshotImageUrl}\n alt={`${app.displayName} screenshot`}\n className=\"h-64 object-contain\"\n onError={() => {\n setImageError(true)\n setIsLoadingImage(false)\n }}\n onLoad={() => setIsLoadingImage(false)}\n />\n ) : null}\n {(imageError || isLoadingImage) && (\n <div className=\"w-full h-64 bg-muted/30 flex items-center justify-center text-muted-foreground text-sm\">\n {isLoadingImage\n ? 'Loading screenshot...'\n : 'No screenshot available'}\n </div>\n )}\n </div>\n </div>\n )\n}\n\nfunction AppDetails({\n app,\n onAppClick,\n onClosePanel,\n}: {\n app: AppForCatalog\n onAppClick?: (app: AppForCatalog) => void\n onClosePanel: () => void\n}) {\n const [isGalleryOpen, setIsGalleryOpen] = React.useState(false)\n const [galleryInitialIndex, setGalleryInitialIndex] = React.useState(0)\n const { approvalMethods, apps } = useAppCatalogContext()\n const { recordClick } = useAppClickHistory()\n\n // Enter: open screenshot gallery\n useHotkeys(\n 'enter',\n () => {\n const tag = document.activeElement?.tagName\n if (\n tag === 'BUTTON' ||\n tag === 'A' ||\n tag === 'INPUT' ||\n tag === 'SELECT' ||\n tag === 'TEXTAREA'\n )\n return\n\n if (app.screenshotIds && app.screenshotIds.length > 0) {\n setGalleryInitialIndex(0)\n setIsGalleryOpen(true)\n }\n },\n { enabled: !isGalleryOpen },\n [app, isGalleryOpen],\n )\n\n // Esc: close the details panel (only when gallery is NOT open)\n useHotkeys(\n 'escape',\n () => {\n onClosePanel()\n },\n { enabled: !isGalleryOpen },\n [isGalleryOpen, onClosePanel],\n )\n\n const handleScreenshotClick = (index: number) => {\n setGalleryInitialIndex(index)\n setIsGalleryOpen(true)\n }\n\n // Find replacement app if deprecated\n const replacementApp = app.deprecated?.replacementSlug\n ? apps.find((a) => a.slug === app.deprecated?.replacementSlug)\n : null\n\n return (\n <>\n <div className=\"flex h-full flex-col p-6\">\n {/* Icon and Title */}\n <div className=\"border-b pb-6\">\n <div className=\"flex items-center gap-3\">\n <AppIcon app={app} className=\"size-16\" />\n <div className=\"-mx-3\">\n <div className=\"flex items-center gap-2 px-3\">\n <h2 className=\"text-2xl font-semibold\">{app.displayName}</h2>\n {app.deprecated &&\n (() => {\n const deprecationType = app.deprecated.type || 'deprecated'\n return (\n <Badge\n variant={\n deprecationType === 'discouraged'\n ? 'secondary'\n : 'destructive'\n }\n >\n {deprecationType === 'discouraged'\n ? 'Discouraged'\n : 'Deprecated'}\n </Badge>\n )\n })()}\n </div>\n {app.appUrl && (\n <a\n href={app.appUrl}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n onClick={() => recordClick(app.slug)}\n className=\"mt-1 inline-flex items-center gap-1 rounded-md px-3 py-1 text-sm text-blue-600 hover:bg-accent/30 hover:underline dark:text-blue-400 transition-all group\"\n >\n {app.appUrl.replaceAll(/https?:\\/\\//g, '')}\n <ExternalLink className=\"size-3.5 shrink-0 opacity-40 group-hover:opacity-100 transition-opacity\" />\n </a>\n )}\n </div>\n </div>\n </div>\n\n {/* Deprecation/Discouraged Warning */}\n {app.deprecated &&\n (() => {\n const deprecationType = app.deprecated.type || 'deprecated'\n const isDiscouraged = deprecationType === 'discouraged'\n return (\n <div\n className={\n isDiscouraged\n ? 'mt-6 p-4 border border-yellow-500/50 rounded-lg bg-yellow-50 dark:bg-yellow-950/20'\n : 'mt-6 p-4 border border-destructive/50 rounded-lg bg-destructive/10'\n }\n >\n <h3\n className={\n isDiscouraged\n ? 'text-sm font-semibold text-yellow-700 dark:text-yellow-500 mb-2'\n : 'text-sm font-semibold text-destructive mb-2'\n }\n >\n {isDiscouraged\n ? 'Usage discouraged'\n : 'This application is deprecated'}\n </h3>\n <p className=\"text-sm text-muted-foreground mb-3\">\n {app.deprecated.comment}\n </p>\n {replacementApp && (\n <button\n onClick={() => onAppClick?.(replacementApp)}\n className=\"text-sm font-medium text-primary hover:underline inline-flex items-center gap-1\"\n >\n View replacement: {replacementApp.displayName}\n <ExternalLink className=\"size-3\" />\n </button>\n )}\n </div>\n )\n })()}\n\n {/* Description */}\n {app.description && (\n <div className=\"mt-6\">\n <h3 className=\"mb-2 text-sm font-medium\">Description</h3>\n <p className=\"text-sm text-muted-foreground\">{app.description}</p>\n </div>\n )}\n\n {/* Screenshots - Clickable preview */}\n {app.screenshotIds && app.screenshotIds.length > 0 && (\n <div className=\"mt-6\">\n <h3 className=\"mb-2 text-sm font-medium\">\n Screenshots ({app.screenshotIds.length})\n </h3>\n <div\n className=\"cursor-pointer hover:opacity-80 transition-opacity\"\n onClick={() => handleScreenshotClick(0)}\n >\n <AppScreenshot app={app} />\n {app.screenshotIds.length > 1 && (\n <p className=\"text-xs text-muted-foreground mt-2 text-center\">\n Click to view all {app.screenshotIds.length} screenshots\n </p>\n )}\n </div>\n </div>\n )}\n\n {/* Access Request Section */}\n <AccessRequestSection app={app} approvalMethods={approvalMethods} />\n\n {/* Links */}\n {app.links && app.links.length > 0 && (\n <div className=\"mt-4\">\n <h3 className=\"mb-1 text-xs font-medium text-muted-foreground\">\n Links\n </h3>\n <div className=\"space-y-0.5\">\n {app.links.map((link) => (\n <a\n key={link.url}\n href={link.url}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"flex items-center gap-1 text-xs text-muted-foreground hover:text-primary truncate\"\n >\n <ExternalLink className=\"size-3 shrink-0\" />\n {link.title || link.url.replaceAll(/https?:\\/\\//g, '')}\n </a>\n ))}\n </div>\n </div>\n )}\n\n {/* Tags */}\n {app.tags && app.tags.length > 0 && (\n <div className=\"mt-6\">\n <h3 className=\"mb-2 text-sm font-medium\">Tags</h3>\n <div className=\"flex flex-wrap gap-2\">\n {app.tags.map((tag) => (\n <Badge key={tag} variant=\"secondary\" className=\"text-xs\">\n {tag}\n </Badge>\n ))}\n </div>\n </div>\n )}\n\n {/* Teams */}\n {app.teams && app.teams.length > 0 && (\n <div className=\"mt-6\">\n <h3 className=\"mb-2 text-sm font-medium\">Teams</h3>\n <div className=\"flex flex-wrap gap-2\">\n {app.teams.map((team) => (\n <Badge key={team} variant=\"outline\" className=\"text-xs\">\n {team}\n </Badge>\n ))}\n </div>\n </div>\n )}\n\n {/* Sources */}\n {app.sources && app.sources.length > 0 && (\n <div className=\"mt-6\">\n <h3 className=\"mb-2 text-sm font-medium\">Sources</h3>\n <ol className=\"list-decimal list-inside space-y-1\">\n {app.sources.map((source, index) => (\n <li key={index} className=\"text-xs text-muted-foreground\">\n <a\n href={source}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"hover:text-primary inline-flex items-center gap-1\"\n >\n {source.replaceAll(/https?:\\/\\//g, '')}\n <ExternalLink className=\"size-3 shrink-0\" />\n </a>\n </li>\n ))}\n </ol>\n </div>\n )}\n </div>\n\n {/* Screenshot Gallery Dialog */}\n <ScreenshotGallery\n app={app}\n screenshotIds={app.screenshotIds || []}\n open={isGalleryOpen}\n onOpenChange={setIsGalleryOpen}\n initialIndex={galleryInitialIndex}\n title={`${app.displayName} - Screenshots`}\n />\n </>\n )\n}\n\ninterface GroupedApps {\n groupName: string\n apps: Array<AppForCatalog>\n}\n\nfunction groupApps(\n apps: Array<AppForCatalog>,\n groupingDef?: GroupingTagDefinition,\n hasSearch?: boolean,\n): Array<GroupedApps> {\n if (!groupingDef) {\n const sortedApps = [...apps].sort((a, b) =>\n a.displayName.localeCompare(b.displayName),\n )\n return [{ groupName: 'All Apps', apps: sortedApps }]\n }\n\n const grouped = new Map<string, Array<AppForCatalog>>()\n const ungrouped: Array<AppForCatalog> = []\n\n for (const app of apps) {\n const matchingTag = app.tags?.find((tag) =>\n tag.startsWith(`${groupingDef.prefix}:`),\n )\n\n if (matchingTag) {\n const value = matchingTag.split(':')[1]\n if (value) {\n const tagValue = groupingDef.values.find((v) => v.value === value)\n const displayName = tagValue?.displayName || value\n\n if (!grouped.has(displayName)) {\n grouped.set(displayName, [])\n }\n grouped.get(displayName)!.push(app)\n } else {\n ungrouped.push(app)\n }\n } else {\n ungrouped.push(app)\n }\n }\n\n const result: Array<GroupedApps> = []\n for (const [groupName, appsInGroup] of grouped) {\n // Sort apps alphabetically within each group\n const sortedGroupApps = appsInGroup.sort((a, b) =>\n a.displayName.localeCompare(b.displayName),\n )\n result.push({ groupName, apps: sortedGroupApps })\n }\n\n if (ungrouped.length > 0) {\n // Sort ungrouped apps alphabetically\n const sortedUngrouped = ungrouped.sort((a, b) =>\n a.displayName.localeCompare(b.displayName),\n )\n result.push({ groupName: 'Other', apps: sortedUngrouped })\n }\n\n // Sort groups: when no search, sort by app count descending\n // When search is active, keep the order based on app relevance\n if (!hasSearch) {\n result.sort((a, b) => b.apps.length - a.apps.length)\n }\n\n return result\n}\n\nexport function AppCatalogGrid({\n apps,\n selectedAppSlug,\n groupingDefinition,\n onAppClick,\n hasSearch = false,\n totalAppsCount,\n onClearFilters,\n}: AppCatalogGridProps) {\n const selectedApp = selectedAppSlug\n ? apps.find((a) => a.slug === selectedAppSlug)\n : null\n\n const groupedApps = groupApps(apps, groupingDefinition, hasSearch)\n\n // Flatten grouped apps to get display order for keyboard navigation\n const appsInDisplayOrder = React.useMemo(\n () => groupedApps.flatMap((group) => group.apps),\n [groupedApps],\n )\n\n // Use keyboard navigation hook with apps in display order\n const { rowRefs } = useKeyboardNavigation({\n apps: appsInDisplayOrder,\n selectedAppSlug,\n onAppClick,\n })\n\n // Define columns\n const columns = React.useMemo<Array<ColumnDef<AppForCatalog>>>(\n () => [\n {\n id: 'application',\n header: 'Application',\n cell: ({ row }) => (\n <div className=\"flex items-center gap-3\">\n <AppIcon app={row.original} className=\"size-6\" />\n <div className=\"flex items-center gap-2\">\n <span className=\"font-medium\">\n {row.original.displayName || 'Unnamed App'}\n </span>\n {row.original.deprecated &&\n (() => {\n const deprecationType =\n row.original.deprecated.type || 'deprecated'\n return (\n <span className=\"text-[0.7rem] text-muted-foreground\">\n (\n {deprecationType === 'discouraged'\n ? 'Discouraged'\n : 'Deprecated'}\n )\n </span>\n )\n })()}\n </div>\n </div>\n ),\n meta: {\n className: 'w-[300px]',\n },\n },\n {\n id: 'description',\n header: 'Description',\n cell: ({ row }) => (\n <span className=\"text-sm text-muted-foreground line-clamp-2\">\n {row.original.description || '—'}\n </span>\n ),\n },\n ],\n [],\n )\n\n // Create a single table instance with all apps\n const table = useReactTable({\n data: apps,\n columns,\n getCoreRowModel: getCoreRowModel(),\n getRowId: (row) => row.id,\n })\n\n // Panel visibility state - default to closed\n const [isPanelOpen, setIsPanelOpen] = useState(false)\n\n // Open panel when app is selected\n useEffect(() => {\n if (selectedApp) {\n setIsPanelOpen(true)\n }\n }, [selectedApp])\n\n // Auto-scroll to selected app (only on initial load)\n const hasScrolledRef = React.useRef(false)\n React.useEffect(() => {\n // Only scroll once on initial load if there's a selection\n if (selectedAppSlug && !hasScrolledRef.current) {\n const rowElement = rowRefs.current.get(selectedAppSlug)\n if (rowElement) {\n rowElement.scrollIntoView({ behavior: 'smooth', block: 'center' })\n }\n hasScrolledRef.current = true\n }\n }, [selectedAppSlug, rowRefs])\n\n const handleAppClick = (app: AppForCatalog) => {\n onAppClick?.(app)\n }\n\n const handleClosePanel = () => {\n setIsPanelOpen(false)\n }\n\n return (\n <ResizablePanelGroup orientation=\"horizontal\" className=\"h-full w-full\">\n {/* Left Panel - Table */}\n <ResizablePanel\n defaultSize={isPanelOpen ? 60 : 100}\n minSize={30}\n className=\"overflow-hidden\"\n >\n <div className=\"h-full overflow-y-auto pr-2 pb-6 [scrollbar-gutter:stable]\">\n <Table>\n <TableHeader className=\"sticky top-0 border-b bg-background z-10\">\n {table.getHeaderGroups().map((headerGroup) => (\n <TableRow key={headerGroup.id}>\n {headerGroup.headers.map((header) => (\n <TableHead\n key={header.id}\n className={cn(\n 'px-4 py-3 text-left font-medium text-sm',\n header.column.columnDef.meta?.className,\n )}\n >\n {header.isPlaceholder\n ? null\n : flexRender(\n header.column.columnDef.header,\n header.getContext(),\n )}\n </TableHead>\n ))}\n </TableRow>\n ))}\n </TableHeader>\n\n <TableBody>\n {groupedApps.map((group) => (\n <React.Fragment key={group.groupName}>\n {/* Group Header Row */}\n <TableRow className=\"bg-muted/50 hover:bg-muted/50\">\n <TableCell\n colSpan={columns.length}\n className=\"px-4 py-6 sticky top-[49px] bg-muted/90 backdrop-blur z-10\"\n >\n <div className=\"flex items-center justify-center\">\n <span className=\"font-bold text-lg tracking-widest uppercase leading-loose text-muted-foreground\">\n {group.groupName}\n </span>\n </div>\n </TableCell>\n </TableRow>\n\n {/* Group Apps */}\n {group.apps.map((app) => {\n const row = table\n .getRowModel()\n .rows.find((r) => r.id === app.id)\n if (!row) return null\n\n return (\n <TableRow\n key={row.id}\n ref={(el) => {\n if (el && row.original.slug) {\n rowRefs.current.set(row.original.slug, el)\n } else if (row.original.slug) {\n rowRefs.current.delete(row.original.slug)\n }\n }}\n onClick={() => handleAppClick(row.original)}\n className={cn(\n 'border-b cursor-pointer transition-colors',\n selectedApp?.id === row.original.id\n ? 'bg-blue-100 dark:bg-blue-950 hover:bg-blue-200 dark:hover:bg-blue-900'\n : 'hover:bg-muted/30',\n )}\n >\n {row.getVisibleCells().map((cell) => (\n <TableCell\n key={cell.id}\n className={cn(\n 'px-4 py-4',\n cell.column.columnDef.meta?.className,\n )}\n >\n {flexRender(\n cell.column.columnDef.cell,\n cell.getContext(),\n )}\n </TableCell>\n ))}\n </TableRow>\n )\n })}\n </React.Fragment>\n ))}\n\n {/* Clear Filters Row */}\n {totalAppsCount &&\n totalAppsCount > apps.length &&\n onClearFilters && (\n <TableRow>\n <TableCell\n colSpan={columns.length}\n className=\"px-4 py-8 text-center\"\n >\n <Button\n variant=\"outline\"\n onClick={onClearFilters}\n className=\"gap-2\"\n >\n <X className=\"h-4 w-4\" />\n Clear filters to show all apps ({totalAppsCount})\n </Button>\n </TableCell>\n </TableRow>\n )}\n </TableBody>\n </Table>\n </div>\n </ResizablePanel>\n\n {/* Right Panel - Details (only render when panel is open) */}\n {isPanelOpen && (\n <>\n {/* Resizable Handle */}\n <ResizableHandle withHandle />\n\n <ResizablePanel\n defaultSize={40}\n minSize={25}\n className=\"overflow-hidden\"\n >\n <div className=\"h-full overflow-y-auto border-l bg-background pl-4\">\n {selectedApp ? (\n <div className=\"relative\">\n <Button\n variant=\"ghost\"\n size=\"icon\"\n className=\"absolute top-4 right-4 z-10 hover:bg-accent\"\n onClick={handleClosePanel}\n aria-label=\"Close details panel\"\n >\n <X className=\"h-5 w-5\" />\n </Button>\n <AppDetails\n app={selectedApp}\n onAppClick={onAppClick}\n onClosePanel={handleClosePanel}\n />\n </div>\n ) : null}\n </div>\n </ResizablePanel>\n </>\n )}\n </ResizablePanelGroup>\n )\n}\n"],"names":["React","_a"],"mappings":"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"}
|
|
@@ -62,9 +62,26 @@ function AppCatalogPage() {
|
|
|
62
62
|
topAppSlugs,
|
|
63
63
|
searchValue: deferredSearchValue
|
|
64
64
|
});
|
|
65
|
+
useEffect(() => {
|
|
66
|
+
if (filteredApps.length === 1 && filteredApps[0]) {
|
|
67
|
+
setSelectedAppSlug(filteredApps[0].slug);
|
|
68
|
+
}
|
|
69
|
+
}, [filteredApps, setSelectedAppSlug]);
|
|
65
70
|
const handleAppClick = (app) => {
|
|
66
71
|
setSelectedAppSlug(app.slug);
|
|
67
72
|
};
|
|
73
|
+
const handleClearFilters = () => {
|
|
74
|
+
setSearchValue("");
|
|
75
|
+
actions.clearAllFilters();
|
|
76
|
+
setSelectedAppSlug(void 0);
|
|
77
|
+
};
|
|
78
|
+
const totalAppsCount = useMemo(() => {
|
|
79
|
+
let count = apps.length;
|
|
80
|
+
if (!filterState.showDeprecated) {
|
|
81
|
+
count = apps.filter((app) => !app.deprecated).length;
|
|
82
|
+
}
|
|
83
|
+
return count;
|
|
84
|
+
}, [apps, filterState.showDeprecated]);
|
|
68
85
|
if (isLoadingApps) {
|
|
69
86
|
return /* @__PURE__ */ jsx("div", { className: "py-6 text-muted-foreground", children: "Loading…" });
|
|
70
87
|
}
|
|
@@ -111,7 +128,9 @@ function AppCatalogPage() {
|
|
|
111
128
|
selectedAppSlug,
|
|
112
129
|
groupingDefinition,
|
|
113
130
|
onAppClick: handleAppClick,
|
|
114
|
-
hasSearch: !!deferredSearchValue
|
|
131
|
+
hasSearch: !!deferredSearchValue,
|
|
132
|
+
totalAppsCount,
|
|
133
|
+
onClearFilters: handleClearFilters
|
|
115
134
|
}
|
|
116
135
|
) })
|
|
117
136
|
] });
|