@igstack/app-catalog-frontend-core 0.9.2 → 0.9.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/esm/__tests__/integration/crossRefLinkListView.integration.test.d.ts +0 -0
- package/dist/esm/__tests__/integration/sourcesAlignment.integration.test.d.ts +0 -0
- package/dist/esm/__tests__/modules/appCatalog/utils/markdownToPlainText.test.d.ts +1 -0
- package/dist/esm/modules/appCatalog/ui/components/SubResourcesSection.js +2 -1
- package/dist/esm/modules/appCatalog/ui/components/SubResourcesSection.js.map +1 -1
- package/dist/esm/modules/appCatalog/ui/filters/FilterCombobox.js +2 -1
- package/dist/esm/modules/appCatalog/ui/filters/FilterCombobox.js.map +1 -1
- package/dist/esm/modules/appCatalog/ui/grid/AppCatalogGrid.js +4 -3
- package/dist/esm/modules/appCatalog/ui/grid/AppCatalogGrid.js.map +1 -1
- package/dist/esm/modules/appCatalog/utils/markdownToPlainText.d.ts +16 -0
- package/dist/esm/modules/appCatalog/utils/markdownToPlainText.js +8 -0
- package/dist/esm/modules/appCatalog/utils/markdownToPlainText.js.map +1 -0
- package/package.json +4 -4
- package/src/__tests__/integration/crossRefLinkListView.integration.test.ts +49 -0
- package/src/__tests__/integration/sourcesAlignment.integration.test.tsx +48 -0
- package/src/__tests__/modules/appCatalog/utils/markdownToPlainText.test.ts +57 -0
- package/src/modules/appCatalog/ui/components/SubResourcesSection.tsx +2 -1
- package/src/modules/appCatalog/ui/filters/FilterCombobox.tsx +2 -1
- package/src/modules/appCatalog/ui/grid/AppCatalogGrid.tsx +8 -3
- package/src/modules/appCatalog/ui/grid/AppCatalogTable.tsx +4 -1
- package/src/modules/appCatalog/utils/markdownToPlainText.ts +31 -0
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -20,6 +20,7 @@ import "@radix-ui/react-tooltip";
|
|
|
20
20
|
import "../../../../ui/empty.js";
|
|
21
21
|
import "../../../../ui/button.js";
|
|
22
22
|
import { getGroupBySlug } from "../../utils/resolveHelpers.js";
|
|
23
|
+
import { markdownToPlainText } from "../../utils/markdownToPlainText.js";
|
|
23
24
|
function getTierBadgeVariant(tierSlug) {
|
|
24
25
|
if (tierSlug === "prod" || tierSlug === "production") return "destructive";
|
|
25
26
|
if (tierSlug === "dev" || tierSlug === "staging") return "secondary";
|
|
@@ -125,7 +126,7 @@ function SubResourcesSection({
|
|
|
125
126
|
/* @__PURE__ */ jsxs(TableCell, { children: [
|
|
126
127
|
/* @__PURE__ */ jsx("div", { className: "font-medium text-sm", children: sr.displayName }),
|
|
127
128
|
(sr.aliases ?? []).length > 0 && /* @__PURE__ */ jsx("div", { className: "text-xs text-muted-foreground mt-0.5", children: (sr.aliases ?? []).join(", ") }),
|
|
128
|
-
sr.description && /* @__PURE__ */ jsx("div", { className: "text-xs text-muted-foreground mt-0.5", children: sr.description })
|
|
129
|
+
sr.description && /* @__PURE__ */ jsx("div", { className: "text-xs text-muted-foreground mt-0.5", children: markdownToPlainText(sr.description) })
|
|
129
130
|
] }),
|
|
130
131
|
/* @__PURE__ */ jsx(TableCell, { children: sr.tier && /* @__PURE__ */ jsx(
|
|
131
132
|
Badge,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SubResourcesSection.js","sources":["../../../../../../src/modules/appCatalog/ui/components/SubResourcesSection.tsx"],"sourcesContent":["import type { Resource } from '@igstack/app-catalog-backend-core'\nimport { Search } from 'lucide-react'\nimport { useMemo, useState } from 'react'\nimport { Badge } from '~/ui/badge'\nimport { Input } from '~/ui/input'\nimport {\n Table,\n TableBody,\n TableCell,\n TableHead,\n TableHeader,\n TableRow,\n} from '~/ui/table'\nimport { PersonBadge } from './PersonBadge'\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from '~/ui/select'\nimport { useAppCatalogContext } from '~/modules/appCatalog'\nimport { getGroupBySlug } from '~/modules/appCatalog/utils/resolveHelpers'\n\ninterface SubResourcesSectionProps {\n subResources: Resource[]\n}\n\nfunction getTierBadgeVariant(\n tierSlug: string,\n): 'default' | 'secondary' | 'destructive' | 'outline' {\n if (tierSlug === 'prod' || tierSlug === 'production') return 'destructive'\n if (tierSlug === 'dev' || tierSlug === 'staging') return 'secondary'\n if (tierSlug === 'preprod') return 'outline'\n if (tierSlug === 'sandbox') return 'outline'\n return 'outline'\n}\n\nfunction getTierBadgeClassName(tierSlug: string): string {\n if (tierSlug === 'preprod')\n return 'border-amber-400 bg-amber-100 text-amber-800 hover:bg-amber-200'\n if (tierSlug === 'sandbox')\n return 'border-gray-400 bg-gray-100 text-gray-700 hover:bg-gray-200'\n return ''\n}\n\nfunction getTierDisplayLabel(tierSlug: string): string {\n if (tierSlug === 'preprod') return 'Pre-Prod'\n if (tierSlug === 'sandbox') return 'Sandbox'\n if (tierSlug === 'prod' || tierSlug === 'production') return 'Prod'\n if (tierSlug === 'dev') return 'Dev'\n if (tierSlug === 'staging') return 'Staging'\n return tierSlug\n}\n\nexport function SubResourcesSection({\n subResources,\n}: SubResourcesSectionProps) {\n const { groups } = useAppCatalogContext()\n const [search, setSearch] = useState('')\n const [tierFilter, setTierFilter] = useState<string>('all')\n\n const uniqueTiers = useMemo(() => {\n const tiers = new Set<string>()\n for (const sr of subResources) {\n if (sr.tier) tiers.add(sr.tier)\n }\n return [...tiers].sort()\n }, [subResources])\n\n const filtered = useMemo(() => {\n let result = subResources\n\n if (tierFilter !== 'all') {\n result = result.filter((sr) => sr.tier === tierFilter)\n }\n\n if (search.trim()) {\n const q = search.trim().toLowerCase()\n result = result.filter(\n (sr) =>\n sr.displayName.toLowerCase().includes(q) ||\n (sr.aliases ?? []).some((a) => a.toLowerCase().includes(q)) ||\n (sr.description?.toLowerCase().includes(q) ?? false),\n )\n }\n\n return result\n }, [subResources, search, tierFilter])\n\n if (subResources.length === 0) return null\n\n return (\n <div className=\"space-y-3\">\n <div className=\"flex items-center justify-between\">\n <div className=\"text-sm font-medium\">\n Sub-Resources ({filtered.length} of {subResources.length})\n </div>\n </div>\n\n {/* Filters */}\n <div className=\"flex gap-2\">\n <div className=\"relative flex-1\">\n <Search className=\"absolute left-2.5 top-2.5 size-4 text-muted-foreground\" />\n <Input\n placeholder=\"Search resources by name or alias...\"\n value={search}\n onChange={(e) => setSearch(e.target.value)}\n className=\"pl-9 h-9\"\n />\n </div>\n {uniqueTiers.length > 1 && (\n <Select value={tierFilter} onValueChange={setTierFilter}>\n <SelectTrigger className=\"w-[130px] h-9\">\n <SelectValue placeholder=\"All tiers\" />\n </SelectTrigger>\n <SelectContent>\n <SelectItem value=\"all\">All tiers</SelectItem>\n {uniqueTiers.map((tier) => (\n <SelectItem key={tier} value={tier}>\n {tier}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n )}\n </div>\n\n {/* Table */}\n <div className=\"rounded-lg border max-h-[400px] overflow-auto\">\n <Table>\n <TableHeader>\n <TableRow>\n <TableHead>Name</TableHead>\n <TableHead className=\"w-[80px]\">Tier</TableHead>\n <TableHead>Owner</TableHead>\n <TableHead>Access Contacts</TableHead>\n </TableRow>\n </TableHeader>\n <TableBody>\n {filtered.length === 0 ? (\n <TableRow>\n <TableCell\n colSpan={4}\n className=\"text-center text-muted-foreground py-8\"\n >\n No resources match your filters\n </TableCell>\n </TableRow>\n ) : (\n filtered.map((sr) => {\n // Resolve maintainer group members\n const maintainerMembers = (\n sr.accessMaintainerGroupSlugs ?? []\n ).flatMap((groupSlug) => {\n const group = getGroupBySlug(groups, groupSlug)\n return group?.memberSlugs ?? []\n })\n // Deduplicate\n const uniqueMaintainers = [...new Set(maintainerMembers)]\n\n return (\n <TableRow key={sr.slug}>\n <TableCell>\n <div className=\"font-medium text-sm\">\n {sr.displayName}\n </div>\n {(sr.aliases ?? []).length > 0 && (\n <div className=\"text-xs text-muted-foreground mt-0.5\">\n {(sr.aliases ?? []).join(', ')}\n </div>\n )}\n {sr.description && (\n <div className=\"text-xs text-muted-foreground mt-0.5\">\n {sr.description}\n </div>\n )}\n </TableCell>\n <TableCell>\n {sr.tier && (\n <Badge\n variant={getTierBadgeVariant(sr.tier)}\n className={`text-xs ${getTierBadgeClassName(sr.tier)}`}\n >\n {getTierDisplayLabel(sr.tier)}\n </Badge>\n )}\n </TableCell>\n <TableCell>\n {sr.ownerPersonSlug && (\n <PersonBadge slug={sr.ownerPersonSlug} />\n )}\n </TableCell>\n <TableCell>\n {uniqueMaintainers.length > 0 ? (\n <div className=\"flex flex-wrap gap-1\">\n {uniqueMaintainers.map((personSlug) => (\n <PersonBadge key={personSlug} slug={personSlug} />\n ))}\n </div>\n ) : (\n <span className=\"text-muted-foreground\">-</span>\n )}\n </TableCell>\n </TableRow>\n )\n })\n )}\n </TableBody>\n </Table>\n </div>\n </div>\n )\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"SubResourcesSection.js","sources":["../../../../../../src/modules/appCatalog/ui/components/SubResourcesSection.tsx"],"sourcesContent":["import type { Resource } from '@igstack/app-catalog-backend-core'\nimport { Search } from 'lucide-react'\nimport { useMemo, useState } from 'react'\nimport { Badge } from '~/ui/badge'\nimport { Input } from '~/ui/input'\nimport {\n Table,\n TableBody,\n TableCell,\n TableHead,\n TableHeader,\n TableRow,\n} from '~/ui/table'\nimport { PersonBadge } from './PersonBadge'\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from '~/ui/select'\nimport { useAppCatalogContext } from '~/modules/appCatalog'\nimport { getGroupBySlug } from '~/modules/appCatalog/utils/resolveHelpers'\nimport { markdownToPlainText } from '~/modules/appCatalog/utils/markdownToPlainText'\n\ninterface SubResourcesSectionProps {\n subResources: Resource[]\n}\n\nfunction getTierBadgeVariant(\n tierSlug: string,\n): 'default' | 'secondary' | 'destructive' | 'outline' {\n if (tierSlug === 'prod' || tierSlug === 'production') return 'destructive'\n if (tierSlug === 'dev' || tierSlug === 'staging') return 'secondary'\n if (tierSlug === 'preprod') return 'outline'\n if (tierSlug === 'sandbox') return 'outline'\n return 'outline'\n}\n\nfunction getTierBadgeClassName(tierSlug: string): string {\n if (tierSlug === 'preprod')\n return 'border-amber-400 bg-amber-100 text-amber-800 hover:bg-amber-200'\n if (tierSlug === 'sandbox')\n return 'border-gray-400 bg-gray-100 text-gray-700 hover:bg-gray-200'\n return ''\n}\n\nfunction getTierDisplayLabel(tierSlug: string): string {\n if (tierSlug === 'preprod') return 'Pre-Prod'\n if (tierSlug === 'sandbox') return 'Sandbox'\n if (tierSlug === 'prod' || tierSlug === 'production') return 'Prod'\n if (tierSlug === 'dev') return 'Dev'\n if (tierSlug === 'staging') return 'Staging'\n return tierSlug\n}\n\nexport function SubResourcesSection({\n subResources,\n}: SubResourcesSectionProps) {\n const { groups } = useAppCatalogContext()\n const [search, setSearch] = useState('')\n const [tierFilter, setTierFilter] = useState<string>('all')\n\n const uniqueTiers = useMemo(() => {\n const tiers = new Set<string>()\n for (const sr of subResources) {\n if (sr.tier) tiers.add(sr.tier)\n }\n return [...tiers].sort()\n }, [subResources])\n\n const filtered = useMemo(() => {\n let result = subResources\n\n if (tierFilter !== 'all') {\n result = result.filter((sr) => sr.tier === tierFilter)\n }\n\n if (search.trim()) {\n const q = search.trim().toLowerCase()\n result = result.filter(\n (sr) =>\n sr.displayName.toLowerCase().includes(q) ||\n (sr.aliases ?? []).some((a) => a.toLowerCase().includes(q)) ||\n (sr.description?.toLowerCase().includes(q) ?? false),\n )\n }\n\n return result\n }, [subResources, search, tierFilter])\n\n if (subResources.length === 0) return null\n\n return (\n <div className=\"space-y-3\">\n <div className=\"flex items-center justify-between\">\n <div className=\"text-sm font-medium\">\n Sub-Resources ({filtered.length} of {subResources.length})\n </div>\n </div>\n\n {/* Filters */}\n <div className=\"flex gap-2\">\n <div className=\"relative flex-1\">\n <Search className=\"absolute left-2.5 top-2.5 size-4 text-muted-foreground\" />\n <Input\n placeholder=\"Search resources by name or alias...\"\n value={search}\n onChange={(e) => setSearch(e.target.value)}\n className=\"pl-9 h-9\"\n />\n </div>\n {uniqueTiers.length > 1 && (\n <Select value={tierFilter} onValueChange={setTierFilter}>\n <SelectTrigger className=\"w-[130px] h-9\">\n <SelectValue placeholder=\"All tiers\" />\n </SelectTrigger>\n <SelectContent>\n <SelectItem value=\"all\">All tiers</SelectItem>\n {uniqueTiers.map((tier) => (\n <SelectItem key={tier} value={tier}>\n {tier}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n )}\n </div>\n\n {/* Table */}\n <div className=\"rounded-lg border max-h-[400px] overflow-auto\">\n <Table>\n <TableHeader>\n <TableRow>\n <TableHead>Name</TableHead>\n <TableHead className=\"w-[80px]\">Tier</TableHead>\n <TableHead>Owner</TableHead>\n <TableHead>Access Contacts</TableHead>\n </TableRow>\n </TableHeader>\n <TableBody>\n {filtered.length === 0 ? (\n <TableRow>\n <TableCell\n colSpan={4}\n className=\"text-center text-muted-foreground py-8\"\n >\n No resources match your filters\n </TableCell>\n </TableRow>\n ) : (\n filtered.map((sr) => {\n // Resolve maintainer group members\n const maintainerMembers = (\n sr.accessMaintainerGroupSlugs ?? []\n ).flatMap((groupSlug) => {\n const group = getGroupBySlug(groups, groupSlug)\n return group?.memberSlugs ?? []\n })\n // Deduplicate\n const uniqueMaintainers = [...new Set(maintainerMembers)]\n\n return (\n <TableRow key={sr.slug}>\n <TableCell>\n <div className=\"font-medium text-sm\">\n {sr.displayName}\n </div>\n {(sr.aliases ?? []).length > 0 && (\n <div className=\"text-xs text-muted-foreground mt-0.5\">\n {(sr.aliases ?? []).join(', ')}\n </div>\n )}\n {sr.description && (\n <div className=\"text-xs text-muted-foreground mt-0.5\">\n {markdownToPlainText(sr.description)}\n </div>\n )}\n </TableCell>\n <TableCell>\n {sr.tier && (\n <Badge\n variant={getTierBadgeVariant(sr.tier)}\n className={`text-xs ${getTierBadgeClassName(sr.tier)}`}\n >\n {getTierDisplayLabel(sr.tier)}\n </Badge>\n )}\n </TableCell>\n <TableCell>\n {sr.ownerPersonSlug && (\n <PersonBadge slug={sr.ownerPersonSlug} />\n )}\n </TableCell>\n <TableCell>\n {uniqueMaintainers.length > 0 ? (\n <div className=\"flex flex-wrap gap-1\">\n {uniqueMaintainers.map((personSlug) => (\n <PersonBadge key={personSlug} slug={personSlug} />\n ))}\n </div>\n ) : (\n <span className=\"text-muted-foreground\">-</span>\n )}\n </TableCell>\n </TableRow>\n )\n })\n )}\n </TableBody>\n </Table>\n </div>\n </div>\n )\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -5,6 +5,7 @@ import { cn } from "../../../../lib/utils.js";
|
|
|
5
5
|
import { Button } from "../../../../ui/button.js";
|
|
6
6
|
import { Command, CommandInput, CommandList, CommandEmpty, CommandGroup, CommandItem } from "../../../../ui/command.js";
|
|
7
7
|
import { Popover, PopoverTrigger, PopoverContent } from "../../../../ui/popover.js";
|
|
8
|
+
import { markdownToPlainText } from "../../utils/markdownToPlainText.js";
|
|
8
9
|
function FilterCombobox({
|
|
9
10
|
label,
|
|
10
11
|
options,
|
|
@@ -91,7 +92,7 @@ function FilterCombobox({
|
|
|
91
92
|
")"
|
|
92
93
|
] })
|
|
93
94
|
] }),
|
|
94
|
-
option.description && /* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: option.description })
|
|
95
|
+
option.description && /* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: markdownToPlainText(option.description) })
|
|
95
96
|
] })
|
|
96
97
|
]
|
|
97
98
|
},
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"FilterCombobox.js","sources":["../../../../../../src/modules/appCatalog/ui/filters/FilterCombobox.tsx"],"sourcesContent":["import { Check, ChevronsUpDown, X } from 'lucide-react'\nimport { useState } from 'react'\nimport { cn } from '~/lib/utils'\nimport { Button } from '~/ui/button'\nimport {\n Command,\n CommandEmpty,\n CommandGroup,\n CommandInput,\n CommandItem,\n CommandList,\n} from '~/ui/command'\nimport { Popover, PopoverContent, PopoverTrigger } from '~/ui/popover'\nimport type { TagOption } from '../context/AppCatalogFiltersContext'\n\ninterface FilterComboboxProps {\n /** Filter prefix (e.g., \"category\") */\n prefix: string\n /** Display label for the filter */\n label: string\n /** Available options */\n options: TagOption[]\n /** Currently selected value */\n value: string | undefined\n /** Counts for each option value */\n counts: Record<string, number>\n /** Callback when value changes */\n onValueChange: (value: string | undefined) => void\n}\n\n/**\n * Searchable combobox for selecting a tag filter value.\n * Shows a clear button (X) when a value is selected.\n */\nexport function FilterCombobox({\n label,\n options,\n value,\n counts,\n onValueChange,\n}: FilterComboboxProps) {\n const [open, setOpen] = useState(false)\n\n const selectedOption = options.find((opt) => opt.value === value)\n const selectedCount = value ? counts[value] || 0 : 0\n\n return (\n <div className=\"flex items-center gap-1\">\n <Popover open={open} onOpenChange={setOpen}>\n <PopoverTrigger asChild>\n <Button\n variant=\"outline\"\n role=\"combobox\"\n aria-expanded={open}\n className=\"min-w-[220px] justify-between\"\n >\n {selectedOption ? (\n <span className=\"truncate\">\n {selectedOption.displayName} ({selectedCount})\n </span>\n ) : (\n <span className=\"text-muted-foreground\">{label}</span>\n )}\n <ChevronsUpDown className=\"ml-2 h-4 w-4 shrink-0 opacity-50\" />\n </Button>\n </PopoverTrigger>\n <PopoverContent className=\"w-[320px] p-0\">\n <Command>\n <CommandInput placeholder={`Search ${label.toLowerCase()}...`} />\n <CommandList className=\"max-h-[400px]\">\n <CommandEmpty>No options found.</CommandEmpty>\n <CommandGroup>\n {/* Clear/Reset option */}\n <CommandItem\n value=\"__clear__\"\n onSelect={() => {\n onValueChange(undefined)\n setOpen(false)\n }}\n >\n <Check\n className={cn(\n 'mr-2 h-4 w-4',\n !value ? 'opacity-100' : 'opacity-0',\n )}\n />\n <span className=\"text-muted-foreground\">All</span>\n </CommandItem>\n\n {/* Regular options */}\n {options.map((option) => {\n const count = counts[option.value] || 0\n return (\n <CommandItem\n key={option.value}\n value={option.displayName}\n onSelect={() => {\n onValueChange(option.value)\n setOpen(false)\n }}\n >\n <Check\n className={cn(\n 'mr-2 h-4 w-4',\n value === option.value ? 'opacity-100' : 'opacity-0',\n )}\n />\n <div className=\"flex flex-col flex-1\">\n <div className=\"flex items-center justify-between gap-2\">\n <span>{option.displayName}</span>\n <span className=\"text-xs text-muted-foreground\">\n ({count})\n </span>\n </div>\n {option.description && (\n <span className=\"text-xs text-muted-foreground\">\n {option.description}\n </span>\n )}\n </div>\n </CommandItem>\n )\n })}\n </CommandGroup>\n </CommandList>\n </Command>\n </PopoverContent>\n </Popover>\n\n {/* Clear button (only shown when value is selected) */}\n {value && (\n <Button\n variant=\"ghost\"\n size=\"icon\"\n className=\"h-8 w-8\"\n onClick={() => onValueChange(undefined)}\n aria-label={`Clear ${label}`}\n >\n <X className=\"h-4 w-4\" />\n </Button>\n )}\n </div>\n )\n}\n"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"FilterCombobox.js","sources":["../../../../../../src/modules/appCatalog/ui/filters/FilterCombobox.tsx"],"sourcesContent":["import { Check, ChevronsUpDown, X } from 'lucide-react'\nimport { useState } from 'react'\nimport { cn } from '~/lib/utils'\nimport { Button } from '~/ui/button'\nimport {\n Command,\n CommandEmpty,\n CommandGroup,\n CommandInput,\n CommandItem,\n CommandList,\n} from '~/ui/command'\nimport { Popover, PopoverContent, PopoverTrigger } from '~/ui/popover'\nimport type { TagOption } from '../context/AppCatalogFiltersContext'\nimport { markdownToPlainText } from '../../utils/markdownToPlainText'\n\ninterface FilterComboboxProps {\n /** Filter prefix (e.g., \"category\") */\n prefix: string\n /** Display label for the filter */\n label: string\n /** Available options */\n options: TagOption[]\n /** Currently selected value */\n value: string | undefined\n /** Counts for each option value */\n counts: Record<string, number>\n /** Callback when value changes */\n onValueChange: (value: string | undefined) => void\n}\n\n/**\n * Searchable combobox for selecting a tag filter value.\n * Shows a clear button (X) when a value is selected.\n */\nexport function FilterCombobox({\n label,\n options,\n value,\n counts,\n onValueChange,\n}: FilterComboboxProps) {\n const [open, setOpen] = useState(false)\n\n const selectedOption = options.find((opt) => opt.value === value)\n const selectedCount = value ? counts[value] || 0 : 0\n\n return (\n <div className=\"flex items-center gap-1\">\n <Popover open={open} onOpenChange={setOpen}>\n <PopoverTrigger asChild>\n <Button\n variant=\"outline\"\n role=\"combobox\"\n aria-expanded={open}\n className=\"min-w-[220px] justify-between\"\n >\n {selectedOption ? (\n <span className=\"truncate\">\n {selectedOption.displayName} ({selectedCount})\n </span>\n ) : (\n <span className=\"text-muted-foreground\">{label}</span>\n )}\n <ChevronsUpDown className=\"ml-2 h-4 w-4 shrink-0 opacity-50\" />\n </Button>\n </PopoverTrigger>\n <PopoverContent className=\"w-[320px] p-0\">\n <Command>\n <CommandInput placeholder={`Search ${label.toLowerCase()}...`} />\n <CommandList className=\"max-h-[400px]\">\n <CommandEmpty>No options found.</CommandEmpty>\n <CommandGroup>\n {/* Clear/Reset option */}\n <CommandItem\n value=\"__clear__\"\n onSelect={() => {\n onValueChange(undefined)\n setOpen(false)\n }}\n >\n <Check\n className={cn(\n 'mr-2 h-4 w-4',\n !value ? 'opacity-100' : 'opacity-0',\n )}\n />\n <span className=\"text-muted-foreground\">All</span>\n </CommandItem>\n\n {/* Regular options */}\n {options.map((option) => {\n const count = counts[option.value] || 0\n return (\n <CommandItem\n key={option.value}\n value={option.displayName}\n onSelect={() => {\n onValueChange(option.value)\n setOpen(false)\n }}\n >\n <Check\n className={cn(\n 'mr-2 h-4 w-4',\n value === option.value ? 'opacity-100' : 'opacity-0',\n )}\n />\n <div className=\"flex flex-col flex-1\">\n <div className=\"flex items-center justify-between gap-2\">\n <span>{option.displayName}</span>\n <span className=\"text-xs text-muted-foreground\">\n ({count})\n </span>\n </div>\n {option.description && (\n <span className=\"text-xs text-muted-foreground\">\n {markdownToPlainText(option.description)}\n </span>\n )}\n </div>\n </CommandItem>\n )\n })}\n </CommandGroup>\n </CommandList>\n </Command>\n </PopoverContent>\n </Popover>\n\n {/* Clear button (only shown when value is selected) */}\n {value && (\n <Button\n variant=\"ghost\"\n size=\"icon\"\n className=\"h-8 w-8\"\n onClick={() => onValueChange(undefined)}\n aria-label={`Clear ${label}`}\n >\n <X className=\"h-4 w-4\" />\n </Button>\n )}\n </div>\n )\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -18,6 +18,7 @@ import { useAppCatalogContext } from "../../context/AppCatalogContext.js";
|
|
|
18
18
|
import { useAppClickHistory } from "../../hooks/useAppClickHistory.js";
|
|
19
19
|
import { useKeyboardNavigation } from "../hooks/useKeyboardNavigation.js";
|
|
20
20
|
import { highlightText } from "../../utils/searchApps.js";
|
|
21
|
+
import { markdownToPlainText } from "../../utils/markdownToPlainText.js";
|
|
21
22
|
import { formatRelativeTime } from "../../utils/formatRelativeTime.js";
|
|
22
23
|
import { TierVariantsSection } from "../components/TierVariantsSection.js";
|
|
23
24
|
import { SubResourcesSection } from "../components/SubResourcesSection.js";
|
|
@@ -320,7 +321,7 @@ function AppDetails({
|
|
|
320
321
|
{
|
|
321
322
|
className: "flex items-center gap-2 text-xs",
|
|
322
323
|
children: [
|
|
323
|
-
/* @__PURE__ */ jsxs("span", { className: "text-muted-foreground shrink-0", children: [
|
|
324
|
+
/* @__PURE__ */ jsxs("span", { className: "text-muted-foreground shrink-0 tabular-nums", children: [
|
|
324
325
|
index + 1,
|
|
325
326
|
"."
|
|
326
327
|
] }),
|
|
@@ -405,7 +406,7 @@ function AppDetails({
|
|
|
405
406
|
}
|
|
406
407
|
)
|
|
407
408
|
] }) : /* @__PURE__ */ jsx("ul", { className: "space-y-2", children: sourceUrls.map((url, index) => /* @__PURE__ */ jsxs("li", { className: "flex items-center gap-2 text-xs", children: [
|
|
408
|
-
/* @__PURE__ */ jsxs("span", { className: "text-muted-foreground shrink-0", children: [
|
|
409
|
+
/* @__PURE__ */ jsxs("span", { className: "text-muted-foreground shrink-0 tabular-nums", children: [
|
|
409
410
|
index + 1,
|
|
410
411
|
"."
|
|
411
412
|
] }),
|
|
@@ -582,7 +583,7 @@ function AppCatalogGrid({
|
|
|
582
583
|
/* @__PURE__ */ jsx("span", { className: "text-sm text-muted-foreground line-clamp-2", children: /* @__PURE__ */ jsx(
|
|
583
584
|
HighlightedText,
|
|
584
585
|
{
|
|
585
|
-
text: row.original.description
|
|
586
|
+
text: row.original.description ? markdownToPlainText(row.original.description) : "—",
|
|
586
587
|
searchQuery
|
|
587
588
|
}
|
|
588
589
|
) }),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AppCatalogGrid.js","sources":["../../../../../../src/modules/appCatalog/ui/grid/AppCatalogGrid.tsx"],"sourcesContent":["import type {\n GroupingTagDefinition,\n Resource,\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, Plus, Trash2, X } from 'lucide-react'\nimport React, { useState } from 'react'\nimport { useHotkeys } from 'react-hotkeys-hook'\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 { useUser } from '~/modules/auth'\nimport { InlineEditableField } from '../components/InlineEditableField'\nimport { MarkdownText } from '../components/MarkdownText'\nimport { ScreenshotGallery } from '../components/ScreenshotGallery'\nimport { useUpdateApp } from '../../hooks/useUpdateApp'\nimport { useAppCatalogContext } from '../../context/AppCatalogContext'\nimport { useAppClickHistory } from '../../hooks/useAppClickHistory'\nimport { useKeyboardNavigation } from '../hooks/useKeyboardNavigation'\nimport { highlightText } from '../../utils/searchApps'\nimport { formatRelativeTime } from '../../utils/formatRelativeTime'\nimport { TierVariantsSection } from '../components/TierVariantsSection'\nimport { SubResourcesSection } from '../components/SubResourcesSection'\nimport { getChildResources } from '../../utils/resolveHelpers'\n\nexport interface AppCatalogGridProps {\n apps: Resource[]\n selectedAppSlug?: string\n groupingDefinition?: GroupingTagDefinition\n onAppClick?: (app: Resource) => void\n /** Whether search is active (affects group sorting) */\n hasSearch?: boolean\n /** Search query for highlighting matches */\n searchQuery?: string\n /** Total count of apps before filtering */\n totalAppsCount?: number\n /** Callback to clear all filters and search */\n onClearFilters?: () => void\n /** Called when the user closes the detail panel (Esc or X) — e.g. to navigate back to the catalog URL */\n onClosePanel?: () => void\n}\n\nfunction getIconUrl(iconName: string): string {\n return `/api/icons/${iconName}`\n}\n\nfunction HighlightedText({\n text,\n searchQuery,\n}: {\n text: string\n searchQuery?: string\n}) {\n if (!searchQuery) {\n return <>{text}</>\n }\n\n const segments = highlightText(text, searchQuery)\n\n return (\n <>\n {segments.map((segment, index) =>\n segment.highlight ? (\n <mark\n key={index}\n className=\"bg-yellow-300 dark:bg-yellow-600/60 font-semibold text-gray-900 dark:text-gray-100\"\n >\n {segment.text}\n </mark>\n ) : (\n <React.Fragment key={index}>{segment.text}</React.Fragment>\n ),\n )}\n </>\n )\n}\n\nfunction AppIcon({ app, className }: { app: Resource; className?: string }) {\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.abbreviation || 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: Resource }) {\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.abbreviation || 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 TiersAndSubResourcesPanel({ app }: { app: Resource }) {\n const { resources } = useAppCatalogContext()\n const appSubResources = React.useMemo(\n () => getChildResources(resources, app.slug),\n [resources, app.slug],\n )\n\n return (\n <>\n {app.tiers && app.tiers.length > 0 && (\n <div className=\"mt-6\">\n <TierVariantsSection tiers={app.tiers} />\n </div>\n )}\n {appSubResources.length > 0 && (\n <div className=\"mt-6\">\n <SubResourcesSection subResources={appSubResources} />\n </div>\n )}\n </>\n )\n}\n\nfunction AppDetails({\n app,\n onAppClick,\n onClosePanel,\n}: {\n app: Resource\n onAppClick?: (app: Resource) => void\n onClosePanel: () => void\n}) {\n const [isGalleryOpen, setIsGalleryOpen] = React.useState(false)\n const [galleryInitialIndex, setGalleryInitialIndex] = React.useState(0)\n const { approvalMethods, resources: allResources } = useAppCatalogContext()\n const { recordClick } = useAppClickHistory()\n const updateApp = useUpdateApp()\n const [draftSource, setDraftSource] = React.useState<string | null>(null)\n const user = useUser()\n const isAdmin = user?.isAdmin ?? false\n\n const sourceUrls: string[] =\n app.sources?.map((s) => (typeof s === 'string' ? s : s.url)) ?? []\n const displaySources =\n draftSource !== null ? [...sourceUrls, draftSource] : sourceUrls\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 ? allResources.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 flex-1 min-w-0\">\n <div className=\"flex items-center gap-2 px-3\">\n <div className=\"text-2xl font-semibold min-w-0\">\n {app.abbreviation\n ? `${app.displayName} (${app.abbreviation})`\n : app.displayName}\n </div>\n {app.deprecated && (\n <Badge\n variant={\n app.deprecated.type === 'discouraged'\n ? 'secondary'\n : 'destructive'\n }\n >\n {app.deprecated.type === 'discouraged'\n ? 'Discouraged'\n : 'Deprecated'}\n </Badge>\n )}\n </div>\n {isAdmin && (\n <div className=\"mt-1 px-3\">\n <span className=\"text-xs text-muted-foreground mr-2\">\n Slug:\n </span>\n <InlineEditableField\n value={app.slug}\n onSave={(slug) =>\n updateApp.mutate({ id: app.id, data: { slug } })\n }\n className=\"text-sm\"\n />\n </div>\n )}\n <div className=\"mt-1 px-3\">\n {isAdmin ? (\n <InlineEditableField\n value={app.appUrl ?? ''}\n onSave={(appUrl) =>\n updateApp.mutate({ id: app.id, data: { appUrl } })\n }\n placeholder=\"App URL\"\n renderView={(url) =>\n url ? (\n <a\n href={url}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n onClick={() => recordClick(app.slug)}\n className=\"inline-flex items-center gap-1 rounded-md py-1 text-sm text-blue-600 hover:bg-accent/30 hover:underline dark:text-blue-400 transition-all\"\n >\n {url.replace(/https?:\\/\\//g, '')}\n <ExternalLink className=\"size-3.5 shrink-0 opacity-40 group-hover:opacity-100 transition-opacity\" />\n </a>\n ) : (\n <span className=\"text-muted-foreground\">—</span>\n )\n }\n />\n ) : app.appUrl ? (\n <a\n href={app.appUrl}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n onClick={() => recordClick(app.slug)}\n className=\"inline-flex items-center gap-1 rounded-md py-1 text-sm text-blue-600 hover:bg-accent/30 hover:underline dark:text-blue-400 transition-all\"\n >\n {app.appUrl.replace(/https?:\\/\\//g, '')}\n <ExternalLink className=\"size-3.5 shrink-0 opacity-40 transition-opacity\" />\n </a>\n ) : (\n <span className=\"text-muted-foreground\">—</span>\n )}\n </div>\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 type=\"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 <div className=\"mt-6\">\n <h3 className=\"mb-2 text-sm font-medium\">Description</h3>\n {isAdmin ? (\n <InlineEditableField\n value={app.description ?? ''}\n onSave={(description) =>\n updateApp.mutate({ id: app.id, data: { description } })\n }\n multiline\n placeholder=\"Description\"\n className=\"min-h-[4rem] resize-y text-sm text-muted-foreground\"\n />\n ) : app.description ? (\n <MarkdownText className=\"prose prose-sm max-w-none text-sm text-muted-foreground [&_p]:m-0\">\n {app.description}\n </MarkdownText>\n ) : (\n <p className=\"text-sm text-muted-foreground\">—</p>\n )}\n </div>\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 {/* Tier Variants and Sub-Resources */}\n <TiersAndSubResourcesPanel app={app} />\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.replace(/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 <div className=\"mt-6\">\n <h3 className=\"mb-2 text-sm font-medium\">Sources</h3>\n {isAdmin ? (\n <>\n <ul className=\"space-y-2\">\n {displaySources.map((url, index) => {\n const isDraft =\n draftSource !== null && index === sourceUrls.length\n return (\n <li\n key={isDraft ? 'draft' : `${index}-${url}`}\n className=\"flex items-center gap-2 text-xs\"\n >\n <span className=\"text-muted-foreground shrink-0\">\n {index + 1}.\n </span>\n <InlineEditableField\n value={url}\n initialEditMode={isDraft}\n onCancel={\n isDraft ? () => setDraftSource(null) : undefined\n }\n onSave={(newUrl) => {\n if (isDraft) {\n setDraftSource(null)\n if (newUrl) {\n updateApp.mutate({\n id: app.id,\n data: { sources: [...sourceUrls, newUrl] },\n })\n }\n } else {\n const next = [...sourceUrls]\n next[index] = newUrl\n updateApp.mutate({\n id: app.id,\n data: { sources: next.filter(Boolean) },\n })\n }\n }}\n placeholder=\"https://...\"\n viewClassName=\"flex-1 min-w-0\"\n renderView={(val) =>\n val ? (\n <a\n href={val}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"hover:text-primary inline-flex items-center gap-1 truncate\"\n >\n {val.replace(/https?:\\/\\//g, '')}\n <ExternalLink className=\"size-3 shrink-0\" />\n </a>\n ) : (\n <span className=\"text-muted-foreground\">—</span>\n )\n }\n />\n {!isDraft && (\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon-sm\"\n aria-label=\"Remove source\"\n className=\"shrink-0 text-muted-foreground hover:text-destructive\"\n onClick={() => {\n const next = sourceUrls.filter(\n (_, i) => i !== index,\n )\n updateApp.mutate({\n id: app.id,\n data: { sources: next },\n })\n }}\n >\n <Trash2 className=\"size-3.5\" />\n </Button>\n )}\n </li>\n )\n })}\n </ul>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n className=\"mt-2 gap-1 text-muted-foreground\"\n onClick={() => setDraftSource('')}\n >\n <Plus className=\"size-3.5\" />\n Add source\n </Button>\n </>\n ) : (\n <ul className=\"space-y-2\">\n {sourceUrls.map((url, index) => (\n <li key={index} className=\"flex items-center gap-2 text-xs\">\n <span className=\"text-muted-foreground shrink-0\">\n {index + 1}.\n </span>\n {url ? (\n <a\n href={url}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"hover:text-primary inline-flex items-center gap-1 truncate\"\n >\n {url.replace(/https?:\\/\\//g, '')}\n <ExternalLink className=\"size-3 shrink-0\" />\n </a>\n ) : (\n <span className=\"text-muted-foreground\">—</span>\n )}\n </li>\n ))}\n </ul>\n )}\n </div>\n\n {/* Freshness: muted \"last checked\" line, rarely-needed metadata. The\n backend owns the semantic (freshness.isStale); we only render. */}\n {app.freshness?.lastCheckedAt && (\n <p className=\"mt-4 text-xs text-muted-foreground\">\n <span title={app.freshness.lastCheckedAt}>\n Last checked {formatRelativeTime(app.freshness.lastCheckedAt)}\n </span>\n {app.freshness.isStale && (\n <span className=\"italic opacity-75\"> · may be out of date</span>\n )}\n </p>\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.abbreviation || app.displayName} - Screenshots`}\n />\n </>\n )\n}\n\ninterface GroupedApps {\n groupName: string\n apps: Resource[]\n}\n\nfunction groupApps(\n apps: Resource[],\n groupingDef?: GroupingTagDefinition,\n hasSearch?: boolean,\n): GroupedApps[] {\n // When search is active, skip grouping and preserve relevance order\n if (hasSearch) {\n return [{ groupName: 'All Apps', apps: [...apps] }]\n }\n\n if (!groupingDef) {\n // No grouping definition - sort alphabetically\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, Resource[]>()\n const ungrouped: Resource[] = []\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: GroupedApps[] = []\n for (const [groupName, appsInGroup] of grouped) {\n // Sort 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 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 by app count descending\n result.sort((a, b) => b.apps.length - a.apps.length)\n\n return result\n}\n\nexport function AppCatalogGrid({\n apps,\n selectedAppSlug,\n groupingDefinition,\n onAppClick,\n hasSearch = false,\n searchQuery,\n totalAppsCount,\n onClearFilters,\n onClosePanel,\n}: AppCatalogGridProps) {\n // Full, unfiltered resource set — the detail panel must resolve the open app\n // from this, not only from the filtered `apps`. Navigating to /app/<slug>\n // (e.g. a deprecated app's \"View replacement\" link, or any deep link) should\n // render that app like typing the URL in the browser, regardless of the\n // active search/filters (#12).\n const { resources: allResourcesForDetail } = useAppCatalogContext()\n const selectedApp = selectedAppSlug\n ? (apps.find((a) => a.slug === selectedAppSlug) ??\n allResourcesForDetail.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 // Build a map of parentSlug -> matched child resource displayName for search annotation\n const { resources: allResources2 } = useAppCatalogContext()\n const matchedSubResourceMap = React.useMemo(() => {\n const map = new Map<string, string>()\n if (!searchQuery?.trim() || allResources2.length === 0) return map\n const queryTerms = searchQuery\n .trim()\n .toLowerCase()\n .split(/\\s+/)\n .filter(Boolean)\n const allTermsMatch = (text: string): boolean =>\n queryTerms.every((term) => text.includes(term))\n\n for (const r of allResources2) {\n if (!r.parentSlug) continue\n if (map.has(r.parentSlug)) continue\n const nameMatch = allTermsMatch(r.displayName.toLowerCase())\n const aliasMatch = (r.aliases ?? []).some((a) =>\n allTermsMatch(a.toLowerCase()),\n )\n const descMatch = r.description\n ? allTermsMatch(r.description.toLowerCase())\n : false\n if (nameMatch || aliasMatch || descMatch) {\n map.set(r.parentSlug, r.displayName)\n }\n }\n return map\n }, [searchQuery, allResources2])\n\n // Define columns\n const columns = React.useMemo<ColumnDef<Resource>[]>(\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 flex-col\">\n <div className=\"flex items-center gap-2\">\n <span className=\"font-medium\">\n <HighlightedText\n text={\n row.original.abbreviation ||\n row.original.displayName ||\n 'Unnamed App'\n }\n searchQuery={searchQuery}\n />\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 {row.original.abbreviation && (\n <span className=\"text-xs text-muted-foreground\">\n <HighlightedText\n text={row.original.displayName}\n searchQuery={searchQuery}\n />\n </span>\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 <div>\n <span className=\"text-sm text-muted-foreground line-clamp-2\">\n <HighlightedText\n text={row.original.description || '—'}\n searchQuery={searchQuery}\n />\n </span>\n {matchedSubResourceMap.get(row.original.slug) && (\n <div className=\"text-xs text-primary mt-0.5\">\n Matched sub-resource:{' '}\n {matchedSubResourceMap.get(row.original.slug)}\n </div>\n )}\n </div>\n ),\n },\n ],\n [searchQuery, matchedSubResourceMap],\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 - derive from selectedApp and explicit close\n const [hasUserClosed, setHasUserClosed] = useState(false)\n\n // Auto-open when app is selected, unless user explicitly closed\n const isPanelOpen = selectedApp !== null && !hasUserClosed\n\n // Reset close flag when selectedApp changes\n React.useEffect(() => {\n if (selectedApp) {\n setHasUserClosed(false)\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: Resource) => {\n onAppClick?.(app)\n }\n\n const handleClosePanel = () => {\n setHasUserClosed(true)\n onClosePanel?.()\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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"}
|
|
1
|
+
{"version":3,"file":"AppCatalogGrid.js","sources":["../../../../../../src/modules/appCatalog/ui/grid/AppCatalogGrid.tsx"],"sourcesContent":["import type {\n GroupingTagDefinition,\n Resource,\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, Plus, Trash2, X } from 'lucide-react'\nimport React, { useState } from 'react'\nimport { useHotkeys } from 'react-hotkeys-hook'\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 { useUser } from '~/modules/auth'\nimport { InlineEditableField } from '../components/InlineEditableField'\nimport { MarkdownText } from '../components/MarkdownText'\nimport { ScreenshotGallery } from '../components/ScreenshotGallery'\nimport { useUpdateApp } from '../../hooks/useUpdateApp'\nimport { useAppCatalogContext } from '../../context/AppCatalogContext'\nimport { useAppClickHistory } from '../../hooks/useAppClickHistory'\nimport { useKeyboardNavigation } from '../hooks/useKeyboardNavigation'\nimport { highlightText } from '../../utils/searchApps'\nimport { markdownToPlainText } from '../../utils/markdownToPlainText'\nimport { formatRelativeTime } from '../../utils/formatRelativeTime'\nimport { TierVariantsSection } from '../components/TierVariantsSection'\nimport { SubResourcesSection } from '../components/SubResourcesSection'\nimport { getChildResources } from '../../utils/resolveHelpers'\n\nexport interface AppCatalogGridProps {\n apps: Resource[]\n selectedAppSlug?: string\n groupingDefinition?: GroupingTagDefinition\n onAppClick?: (app: Resource) => void\n /** Whether search is active (affects group sorting) */\n hasSearch?: boolean\n /** Search query for highlighting matches */\n searchQuery?: string\n /** Total count of apps before filtering */\n totalAppsCount?: number\n /** Callback to clear all filters and search */\n onClearFilters?: () => void\n /** Called when the user closes the detail panel (Esc or X) — e.g. to navigate back to the catalog URL */\n onClosePanel?: () => void\n}\n\nfunction getIconUrl(iconName: string): string {\n return `/api/icons/${iconName}`\n}\n\nfunction HighlightedText({\n text,\n searchQuery,\n}: {\n text: string\n searchQuery?: string\n}) {\n if (!searchQuery) {\n return <>{text}</>\n }\n\n const segments = highlightText(text, searchQuery)\n\n return (\n <>\n {segments.map((segment, index) =>\n segment.highlight ? (\n <mark\n key={index}\n className=\"bg-yellow-300 dark:bg-yellow-600/60 font-semibold text-gray-900 dark:text-gray-100\"\n >\n {segment.text}\n </mark>\n ) : (\n <React.Fragment key={index}>{segment.text}</React.Fragment>\n ),\n )}\n </>\n )\n}\n\nfunction AppIcon({ app, className }: { app: Resource; className?: string }) {\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.abbreviation || 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: Resource }) {\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.abbreviation || 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 TiersAndSubResourcesPanel({ app }: { app: Resource }) {\n const { resources } = useAppCatalogContext()\n const appSubResources = React.useMemo(\n () => getChildResources(resources, app.slug),\n [resources, app.slug],\n )\n\n return (\n <>\n {app.tiers && app.tiers.length > 0 && (\n <div className=\"mt-6\">\n <TierVariantsSection tiers={app.tiers} />\n </div>\n )}\n {appSubResources.length > 0 && (\n <div className=\"mt-6\">\n <SubResourcesSection subResources={appSubResources} />\n </div>\n )}\n </>\n )\n}\n\nfunction AppDetails({\n app,\n onAppClick,\n onClosePanel,\n}: {\n app: Resource\n onAppClick?: (app: Resource) => void\n onClosePanel: () => void\n}) {\n const [isGalleryOpen, setIsGalleryOpen] = React.useState(false)\n const [galleryInitialIndex, setGalleryInitialIndex] = React.useState(0)\n const { approvalMethods, resources: allResources } = useAppCatalogContext()\n const { recordClick } = useAppClickHistory()\n const updateApp = useUpdateApp()\n const [draftSource, setDraftSource] = React.useState<string | null>(null)\n const user = useUser()\n const isAdmin = user?.isAdmin ?? false\n\n const sourceUrls: string[] =\n app.sources?.map((s) => (typeof s === 'string' ? s : s.url)) ?? []\n const displaySources =\n draftSource !== null ? [...sourceUrls, draftSource] : sourceUrls\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 ? allResources.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 flex-1 min-w-0\">\n <div className=\"flex items-center gap-2 px-3\">\n <div className=\"text-2xl font-semibold min-w-0\">\n {app.abbreviation\n ? `${app.displayName} (${app.abbreviation})`\n : app.displayName}\n </div>\n {app.deprecated && (\n <Badge\n variant={\n app.deprecated.type === 'discouraged'\n ? 'secondary'\n : 'destructive'\n }\n >\n {app.deprecated.type === 'discouraged'\n ? 'Discouraged'\n : 'Deprecated'}\n </Badge>\n )}\n </div>\n {isAdmin && (\n <div className=\"mt-1 px-3\">\n <span className=\"text-xs text-muted-foreground mr-2\">\n Slug:\n </span>\n <InlineEditableField\n value={app.slug}\n onSave={(slug) =>\n updateApp.mutate({ id: app.id, data: { slug } })\n }\n className=\"text-sm\"\n />\n </div>\n )}\n <div className=\"mt-1 px-3\">\n {isAdmin ? (\n <InlineEditableField\n value={app.appUrl ?? ''}\n onSave={(appUrl) =>\n updateApp.mutate({ id: app.id, data: { appUrl } })\n }\n placeholder=\"App URL\"\n renderView={(url) =>\n url ? (\n <a\n href={url}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n onClick={() => recordClick(app.slug)}\n className=\"inline-flex items-center gap-1 rounded-md py-1 text-sm text-blue-600 hover:bg-accent/30 hover:underline dark:text-blue-400 transition-all\"\n >\n {url.replace(/https?:\\/\\//g, '')}\n <ExternalLink className=\"size-3.5 shrink-0 opacity-40 group-hover:opacity-100 transition-opacity\" />\n </a>\n ) : (\n <span className=\"text-muted-foreground\">—</span>\n )\n }\n />\n ) : app.appUrl ? (\n <a\n href={app.appUrl}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n onClick={() => recordClick(app.slug)}\n className=\"inline-flex items-center gap-1 rounded-md py-1 text-sm text-blue-600 hover:bg-accent/30 hover:underline dark:text-blue-400 transition-all\"\n >\n {app.appUrl.replace(/https?:\\/\\//g, '')}\n <ExternalLink className=\"size-3.5 shrink-0 opacity-40 transition-opacity\" />\n </a>\n ) : (\n <span className=\"text-muted-foreground\">—</span>\n )}\n </div>\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 type=\"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 <div className=\"mt-6\">\n <h3 className=\"mb-2 text-sm font-medium\">Description</h3>\n {isAdmin ? (\n <InlineEditableField\n value={app.description ?? ''}\n onSave={(description) =>\n updateApp.mutate({ id: app.id, data: { description } })\n }\n multiline\n placeholder=\"Description\"\n className=\"min-h-[4rem] resize-y text-sm text-muted-foreground\"\n />\n ) : app.description ? (\n <MarkdownText className=\"prose prose-sm max-w-none text-sm text-muted-foreground [&_p]:m-0\">\n {app.description}\n </MarkdownText>\n ) : (\n <p className=\"text-sm text-muted-foreground\">—</p>\n )}\n </div>\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 {/* Tier Variants and Sub-Resources */}\n <TiersAndSubResourcesPanel app={app} />\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.replace(/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 <div className=\"mt-6\">\n <h3 className=\"mb-2 text-sm font-medium\">Sources</h3>\n {isAdmin ? (\n <>\n <ul className=\"space-y-2\">\n {displaySources.map((url, index) => {\n const isDraft =\n draftSource !== null && index === sourceUrls.length\n return (\n <li\n key={isDraft ? 'draft' : `${index}-${url}`}\n className=\"flex items-center gap-2 text-xs\"\n >\n <span className=\"text-muted-foreground shrink-0 tabular-nums\">\n {index + 1}.\n </span>\n <InlineEditableField\n value={url}\n initialEditMode={isDraft}\n onCancel={\n isDraft ? () => setDraftSource(null) : undefined\n }\n onSave={(newUrl) => {\n if (isDraft) {\n setDraftSource(null)\n if (newUrl) {\n updateApp.mutate({\n id: app.id,\n data: { sources: [...sourceUrls, newUrl] },\n })\n }\n } else {\n const next = [...sourceUrls]\n next[index] = newUrl\n updateApp.mutate({\n id: app.id,\n data: { sources: next.filter(Boolean) },\n })\n }\n }}\n placeholder=\"https://...\"\n viewClassName=\"flex-1 min-w-0\"\n renderView={(val) =>\n val ? (\n <a\n href={val}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"hover:text-primary inline-flex items-center gap-1 truncate\"\n >\n {val.replace(/https?:\\/\\//g, '')}\n <ExternalLink className=\"size-3 shrink-0\" />\n </a>\n ) : (\n <span className=\"text-muted-foreground\">—</span>\n )\n }\n />\n {!isDraft && (\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon-sm\"\n aria-label=\"Remove source\"\n className=\"shrink-0 text-muted-foreground hover:text-destructive\"\n onClick={() => {\n const next = sourceUrls.filter(\n (_, i) => i !== index,\n )\n updateApp.mutate({\n id: app.id,\n data: { sources: next },\n })\n }}\n >\n <Trash2 className=\"size-3.5\" />\n </Button>\n )}\n </li>\n )\n })}\n </ul>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n className=\"mt-2 gap-1 text-muted-foreground\"\n onClick={() => setDraftSource('')}\n >\n <Plus className=\"size-3.5\" />\n Add source\n </Button>\n </>\n ) : (\n <ul className=\"space-y-2\">\n {sourceUrls.map((url, index) => (\n <li key={index} className=\"flex items-center gap-2 text-xs\">\n <span className=\"text-muted-foreground shrink-0 tabular-nums\">\n {index + 1}.\n </span>\n {url ? (\n <a\n href={url}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"hover:text-primary inline-flex items-center gap-1 truncate\"\n >\n {url.replace(/https?:\\/\\//g, '')}\n <ExternalLink className=\"size-3 shrink-0\" />\n </a>\n ) : (\n <span className=\"text-muted-foreground\">—</span>\n )}\n </li>\n ))}\n </ul>\n )}\n </div>\n\n {/* Freshness: muted \"last checked\" line, rarely-needed metadata. The\n backend owns the semantic (freshness.isStale); we only render. */}\n {app.freshness?.lastCheckedAt && (\n <p className=\"mt-4 text-xs text-muted-foreground\">\n <span title={app.freshness.lastCheckedAt}>\n Last checked {formatRelativeTime(app.freshness.lastCheckedAt)}\n </span>\n {app.freshness.isStale && (\n <span className=\"italic opacity-75\"> · may be out of date</span>\n )}\n </p>\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.abbreviation || app.displayName} - Screenshots`}\n />\n </>\n )\n}\n\ninterface GroupedApps {\n groupName: string\n apps: Resource[]\n}\n\nfunction groupApps(\n apps: Resource[],\n groupingDef?: GroupingTagDefinition,\n hasSearch?: boolean,\n): GroupedApps[] {\n // When search is active, skip grouping and preserve relevance order\n if (hasSearch) {\n return [{ groupName: 'All Apps', apps: [...apps] }]\n }\n\n if (!groupingDef) {\n // No grouping definition - sort alphabetically\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, Resource[]>()\n const ungrouped: Resource[] = []\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: GroupedApps[] = []\n for (const [groupName, appsInGroup] of grouped) {\n // Sort 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 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 by app count descending\n result.sort((a, b) => b.apps.length - a.apps.length)\n\n return result\n}\n\nexport function AppCatalogGrid({\n apps,\n selectedAppSlug,\n groupingDefinition,\n onAppClick,\n hasSearch = false,\n searchQuery,\n totalAppsCount,\n onClearFilters,\n onClosePanel,\n}: AppCatalogGridProps) {\n // Full, unfiltered resource set — the detail panel must resolve the open app\n // from this, not only from the filtered `apps`. Navigating to /app/<slug>\n // (e.g. a deprecated app's \"View replacement\" link, or any deep link) should\n // render that app like typing the URL in the browser, regardless of the\n // active search/filters (#12).\n const { resources: allResourcesForDetail } = useAppCatalogContext()\n const selectedApp = selectedAppSlug\n ? (apps.find((a) => a.slug === selectedAppSlug) ??\n allResourcesForDetail.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 // Build a map of parentSlug -> matched child resource displayName for search annotation\n const { resources: allResources2 } = useAppCatalogContext()\n const matchedSubResourceMap = React.useMemo(() => {\n const map = new Map<string, string>()\n if (!searchQuery?.trim() || allResources2.length === 0) return map\n const queryTerms = searchQuery\n .trim()\n .toLowerCase()\n .split(/\\s+/)\n .filter(Boolean)\n const allTermsMatch = (text: string): boolean =>\n queryTerms.every((term) => text.includes(term))\n\n for (const r of allResources2) {\n if (!r.parentSlug) continue\n if (map.has(r.parentSlug)) continue\n const nameMatch = allTermsMatch(r.displayName.toLowerCase())\n const aliasMatch = (r.aliases ?? []).some((a) =>\n allTermsMatch(a.toLowerCase()),\n )\n const descMatch = r.description\n ? allTermsMatch(r.description.toLowerCase())\n : false\n if (nameMatch || aliasMatch || descMatch) {\n map.set(r.parentSlug, r.displayName)\n }\n }\n return map\n }, [searchQuery, allResources2])\n\n // Define columns\n const columns = React.useMemo<ColumnDef<Resource>[]>(\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 flex-col\">\n <div className=\"flex items-center gap-2\">\n <span className=\"font-medium\">\n <HighlightedText\n text={\n row.original.abbreviation ||\n row.original.displayName ||\n 'Unnamed App'\n }\n searchQuery={searchQuery}\n />\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 {row.original.abbreviation && (\n <span className=\"text-xs text-muted-foreground\">\n <HighlightedText\n text={row.original.displayName}\n searchQuery={searchQuery}\n />\n </span>\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 <div>\n <span className=\"text-sm text-muted-foreground line-clamp-2\">\n <HighlightedText\n text={\n row.original.description\n ? markdownToPlainText(row.original.description)\n : '—'\n }\n searchQuery={searchQuery}\n />\n </span>\n {matchedSubResourceMap.get(row.original.slug) && (\n <div className=\"text-xs text-primary mt-0.5\">\n Matched sub-resource:{' '}\n {matchedSubResourceMap.get(row.original.slug)}\n </div>\n )}\n </div>\n ),\n },\n ],\n [searchQuery, matchedSubResourceMap],\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 - derive from selectedApp and explicit close\n const [hasUserClosed, setHasUserClosed] = useState(false)\n\n // Auto-open when app is selected, unless user explicitly closed\n const isPanelOpen = selectedApp !== null && !hasUserClosed\n\n // Reset close flag when selectedApp changes\n React.useEffect(() => {\n if (selectedApp) {\n setHasUserClosed(false)\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: Resource) => {\n onAppClick?.(app)\n }\n\n const handleClosePanel = () => {\n setHasUserClosed(true)\n onClosePanel?.()\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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"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reduce a short markdown string to its visible plain text.
|
|
3
|
+
*
|
|
4
|
+
* Descriptions may contain markdown — notably cross-reference links like
|
|
5
|
+
* `[Signatera Portal](/app/signatera-portal)` (#25). The detail panel renders
|
|
6
|
+
* those as interactive links via {@link MarkdownText}, but in compact / clamped
|
|
7
|
+
* surfaces (grid list preview, table row, filter combobox, sub-resources) an
|
|
8
|
+
* interactive link would break `line-clamp` and can't be reliably highlighted
|
|
9
|
+
* for search. There we show the *text* only — so `[Name](/app/slug)` shows
|
|
10
|
+
* "Name", never the raw bracket/paren syntax.
|
|
11
|
+
*
|
|
12
|
+
* This is deliberately a small, allocation-cheap regex pass (not a full
|
|
13
|
+
* markdown parser): it covers the inline constructs that actually appear in
|
|
14
|
+
* catalog descriptions — links, images, emphasis, and inline code.
|
|
15
|
+
*/
|
|
16
|
+
export declare function markdownToPlainText(input: string): string;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
function markdownToPlainText(input) {
|
|
2
|
+
if (!input) return "";
|
|
3
|
+
return input.replace(/!\[([^\]]*)\]\([^)]*\)/g, "$1").replace(/\[([^\]]*)\]\([^)]*\)/g, "$1").replace(/(\*\*|__)(.*?)\1/g, "$2").replace(/(\*|_)(.*?)\1/g, "$2").replace(/`([^`]*)`/g, "$1");
|
|
4
|
+
}
|
|
5
|
+
export {
|
|
6
|
+
markdownToPlainText
|
|
7
|
+
};
|
|
8
|
+
//# sourceMappingURL=markdownToPlainText.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"markdownToPlainText.js","sources":["../../../../../src/modules/appCatalog/utils/markdownToPlainText.ts"],"sourcesContent":["/**\n * Reduce a short markdown string to its visible plain text.\n *\n * Descriptions may contain markdown — notably cross-reference links like\n * `[Signatera Portal](/app/signatera-portal)` (#25). The detail panel renders\n * those as interactive links via {@link MarkdownText}, but in compact / clamped\n * surfaces (grid list preview, table row, filter combobox, sub-resources) an\n * interactive link would break `line-clamp` and can't be reliably highlighted\n * for search. There we show the *text* only — so `[Name](/app/slug)` shows\n * \"Name\", never the raw bracket/paren syntax.\n *\n * This is deliberately a small, allocation-cheap regex pass (not a full\n * markdown parser): it covers the inline constructs that actually appear in\n * catalog descriptions — links, images, emphasis, and inline code.\n */\nexport function markdownToPlainText(input: string): string {\n if (!input) return ''\n\n return (\n input\n // Images:  → alt (must run before links)\n .replace(/!\\[([^\\]]*)\\]\\([^)]*\\)/g, '$1')\n // Links: [text](url) → text\n .replace(/\\[([^\\]]*)\\]\\([^)]*\\)/g, '$1')\n // Bold / italic markers: **x**, __x__, *x*, _x_ → x\n .replace(/(\\*\\*|__)(.*?)\\1/g, '$2')\n .replace(/(\\*|_)(.*?)\\1/g, '$2')\n // Inline code: `x` → x\n .replace(/`([^`]*)`/g, '$1')\n )\n}\n"],"names":[],"mappings":"AAeO,SAAS,oBAAoB,OAAuB;AACzD,MAAI,CAAC,MAAO,QAAO;AAEnB,SACE,MAEG,QAAQ,2BAA2B,IAAI,EAEvC,QAAQ,0BAA0B,IAAI,EAEtC,QAAQ,qBAAqB,IAAI,EACjC,QAAQ,kBAAkB,IAAI,EAE9B,QAAQ,cAAc,IAAI;AAEjC;"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@igstack/app-catalog-frontend-core",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.4",
|
|
4
4
|
"description": "Frontend core library for App Catalog",
|
|
5
5
|
"homepage": "https://github.com/lislon/app-catalog",
|
|
6
6
|
"repository": {
|
|
@@ -134,8 +134,8 @@
|
|
|
134
134
|
"vite-plugin-static-copy": "^3.1.4",
|
|
135
135
|
"vite-plugin-svgr": "^4.2.0",
|
|
136
136
|
"vitest": "^4.1.2",
|
|
137
|
-
"@igstack/app-catalog-backend-core": "0.9.
|
|
138
|
-
"@igstack/app-catalog-shared-core": "0.9.
|
|
137
|
+
"@igstack/app-catalog-backend-core": "0.9.4",
|
|
138
|
+
"@igstack/app-catalog-shared-core": "0.9.4"
|
|
139
139
|
},
|
|
140
140
|
"peerDependencies": {
|
|
141
141
|
"react": "19.1.2",
|
|
@@ -144,7 +144,7 @@
|
|
|
144
144
|
"vite": "^6.3.5",
|
|
145
145
|
"vite-plugin-svgr": "^4.2.0"
|
|
146
146
|
},
|
|
147
|
-
"gitHead": "
|
|
147
|
+
"gitHead": "a8315ee4ee9567d17f56c2a4aaedce8dd8edb9f0",
|
|
148
148
|
"scripts": {
|
|
149
149
|
"build": "vite build",
|
|
150
150
|
"build:lenient": "vite build --mode lenient",
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { screen, waitFor } from '@testing-library/react'
|
|
3
|
+
import '@testing-library/jest-dom/vitest'
|
|
4
|
+
import { given } from './harness/given'
|
|
5
|
+
import { magazine } from './mock-backend/magazines'
|
|
6
|
+
|
|
7
|
+
// #25 regression: descriptions now carry markdown cross-reference links like
|
|
8
|
+
// `[Signatera Portal](/app/signatera-portal)`. The detail panel renders them
|
|
9
|
+
// as real links, but the COMPACT surfaces (grid list preview / table row) show
|
|
10
|
+
// `description` as plain text, so the raw markdown syntax leaked to users.
|
|
11
|
+
// In clamped previews we render the visible text only — no bracket/paren
|
|
12
|
+
// syntax, and no dead/interactive link that would break `line-clamp`.
|
|
13
|
+
describe('Cross-reference links do not leak raw markdown in the list view (#25)', () => {
|
|
14
|
+
const withCrossRefApp = magazine.full(({ backendCfg }) => {
|
|
15
|
+
backendCfg.withApp({
|
|
16
|
+
slug: 'natera-portals-ish',
|
|
17
|
+
displayName: 'Portals Hub',
|
|
18
|
+
description:
|
|
19
|
+
'See [Signatera Portal](/app/signatera-portal) and [Prospera Portal](/app/prospera-portal).',
|
|
20
|
+
})
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
it('shows link text (not raw markdown) in the list-view description cell', async () => {
|
|
24
|
+
const { ui } = await given(withCrossRefApp)
|
|
25
|
+
await waitFor(() => {
|
|
26
|
+
expect(ui.catalog.getTableData().length).toBeGreaterThan(0)
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
const row = ui.catalog.getTableData().find((r) => r.name === 'Portals Hub')
|
|
30
|
+
expect(row).toBeDefined()
|
|
31
|
+
|
|
32
|
+
// The clamped preview shows the visible text…
|
|
33
|
+
expect(row!.description).toContain('Signatera Portal')
|
|
34
|
+
expect(row!.description).toContain('Prospera Portal')
|
|
35
|
+
// …and NEVER leaks the raw markdown link syntax.
|
|
36
|
+
expect(row!.description).not.toContain('](/app/')
|
|
37
|
+
expect(row!.description).not.toContain('[Signatera Portal]')
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it('does not render an interactive link inside the clamped list preview', async () => {
|
|
41
|
+
await given(withCrossRefApp)
|
|
42
|
+
await waitFor(() => {
|
|
43
|
+
expect(screen.getByText(/See Signatera Portal and/)).toBeInTheDocument()
|
|
44
|
+
})
|
|
45
|
+
// A clamped preview must not carry a navigational link that could break the
|
|
46
|
+
// clamp/layout — the interactive cross-links live in the detail panel.
|
|
47
|
+
expect(screen.queryByRole('link', { name: 'Signatera Portal' })).toBeNull()
|
|
48
|
+
})
|
|
49
|
+
})
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { waitFor } from '@testing-library/react'
|
|
3
|
+
import '@testing-library/jest-dom/vitest'
|
|
4
|
+
|
|
5
|
+
import { given } from './harness/given'
|
|
6
|
+
import { magazine } from './mock-backend/magazines'
|
|
7
|
+
|
|
8
|
+
// #28: In the app-detail "Sources" numbered list, the item markers ("1.", "2.")
|
|
9
|
+
// are shrink-wrapped spans. In a proportional font the glyph "2" is wider than
|
|
10
|
+
// "1", so without equal-width digits the following text on row 2 starts a few
|
|
11
|
+
// pixels to the right of row 1 — the list stops looking left-aligned. The fix
|
|
12
|
+
// is `tabular-nums` on the marker span so every digit occupies the same width.
|
|
13
|
+
describe('Sources list marker alignment (#28)', () => {
|
|
14
|
+
it('renders numbered source markers with tabular-nums so rows left-align', async () => {
|
|
15
|
+
const { ui } = await given(
|
|
16
|
+
magazine.full((ctx) => {
|
|
17
|
+
ctx.backendCfg.withApp({
|
|
18
|
+
slug: 'multi-source-app',
|
|
19
|
+
displayName: 'Multi Source App',
|
|
20
|
+
description: 'An app that cites several sources',
|
|
21
|
+
sources: [
|
|
22
|
+
'https://wiki.example.com/spaces/AAA/pages/1/First-Source',
|
|
23
|
+
'https://wiki.example.com/spaces/BBB/pages/2/Second-Source',
|
|
24
|
+
],
|
|
25
|
+
})
|
|
26
|
+
}),
|
|
27
|
+
{ initialRoute: '/app/multi-source-app' },
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
await waitFor(() => {
|
|
31
|
+
expect(ui.catalog.isDetailPanelOpen()).toBe(true)
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
// Locate the numbered markers in the "Sources" list.
|
|
35
|
+
const markers = Array.from(document.querySelectorAll('li > span')).filter(
|
|
36
|
+
(el) => /^\d+\.$/.test(el.textContent.trim()),
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
// Two sources → two numbered markers.
|
|
40
|
+
expect(markers.length).toBe(2)
|
|
41
|
+
|
|
42
|
+
// Each marker must use equal-width digits so "1." and "2." occupy the same
|
|
43
|
+
// width and all rows share the same left edge.
|
|
44
|
+
for (const marker of markers) {
|
|
45
|
+
expect(marker.className).toContain('tabular-nums')
|
|
46
|
+
}
|
|
47
|
+
})
|
|
48
|
+
})
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { markdownToPlainText } from '~/modules/appCatalog/utils/markdownToPlainText'
|
|
3
|
+
|
|
4
|
+
// #25 regression: markdown-bearing text fields (descriptions) are rendered raw
|
|
5
|
+
// in compact/clamped surfaces (grid list preview, table row, filter combobox,
|
|
6
|
+
// sub-resources). Link syntax like `[Signatera Portal](/app/signatera-portal)`
|
|
7
|
+
// leaked as literal text. In clamped previews we strip markdown to its visible
|
|
8
|
+
// text so nothing leaks and `line-clamp` / search highlighting keep working.
|
|
9
|
+
describe('markdownToPlainText', () => {
|
|
10
|
+
it('reduces an internal link to just its text', () => {
|
|
11
|
+
expect(
|
|
12
|
+
markdownToPlainText('See [Signatera Portal](/app/signatera-portal) too'),
|
|
13
|
+
).toBe('See Signatera Portal too')
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
it('reduces an external link to just its text', () => {
|
|
17
|
+
expect(markdownToPlainText('Read the [docs](https://example.com)')).toBe(
|
|
18
|
+
'Read the docs',
|
|
19
|
+
)
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it('handles multiple links in one string', () => {
|
|
23
|
+
expect(
|
|
24
|
+
markdownToPlainText(
|
|
25
|
+
'[Signatera Portal](/app/signatera-portal) and [Prospera Portal](/app/prospera-portal)',
|
|
26
|
+
),
|
|
27
|
+
).toBe('Signatera Portal and Prospera Portal')
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it('strips image syntax to its alt text', () => {
|
|
31
|
+
expect(markdownToPlainText(' brand')).toBe(
|
|
32
|
+
'logo brand',
|
|
33
|
+
)
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it('strips bold and italic markers', () => {
|
|
37
|
+
expect(markdownToPlainText('a **bold** and _italic_ word')).toBe(
|
|
38
|
+
'a bold and italic word',
|
|
39
|
+
)
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('strips inline code backticks', () => {
|
|
43
|
+
expect(markdownToPlainText('run `pnpm build` now')).toBe(
|
|
44
|
+
'run pnpm build now',
|
|
45
|
+
)
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it('leaves plain text untouched', () => {
|
|
49
|
+
expect(markdownToPlainText('just plain text, no markdown')).toBe(
|
|
50
|
+
'just plain text, no markdown',
|
|
51
|
+
)
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('returns an empty string for empty input', () => {
|
|
55
|
+
expect(markdownToPlainText('')).toBe('')
|
|
56
|
+
})
|
|
57
|
+
})
|
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
} from '~/ui/select'
|
|
22
22
|
import { useAppCatalogContext } from '~/modules/appCatalog'
|
|
23
23
|
import { getGroupBySlug } from '~/modules/appCatalog/utils/resolveHelpers'
|
|
24
|
+
import { markdownToPlainText } from '~/modules/appCatalog/utils/markdownToPlainText'
|
|
24
25
|
|
|
25
26
|
interface SubResourcesSectionProps {
|
|
26
27
|
subResources: Resource[]
|
|
@@ -172,7 +173,7 @@ export function SubResourcesSection({
|
|
|
172
173
|
)}
|
|
173
174
|
{sr.description && (
|
|
174
175
|
<div className="text-xs text-muted-foreground mt-0.5">
|
|
175
|
-
{sr.description}
|
|
176
|
+
{markdownToPlainText(sr.description)}
|
|
176
177
|
</div>
|
|
177
178
|
)}
|
|
178
179
|
</TableCell>
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
} from '~/ui/command'
|
|
13
13
|
import { Popover, PopoverContent, PopoverTrigger } from '~/ui/popover'
|
|
14
14
|
import type { TagOption } from '../context/AppCatalogFiltersContext'
|
|
15
|
+
import { markdownToPlainText } from '../../utils/markdownToPlainText'
|
|
15
16
|
|
|
16
17
|
interface FilterComboboxProps {
|
|
17
18
|
/** Filter prefix (e.g., "category") */
|
|
@@ -114,7 +115,7 @@ export function FilterCombobox({
|
|
|
114
115
|
</div>
|
|
115
116
|
{option.description && (
|
|
116
117
|
<span className="text-xs text-muted-foreground">
|
|
117
|
-
{option.description}
|
|
118
|
+
{markdownToPlainText(option.description)}
|
|
118
119
|
</span>
|
|
119
120
|
)}
|
|
120
121
|
</div>
|
|
@@ -38,6 +38,7 @@ import { useAppCatalogContext } from '../../context/AppCatalogContext'
|
|
|
38
38
|
import { useAppClickHistory } from '../../hooks/useAppClickHistory'
|
|
39
39
|
import { useKeyboardNavigation } from '../hooks/useKeyboardNavigation'
|
|
40
40
|
import { highlightText } from '../../utils/searchApps'
|
|
41
|
+
import { markdownToPlainText } from '../../utils/markdownToPlainText'
|
|
41
42
|
import { formatRelativeTime } from '../../utils/formatRelativeTime'
|
|
42
43
|
import { TierVariantsSection } from '../components/TierVariantsSection'
|
|
43
44
|
import { SubResourcesSection } from '../components/SubResourcesSection'
|
|
@@ -499,7 +500,7 @@ function AppDetails({
|
|
|
499
500
|
key={isDraft ? 'draft' : `${index}-${url}`}
|
|
500
501
|
className="flex items-center gap-2 text-xs"
|
|
501
502
|
>
|
|
502
|
-
<span className="text-muted-foreground shrink-0">
|
|
503
|
+
<span className="text-muted-foreground shrink-0 tabular-nums">
|
|
503
504
|
{index + 1}.
|
|
504
505
|
</span>
|
|
505
506
|
<InlineEditableField
|
|
@@ -583,7 +584,7 @@ function AppDetails({
|
|
|
583
584
|
<ul className="space-y-2">
|
|
584
585
|
{sourceUrls.map((url, index) => (
|
|
585
586
|
<li key={index} className="flex items-center gap-2 text-xs">
|
|
586
|
-
<span className="text-muted-foreground shrink-0">
|
|
587
|
+
<span className="text-muted-foreground shrink-0 tabular-nums">
|
|
587
588
|
{index + 1}.
|
|
588
589
|
</span>
|
|
589
590
|
{url ? (
|
|
@@ -829,7 +830,11 @@ export function AppCatalogGrid({
|
|
|
829
830
|
<div>
|
|
830
831
|
<span className="text-sm text-muted-foreground line-clamp-2">
|
|
831
832
|
<HighlightedText
|
|
832
|
-
text={
|
|
833
|
+
text={
|
|
834
|
+
row.original.description
|
|
835
|
+
? markdownToPlainText(row.original.description)
|
|
836
|
+
: '—'
|
|
837
|
+
}
|
|
833
838
|
searchQuery={searchQuery}
|
|
834
839
|
/>
|
|
835
840
|
</span>
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
TableHeader,
|
|
15
15
|
TableRow,
|
|
16
16
|
} from '~/ui/table'
|
|
17
|
+
import { markdownToPlainText } from '../../utils/markdownToPlainText'
|
|
17
18
|
import { getAppUrl } from './appCatalogUtils'
|
|
18
19
|
|
|
19
20
|
export interface AppCatalogTableProps {
|
|
@@ -43,7 +44,9 @@ export function AppCatalogTable({ apps }: AppCatalogTableProps) {
|
|
|
43
44
|
header: 'Description',
|
|
44
45
|
cell: ({ row }) => (
|
|
45
46
|
<div className="max-w-md line-clamp-2">
|
|
46
|
-
{row.original.description
|
|
47
|
+
{row.original.description
|
|
48
|
+
? markdownToPlainText(row.original.description)
|
|
49
|
+
: null}
|
|
47
50
|
</div>
|
|
48
51
|
),
|
|
49
52
|
},
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reduce a short markdown string to its visible plain text.
|
|
3
|
+
*
|
|
4
|
+
* Descriptions may contain markdown — notably cross-reference links like
|
|
5
|
+
* `[Signatera Portal](/app/signatera-portal)` (#25). The detail panel renders
|
|
6
|
+
* those as interactive links via {@link MarkdownText}, but in compact / clamped
|
|
7
|
+
* surfaces (grid list preview, table row, filter combobox, sub-resources) an
|
|
8
|
+
* interactive link would break `line-clamp` and can't be reliably highlighted
|
|
9
|
+
* for search. There we show the *text* only — so `[Name](/app/slug)` shows
|
|
10
|
+
* "Name", never the raw bracket/paren syntax.
|
|
11
|
+
*
|
|
12
|
+
* This is deliberately a small, allocation-cheap regex pass (not a full
|
|
13
|
+
* markdown parser): it covers the inline constructs that actually appear in
|
|
14
|
+
* catalog descriptions — links, images, emphasis, and inline code.
|
|
15
|
+
*/
|
|
16
|
+
export function markdownToPlainText(input: string): string {
|
|
17
|
+
if (!input) return ''
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
input
|
|
21
|
+
// Images:  → alt (must run before links)
|
|
22
|
+
.replace(/!\[([^\]]*)\]\([^)]*\)/g, '$1')
|
|
23
|
+
// Links: [text](url) → text
|
|
24
|
+
.replace(/\[([^\]]*)\]\([^)]*\)/g, '$1')
|
|
25
|
+
// Bold / italic markers: **x**, __x__, *x*, _x_ → x
|
|
26
|
+
.replace(/(\*\*|__)(.*?)\1/g, '$2')
|
|
27
|
+
.replace(/(\*|_)(.*?)\1/g, '$2')
|
|
28
|
+
// Inline code: `x` → x
|
|
29
|
+
.replace(/`([^`]*)`/g, '$1')
|
|
30
|
+
)
|
|
31
|
+
}
|