@igstack/app-catalog-frontend-core 0.14.1 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -41,11 +41,6 @@ export declare class AppDetailTools {
41
41
  clickViewReplacement(): Promise<void>;
42
42
  /** The title shown in the currently open detail panel (empty if none). */
43
43
  getOpenTitle(): string;
44
- /**
45
- * The prominent primary "Open" button in the app detail card.
46
- * Returns the anchor element when the app has an appUrl, null otherwise.
47
- */
48
- getOpenButton(): HTMLAnchorElement | null;
49
44
  private getPanel;
50
45
  private findHeading;
51
46
  private getNextSiblingText;
@@ -95,10 +95,17 @@ function AccessRequestSection({
95
95
  accessRequest.requestPrompt || accessRequest.comments
96
96
  );
97
97
  const isUndocumented = (approvalMethod == null ? void 0 : approvalMethod.type) === "unknown" || (approvalMethod == null ? void 0 : approvalMethod.type) === "custom" && !hasWrittenSteps;
98
+ const isTwoStep = Boolean(
99
+ hasWrittenSteps && accessRequest.postApprovalInstructions
100
+ );
98
101
  return /* @__PURE__ */ jsxs("div", { className: "mt-6 rounded-lg border-[1.5px] border-primary/40 bg-primary/[0.03] p-4 space-y-4", children: [
99
102
  /* @__PURE__ */ jsx("h3", { className: "text-sm font-semibold text-foreground", children: "How to get access" }),
100
103
  isOpen && /* @__PURE__ */ jsx("p", { className: "text-sm font-medium text-primary", children: "✓ Open to everyone — no request needed. Just open it." }),
101
104
  isUndocumented && /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: "Access process not documented yet — contact the owner below to find out." }),
105
+ isTwoStep && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
106
+ /* @__PURE__ */ jsx("span", { className: "text-xs font-bold text-primary bg-primary/10 rounded-full px-2.5 py-0.5", children: "Step 1" }),
107
+ /* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: "Request access" })
108
+ ] }),
102
109
  approvalMethod && approvalMethod.type !== "custom" && /* @__PURE__ */ jsx("div", { className: "flex items-center gap-2", children: approvalMethod.type === "service" && approvalMethod.config.url ? /* @__PURE__ */ jsxs(Fragment, { children: [
103
110
  /* @__PURE__ */ jsxs(
104
111
  "a",
@@ -183,17 +190,29 @@ function AccessRequestSection({
183
190
  `${urlObj.url}-${idx}`
184
191
  )) })
185
192
  ] }),
186
- accessRequest.postApprovalInstructions && /* @__PURE__ */ jsx(Accordion, { type: "single", collapsible: true, children: /* @__PURE__ */ jsxs(
187
- AccordionItem,
188
- {
189
- value: "post-approval",
190
- className: "border rounded-lg px-4",
191
- children: [
192
- /* @__PURE__ */ jsx(AccordionTrigger, { className: "text-sm hover:no-underline py-3", children: "Post-Approval Instructions" }),
193
- /* @__PURE__ */ jsx(AccordionContent, { className: "pb-3", children: /* @__PURE__ */ jsx("div", { className: "text-sm text-muted-foreground prose prose-sm max-w-none", children: /* @__PURE__ */ jsx(ReactMarkdown, { components: { a: MarkdownLink }, children: accessRequest.postApprovalInstructions }) }) })
194
- ]
195
- }
196
- ) })
193
+ accessRequest.postApprovalInstructions && (isTwoStep ? (
194
+ // Two-step apps: show Step 2 prominently, always expanded
195
+ /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
196
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
197
+ /* @__PURE__ */ jsx("span", { className: "text-xs font-bold text-primary bg-primary/10 rounded-full px-2.5 py-0.5", children: "Step 2" }),
198
+ /* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: "After approval" })
199
+ ] }),
200
+ /* @__PURE__ */ jsx("div", { className: "text-sm text-muted-foreground prose prose-sm max-w-none [&_ol]:list-decimal [&_ol]:pl-5 [&_ul]:list-disc [&_ul]:pl-5", children: /* @__PURE__ */ jsx(ReactMarkdown, { components: { a: MarkdownLink }, children: accessRequest.postApprovalInstructions }) })
201
+ ] })
202
+ ) : (
203
+ // Single-step apps: keep collapsible accordion (secondary info)
204
+ /* @__PURE__ */ jsx(Accordion, { type: "single", collapsible: true, children: /* @__PURE__ */ jsxs(
205
+ AccordionItem,
206
+ {
207
+ value: "post-approval",
208
+ className: "border rounded-lg px-4",
209
+ children: [
210
+ /* @__PURE__ */ jsx(AccordionTrigger, { className: "text-sm hover:no-underline py-3", children: "Post-Approval Instructions" }),
211
+ /* @__PURE__ */ jsx(AccordionContent, { className: "pb-3", children: /* @__PURE__ */ jsx("div", { className: "text-sm text-muted-foreground prose prose-sm max-w-none [&_ol]:list-decimal [&_ol]:pl-5 [&_ul]:list-disc [&_ul]:pl-5", children: /* @__PURE__ */ jsx(ReactMarkdown, { components: { a: MarkdownLink }, children: accessRequest.postApprovalInstructions }) }) })
212
+ ]
213
+ }
214
+ ) })
215
+ ))
197
216
  ] });
198
217
  }
199
218
  export {
@@ -1 +1 @@
1
- {"version":3,"file":"AccessRequestSection.js","sources":["../../../../../../src/modules/appCatalog/ui/components/AccessRequestSection.tsx"],"sourcesContent":["import type {\n AppApprovalMethod,\n Resource,\n} from '@igstack/app-catalog-backend-core'\nimport { Bot, Check, Copy, ExternalLink, Settings, Users } from 'lucide-react'\nimport { useCallback, useEffect, useRef, useState } from 'react'\nimport ReactMarkdown from 'react-markdown'\nimport { Button } from '~/ui/button'\nimport {\n Accordion,\n AccordionContent,\n AccordionItem,\n AccordionTrigger,\n} from '~/ui/accordion'\nimport {\n Table,\n TableBody,\n TableCell,\n TableHead,\n TableHeader,\n TableRow,\n} from '~/ui/table'\nimport { MarkdownLink } from './MarkdownText'\nimport { PersonBadge } from './PersonBadge'\n\n// Constants\nconst COPY_FEEDBACK_DURATION = 2000\n\ninterface AccessRequestSectionProps {\n app: Resource\n approvalMethods: AppApprovalMethod[]\n}\n\n// Helper function for approval method icons\nfunction getApprovalMethodIcon(\n type: 'service' | 'personTeam' | 'custom' | 'noAccessRequired' | 'unknown',\n) {\n switch (type) {\n case 'service':\n return <Bot className=\"size-5 text-primary\" />\n case 'personTeam':\n return <Users className=\"size-5 text-primary\" />\n case 'custom':\n case 'noAccessRequired':\n case 'unknown':\n return <Settings className=\"size-5 text-primary\" />\n }\n}\n\n/**\n * Custom hook for handling copy-to-clipboard functionality with feedback\n * Includes proper cleanup to prevent memory leaks\n */\nfunction useCopyToClipboard() {\n const [copiedId, setCopiedId] = useState<string | null>(null)\n const [error, setError] = useState<string | null>(null)\n const timeoutRef = useRef<NodeJS.Timeout | null>(null)\n\n // Cleanup timeout on unmount\n useEffect(() => {\n return () => {\n if (timeoutRef.current) {\n clearTimeout(timeoutRef.current)\n }\n }\n }, [])\n\n const copyToClipboard = useCallback(async (text: string, id: string) => {\n // Clear existing timeout\n if (timeoutRef.current) {\n clearTimeout(timeoutRef.current)\n }\n\n try {\n await navigator.clipboard.writeText(text)\n setCopiedId(id)\n setError(null)\n\n // Set new timeout with cleanup\n timeoutRef.current = setTimeout(() => {\n setCopiedId(null)\n }, COPY_FEEDBACK_DURATION)\n } catch (err) {\n console.error('Failed to copy to clipboard:', err)\n setError('Failed to copy')\n\n // Clear error after duration\n timeoutRef.current = setTimeout(() => {\n setError(null)\n }, COPY_FEEDBACK_DURATION)\n }\n }, [])\n\n return { copiedId, error, copyToClipboard }\n}\n\n/**\n * Reusable copy button component with accessibility\n */\ninterface CopyButtonProps {\n onCopy: () => void\n isCopied: boolean\n ariaLabel: string\n className?: string\n}\n\nfunction CopyButton({\n onCopy,\n isCopied,\n ariaLabel,\n className,\n}: CopyButtonProps) {\n return (\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n onClick={onCopy}\n className={className}\n aria-label={ariaLabel}\n title={isCopied ? 'Copied!' : 'Copy to clipboard'}\n >\n {isCopied ? (\n <>\n <Check className=\"h-3.5 w-3.5 text-green-600\" />\n <span className=\"sr-only\" role=\"status\" aria-live=\"polite\">\n Copied to clipboard\n </span>\n </>\n ) : (\n <Copy className=\"h-3.5 w-3.5\" />\n )}\n </Button>\n )\n}\n\nexport function AccessRequestSection({\n app,\n approvalMethods,\n}: AccessRequestSectionProps) {\n const { copiedId, copyToClipboard } = useCopyToClipboard()\n const accessRequest = app.accessRequest\n const approvalMethod = approvalMethods.find(\n (m) => m.slug === accessRequest?.approvalMethodSlug,\n )\n\n const handleCopyPrompt = useCallback(() => {\n if (accessRequest?.requestPrompt) {\n copyToClipboard(accessRequest.requestPrompt, 'prompt')\n }\n }, [accessRequest?.requestPrompt, copyToClipboard])\n\n // Early return if no access request\n if (!accessRequest) return null\n\n // Access UX (#31): some methods carry no clickable target — `noAccessRequired`\n // (open), `unknown` (undocumented), and bare `custom` with no comments. The\n // old UI rendered nothing for `custom`, leaving a blank/dead section. Instead\n // surface an explicit, actionable line so the user is never stranded:\n // - open → \"No request needed — just open it.\"\n // - unknown / undocumented custom → route them to the owner (shown below).\n const isOpen = approvalMethod?.type === 'noAccessRequired'\n const hasWrittenSteps = Boolean(\n accessRequest.requestPrompt || accessRequest.comments,\n )\n const isUndocumented =\n approvalMethod?.type === 'unknown' ||\n (approvalMethod?.type === 'custom' && !hasWrittenSteps)\n\n return (\n <div className=\"mt-6 rounded-lg border-[1.5px] border-primary/40 bg-primary/[0.03] p-4 space-y-4\">\n <h3 className=\"text-sm font-semibold text-foreground\">\n How to get access\n </h3>\n\n {isOpen && (\n <p className=\"text-sm font-medium text-primary\">\n ✓ Open to everyone — no request needed. Just open it.\n </p>\n )}\n\n {isUndocumented && (\n <p className=\"text-sm text-muted-foreground\">\n Access process not documented yet — contact the owner below to find\n out.\n </p>\n )}\n\n {/* Approval Method */}\n {approvalMethod && approvalMethod.type !== 'custom' && (\n <div className=\"flex items-center gap-2\">\n {approvalMethod.type === 'service' && approvalMethod.config.url ? (\n <>\n <a\n href={approvalMethod.config.url}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"flex items-center gap-2 hover:text-primary transition-colors\"\n title={approvalMethod.config.url}\n >\n <div>{getApprovalMethodIcon(approvalMethod.type)}</div>\n <div className=\"font-medium\">{approvalMethod.displayName}</div>\n </a>\n <a\n href={approvalMethod.config.url}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"text-muted-foreground hover:text-primary\"\n title={approvalMethod.config.url}\n >\n <ExternalLink className=\"size-4\" />\n </a>\n </>\n ) : (\n <>\n <div>{getApprovalMethodIcon(approvalMethod.type)}</div>\n <div className=\"font-medium\">{approvalMethod.displayName}</div>\n </>\n )}\n </div>\n )}\n\n {/* Request Prompt - Inline */}\n {accessRequest.requestPrompt && (\n <div className=\"text-sm inline-flex items-center gap-2\">\n <span className=\"whitespace-nowrap shrink-0\">Request Prompt:</span>\n <span className=\"prose prose-sm inline [&>*]:inline [&>*]:m-0\">\n <ReactMarkdown components={{ a: MarkdownLink }}>\n {accessRequest.requestPrompt}\n </ReactMarkdown>\n </span>\n <CopyButton\n onCopy={handleCopyPrompt}\n isCopied={copiedId === 'prompt'}\n ariaLabel=\"Copy request prompt\"\n className=\"h-6 w-6 p-0 shrink-0\"\n />\n </div>\n )}\n\n {/* Comments */}\n {accessRequest.comments && (\n <div className=\"text-sm text-muted-foreground prose prose-sm max-w-none\">\n <ReactMarkdown components={{ a: MarkdownLink }}>\n {accessRequest.comments}\n </ReactMarkdown>\n </div>\n )}\n\n {/* Roles Table */}\n {accessRequest.roles && accessRequest.roles.length > 0 && (\n <div>\n <h4 className=\"mb-2 text-sm font-medium\">Available Roles</h4>\n <div className=\"rounded-lg border\">\n <Table>\n <TableHeader>\n <TableRow>\n <TableHead className=\"whitespace-nowrap\">Role</TableHead>\n <TableHead>Description</TableHead>\n </TableRow>\n </TableHeader>\n <TableBody>\n {accessRequest.roles.map((role, idx) => (\n <TableRow key={`${role.displayName}-${idx}`}>\n <TableCell className=\"font-medium whitespace-nowrap\">\n {role.displayName}\n </TableCell>\n <TableCell className=\"text-sm text-muted-foreground\">\n {role.description || '—'}\n {role.adminNotes && (\n <div className=\"mt-1 text-xs italic text-muted-foreground/80\">\n Note: {role.adminNotes}\n </div>\n )}\n </TableCell>\n </TableRow>\n ))}\n </TableBody>\n </Table>\n </div>\n </div>\n )}\n\n {/* Approvers */}\n {accessRequest.approverPersonSlugs &&\n accessRequest.approverPersonSlugs.length > 0 && (\n <div>\n <h4 className=\"mb-2 text-sm font-medium\">Approvers</h4>\n <div className=\"flex flex-wrap gap-2\">\n {accessRequest.approverPersonSlugs.map((slug) => (\n <PersonBadge key={slug} slug={slug} />\n ))}\n </div>\n </div>\n )}\n\n {/* Documentation URLs */}\n {accessRequest.urls && accessRequest.urls.length > 0 && (\n <div>\n <h4 className=\"mb-2 text-sm font-medium\">Documentation</h4>\n <div className=\"flex flex-col gap-1.5\">\n {accessRequest.urls.map((urlObj, idx) => (\n <a\n key={`${urlObj.url}-${idx}`}\n href={urlObj.url}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"text-sm text-muted-foreground hover:text-primary inline-flex items-center gap-1.5\"\n >\n {urlObj.label || urlObj.url}\n <ExternalLink className=\"size-3\" />\n </a>\n ))}\n </div>\n </div>\n )}\n\n {/* Post-Approval Instructions - Collapsible (secondary info) */}\n {accessRequest.postApprovalInstructions && (\n <Accordion type=\"single\" collapsible>\n <AccordionItem\n value=\"post-approval\"\n className=\"border rounded-lg px-4\"\n >\n <AccordionTrigger className=\"text-sm hover:no-underline py-3\">\n Post-Approval Instructions\n </AccordionTrigger>\n <AccordionContent className=\"pb-3\">\n <div className=\"text-sm text-muted-foreground prose prose-sm max-w-none\">\n <ReactMarkdown components={{ a: MarkdownLink }}>\n {accessRequest.postApprovalInstructions}\n </ReactMarkdown>\n </div>\n </AccordionContent>\n </AccordionItem>\n </Accordion>\n )}\n </div>\n )\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"AccessRequestSection.js","sources":["../../../../../../src/modules/appCatalog/ui/components/AccessRequestSection.tsx"],"sourcesContent":["import type {\n AppApprovalMethod,\n Resource,\n} from '@igstack/app-catalog-backend-core'\nimport { Bot, Check, Copy, ExternalLink, Settings, Users } from 'lucide-react'\nimport { useCallback, useEffect, useRef, useState } from 'react'\nimport ReactMarkdown from 'react-markdown'\nimport { Button } from '~/ui/button'\nimport {\n Accordion,\n AccordionContent,\n AccordionItem,\n AccordionTrigger,\n} from '~/ui/accordion'\nimport {\n Table,\n TableBody,\n TableCell,\n TableHead,\n TableHeader,\n TableRow,\n} from '~/ui/table'\nimport { MarkdownLink } from './MarkdownText'\nimport { PersonBadge } from './PersonBadge'\n\n// Constants\nconst COPY_FEEDBACK_DURATION = 2000\n\ninterface AccessRequestSectionProps {\n app: Resource\n approvalMethods: AppApprovalMethod[]\n}\n\n// Helper function for approval method icons\nfunction getApprovalMethodIcon(\n type: 'service' | 'personTeam' | 'custom' | 'noAccessRequired' | 'unknown',\n) {\n switch (type) {\n case 'service':\n return <Bot className=\"size-5 text-primary\" />\n case 'personTeam':\n return <Users className=\"size-5 text-primary\" />\n case 'custom':\n case 'noAccessRequired':\n case 'unknown':\n return <Settings className=\"size-5 text-primary\" />\n }\n}\n\n/**\n * Custom hook for handling copy-to-clipboard functionality with feedback\n * Includes proper cleanup to prevent memory leaks\n */\nfunction useCopyToClipboard() {\n const [copiedId, setCopiedId] = useState<string | null>(null)\n const [error, setError] = useState<string | null>(null)\n const timeoutRef = useRef<NodeJS.Timeout | null>(null)\n\n // Cleanup timeout on unmount\n useEffect(() => {\n return () => {\n if (timeoutRef.current) {\n clearTimeout(timeoutRef.current)\n }\n }\n }, [])\n\n const copyToClipboard = useCallback(async (text: string, id: string) => {\n // Clear existing timeout\n if (timeoutRef.current) {\n clearTimeout(timeoutRef.current)\n }\n\n try {\n await navigator.clipboard.writeText(text)\n setCopiedId(id)\n setError(null)\n\n // Set new timeout with cleanup\n timeoutRef.current = setTimeout(() => {\n setCopiedId(null)\n }, COPY_FEEDBACK_DURATION)\n } catch (err) {\n console.error('Failed to copy to clipboard:', err)\n setError('Failed to copy')\n\n // Clear error after duration\n timeoutRef.current = setTimeout(() => {\n setError(null)\n }, COPY_FEEDBACK_DURATION)\n }\n }, [])\n\n return { copiedId, error, copyToClipboard }\n}\n\n/**\n * Reusable copy button component with accessibility\n */\ninterface CopyButtonProps {\n onCopy: () => void\n isCopied: boolean\n ariaLabel: string\n className?: string\n}\n\nfunction CopyButton({\n onCopy,\n isCopied,\n ariaLabel,\n className,\n}: CopyButtonProps) {\n return (\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n onClick={onCopy}\n className={className}\n aria-label={ariaLabel}\n title={isCopied ? 'Copied!' : 'Copy to clipboard'}\n >\n {isCopied ? (\n <>\n <Check className=\"h-3.5 w-3.5 text-green-600\" />\n <span className=\"sr-only\" role=\"status\" aria-live=\"polite\">\n Copied to clipboard\n </span>\n </>\n ) : (\n <Copy className=\"h-3.5 w-3.5\" />\n )}\n </Button>\n )\n}\n\nexport function AccessRequestSection({\n app,\n approvalMethods,\n}: AccessRequestSectionProps) {\n const { copiedId, copyToClipboard } = useCopyToClipboard()\n const accessRequest = app.accessRequest\n const approvalMethod = approvalMethods.find(\n (m) => m.slug === accessRequest?.approvalMethodSlug,\n )\n\n const handleCopyPrompt = useCallback(() => {\n if (accessRequest?.requestPrompt) {\n copyToClipboard(accessRequest.requestPrompt, 'prompt')\n }\n }, [accessRequest?.requestPrompt, copyToClipboard])\n\n // Early return if no access request\n if (!accessRequest) return null\n\n // Access UX (#31): some methods carry no clickable target — `noAccessRequired`\n // (open), `unknown` (undocumented), and bare `custom` with no comments. The\n // old UI rendered nothing for `custom`, leaving a blank/dead section. Instead\n // surface an explicit, actionable line so the user is never stranded:\n // - open → \"No request needed — just open it.\"\n // - unknown / undocumented custom → route them to the owner (shown below).\n const isOpen = approvalMethod?.type === 'noAccessRequired'\n const hasWrittenSteps = Boolean(\n accessRequest.requestPrompt || accessRequest.comments,\n )\n const isUndocumented =\n approvalMethod?.type === 'unknown' ||\n (approvalMethod?.type === 'custom' && !hasWrittenSteps)\n // Two-step: has a primary request action AND follow-up post-approval steps\n const isTwoStep = Boolean(\n hasWrittenSteps && accessRequest.postApprovalInstructions,\n )\n\n return (\n <div className=\"mt-6 rounded-lg border-[1.5px] border-primary/40 bg-primary/[0.03] p-4 space-y-4\">\n <h3 className=\"text-sm font-semibold text-foreground\">\n How to get access\n </h3>\n\n {isOpen && (\n <p className=\"text-sm font-medium text-primary\">\n ✓ Open to everyone — no request needed. Just open it.\n </p>\n )}\n\n {isUndocumented && (\n <p className=\"text-sm text-muted-foreground\">\n Access process not documented yet — contact the owner below to find\n out.\n </p>\n )}\n\n {isTwoStep && (\n <div className=\"flex items-center gap-2\">\n <span className=\"text-xs font-bold text-primary bg-primary/10 rounded-full px-2.5 py-0.5\">\n Step 1\n </span>\n <span className=\"text-xs text-muted-foreground\">Request access</span>\n </div>\n )}\n\n {/* Approval Method */}\n {approvalMethod && approvalMethod.type !== 'custom' && (\n <div className=\"flex items-center gap-2\">\n {approvalMethod.type === 'service' && approvalMethod.config.url ? (\n <>\n <a\n href={approvalMethod.config.url}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"flex items-center gap-2 hover:text-primary transition-colors\"\n title={approvalMethod.config.url}\n >\n <div>{getApprovalMethodIcon(approvalMethod.type)}</div>\n <div className=\"font-medium\">{approvalMethod.displayName}</div>\n </a>\n <a\n href={approvalMethod.config.url}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"text-muted-foreground hover:text-primary\"\n title={approvalMethod.config.url}\n >\n <ExternalLink className=\"size-4\" />\n </a>\n </>\n ) : (\n <>\n <div>{getApprovalMethodIcon(approvalMethod.type)}</div>\n <div className=\"font-medium\">{approvalMethod.displayName}</div>\n </>\n )}\n </div>\n )}\n\n {/* Request Prompt - Inline */}\n {accessRequest.requestPrompt && (\n <div className=\"text-sm inline-flex items-center gap-2\">\n <span className=\"whitespace-nowrap shrink-0\">Request Prompt:</span>\n <span className=\"prose prose-sm inline [&>*]:inline [&>*]:m-0\">\n <ReactMarkdown components={{ a: MarkdownLink }}>\n {accessRequest.requestPrompt}\n </ReactMarkdown>\n </span>\n <CopyButton\n onCopy={handleCopyPrompt}\n isCopied={copiedId === 'prompt'}\n ariaLabel=\"Copy request prompt\"\n className=\"h-6 w-6 p-0 shrink-0\"\n />\n </div>\n )}\n\n {/* Comments */}\n {accessRequest.comments && (\n <div className=\"text-sm text-muted-foreground prose prose-sm max-w-none\">\n <ReactMarkdown components={{ a: MarkdownLink }}>\n {accessRequest.comments}\n </ReactMarkdown>\n </div>\n )}\n\n {/* Roles Table */}\n {accessRequest.roles && accessRequest.roles.length > 0 && (\n <div>\n <h4 className=\"mb-2 text-sm font-medium\">Available Roles</h4>\n <div className=\"rounded-lg border\">\n <Table>\n <TableHeader>\n <TableRow>\n <TableHead className=\"whitespace-nowrap\">Role</TableHead>\n <TableHead>Description</TableHead>\n </TableRow>\n </TableHeader>\n <TableBody>\n {accessRequest.roles.map((role, idx) => (\n <TableRow key={`${role.displayName}-${idx}`}>\n <TableCell className=\"font-medium whitespace-nowrap\">\n {role.displayName}\n </TableCell>\n <TableCell className=\"text-sm text-muted-foreground\">\n {role.description || '—'}\n {role.adminNotes && (\n <div className=\"mt-1 text-xs italic text-muted-foreground/80\">\n Note: {role.adminNotes}\n </div>\n )}\n </TableCell>\n </TableRow>\n ))}\n </TableBody>\n </Table>\n </div>\n </div>\n )}\n\n {/* Approvers */}\n {accessRequest.approverPersonSlugs &&\n accessRequest.approverPersonSlugs.length > 0 && (\n <div>\n <h4 className=\"mb-2 text-sm font-medium\">Approvers</h4>\n <div className=\"flex flex-wrap gap-2\">\n {accessRequest.approverPersonSlugs.map((slug) => (\n <PersonBadge key={slug} slug={slug} />\n ))}\n </div>\n </div>\n )}\n\n {/* Documentation URLs */}\n {accessRequest.urls && accessRequest.urls.length > 0 && (\n <div>\n <h4 className=\"mb-2 text-sm font-medium\">Documentation</h4>\n <div className=\"flex flex-col gap-1.5\">\n {accessRequest.urls.map((urlObj, idx) => (\n <a\n key={`${urlObj.url}-${idx}`}\n href={urlObj.url}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"text-sm text-muted-foreground hover:text-primary inline-flex items-center gap-1.5\"\n >\n {urlObj.label || urlObj.url}\n <ExternalLink className=\"size-3\" />\n </a>\n ))}\n </div>\n </div>\n )}\n\n {/* Post-Approval Instructions */}\n {accessRequest.postApprovalInstructions &&\n (isTwoStep ? (\n // Two-step apps: show Step 2 prominently, always expanded\n <div className=\"space-y-2\">\n <div className=\"flex items-center gap-2\">\n <span className=\"text-xs font-bold text-primary bg-primary/10 rounded-full px-2.5 py-0.5\">\n Step 2\n </span>\n <span className=\"text-xs text-muted-foreground\">\n After approval\n </span>\n </div>\n <div className=\"text-sm text-muted-foreground prose prose-sm max-w-none [&_ol]:list-decimal [&_ol]:pl-5 [&_ul]:list-disc [&_ul]:pl-5\">\n <ReactMarkdown components={{ a: MarkdownLink }}>\n {accessRequest.postApprovalInstructions}\n </ReactMarkdown>\n </div>\n </div>\n ) : (\n // Single-step apps: keep collapsible accordion (secondary info)\n <Accordion type=\"single\" collapsible>\n <AccordionItem\n value=\"post-approval\"\n className=\"border rounded-lg px-4\"\n >\n <AccordionTrigger className=\"text-sm hover:no-underline py-3\">\n Post-Approval Instructions\n </AccordionTrigger>\n <AccordionContent className=\"pb-3\">\n <div className=\"text-sm text-muted-foreground prose prose-sm max-w-none [&_ol]:list-decimal [&_ol]:pl-5 [&_ul]:list-disc [&_ul]:pl-5\">\n <ReactMarkdown components={{ a: MarkdownLink }}>\n {accessRequest.postApprovalInstructions}\n </ReactMarkdown>\n </div>\n </AccordionContent>\n </AccordionItem>\n </Accordion>\n ))}\n </div>\n )\n}\n"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@igstack/app-catalog-frontend-core",
3
- "version": "0.14.1",
3
+ "version": "0.16.0",
4
4
  "description": "Frontend core library for App Catalog",
5
5
  "homepage": "https://github.com/lislon/app-catalog",
6
6
  "repository": {
@@ -134,8 +134,8 @@
134
134
  "vite-plugin-static-copy": "^3.1.4",
135
135
  "vite-plugin-svgr": "^4.2.0",
136
136
  "vitest": "^4.1.2",
137
- "@igstack/app-catalog-shared-core": "0.14.1",
138
- "@igstack/app-catalog-backend-core": "0.14.1"
137
+ "@igstack/app-catalog-backend-core": "0.16.0",
138
+ "@igstack/app-catalog-shared-core": "0.16.0"
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": "30c7add877ea524033c3f4e96b4e4c56a1a933e8",
147
+ "gitHead": "8ef46d75c527a6d4a00965f7529e000c03cf0bc6",
148
148
  "scripts": {
149
149
  "build": "vite build",
150
150
  "build:lenient": "vite build --mode lenient",
@@ -148,36 +148,6 @@ describe('App Catalog Integration', () => {
148
148
  expect(ui.catalog.isDetailPanelOpen()).toBe(false)
149
149
  })
150
150
 
151
- // Detail card UX (#45): the detail panel shows a prominent primary "Open" button
152
- // with the destination URL visible alongside it.
153
- it('shows a prominent primary Open button with URL in the app detail card', async () => {
154
- const { ui } = await given(
155
- magazine.custom(({ backendCfg }) => {
156
- backendCfg.withApp({
157
- displayName: 'Website App',
158
- description: 'Has a website',
159
- appUrl: 'https://example.natera.com/app',
160
- })
161
- backendCfg.withApp({
162
- displayName: 'No URL App',
163
- description: 'No url',
164
- })
165
- }),
166
- )
167
-
168
- await ui.catalog.openApp('Website App')
169
- const btn = ui.app.getOpenButton()
170
- expect(btn).not.toBeNull()
171
- expect(btn!.getAttribute('href')).toBe('https://example.natera.com/app')
172
- expect(btn!.getAttribute('target')).toBe('_blank')
173
- // The URL is shown in the button (stripped of protocol).
174
- expect(btn!.textContent).toContain('example.natera.com/app')
175
-
176
- // An app without appUrl shows no Open button.
177
- await ui.catalog.openApp('No URL App')
178
- expect(ui.app.getOpenButton()).toBeNull()
179
- })
180
-
181
151
  // Test 5: Returning user — cached data + no onboarding + backend down
182
152
  it('returning user sees cached apps even when backend is unavailable', async () => {
183
153
  suppressConsole([/TRPC Error/, /Failed to fetch/])
@@ -328,47 +298,4 @@ describe('App Catalog Integration', () => {
328
298
  /(?<![\d])0(?![\d]).*clear filters/i,
329
299
  )
330
300
  })
331
-
332
- it('highlights matched query text in search result app names (#57)', async () => {
333
- const { ui } = await given(
334
- magazine.custom(({ backendCfg }) => {
335
- backendCfg.withApp({ displayName: 'Kubernetes Platform' })
336
- backendCfg.withApp({ displayName: 'Other Tool' })
337
- }),
338
- )
339
-
340
- await ui.catalog.search('kube')
341
-
342
- await waitFor(() => {
343
- const marks = document.querySelectorAll('mark')
344
- const markTexts = [...marks].map((m) => String(m.textContent))
345
- expect(markTexts.some((t) => t.toLowerCase().includes('kube'))).toBe(true)
346
- })
347
- })
348
-
349
- it('clear (×) button appears when search has text and clears on click (#54)', async () => {
350
- const { ui } = await given(magazine.full())
351
-
352
- // Before typing: no clear button
353
- expect(
354
- document.querySelector('[aria-label="Clear search"]'),
355
- ).not.toBeInTheDocument()
356
-
357
- // After typing: clear button appears
358
- await ui.catalog.search('jira')
359
- await waitFor(() => {
360
- expect(
361
- document.querySelector('[aria-label="Clear search"]'),
362
- ).toBeInTheDocument()
363
- })
364
-
365
- // Click × — input clears, button disappears
366
- fireEvent.click(document.querySelector('[aria-label="Clear search"]')!)
367
- await waitFor(() => {
368
- expect(ui.catalog.getSearchInput().value).toBe('')
369
- expect(
370
- document.querySelector('[aria-label="Clear search"]'),
371
- ).not.toBeInTheDocument()
372
- })
373
- })
374
301
  })
@@ -1,5 +1,5 @@
1
1
  import { describe, expect, it } from 'vitest'
2
- import { fireEvent, screen, waitFor } from '@testing-library/react'
2
+ import { fireEvent, waitFor } from '@testing-library/react'
3
3
  import '@testing-library/jest-dom/vitest'
4
4
 
5
5
  import { given } from './harness/given'
@@ -45,29 +45,6 @@ describe('App deep-link routing', () => {
45
45
  expect(router.state.location.pathname).toBe('/')
46
46
  })
47
47
 
48
- it('Esc closes card opened by mouse click (#53 bug 1)', async () => {
49
- const { ui, router } = await given(magazine.full(), { initialRoute: '/' })
50
- // Open via click (mouse, not keyboard nav)
51
- await ui.catalog.openApp('Jira')
52
- await waitFor(() => expect(ui.catalog.isDetailPanelOpen()).toBe(true))
53
- // Esc should close — no "dead click" needed
54
- fireEvent.keyDown(document, { key: 'Escape', code: 'Escape' })
55
- await waitFor(() => expect(ui.catalog.isDetailPanelOpen()).toBe(false))
56
- expect(router.state.location.pathname).toBe('/')
57
- })
58
-
59
- it('closes the detail panel when the × close button is clicked (#59)', async () => {
60
- const { ui, router } = await given(magazine.full(), { initialRoute: '/' })
61
- await ui.catalog.openApp('Jira')
62
- await waitFor(() => expect(ui.catalog.isDetailPanelOpen()).toBe(true))
63
- const closeBtn = screen.getByRole('button', { name: /^Close$/i })
64
- fireEvent.click(closeBtn)
65
- await waitFor(() => {
66
- expect(ui.catalog.isDetailPanelOpen()).toBe(false)
67
- })
68
- expect(router.state.location.pathname).toBe('/')
69
- })
70
-
71
48
  it('ignores legacy ?app= query param (no detail opens)', async () => {
72
49
  const { ui } = await given(magazine.full(), { initialRoute: '/?app=jira' })
73
50
  await waitFor(() => {
@@ -45,21 +45,6 @@ describe('App freshness line', () => {
45
45
  expect(screen.getByText(/may be out of date/i)).toBeInTheDocument()
46
46
  })
47
47
 
48
- it('shows "Added" date before Sources when createdAt is present (#55)', async () => {
49
- const { ui } = await given(
50
- magazine.full(({ backendCfg }) => {
51
- backendCfg.withApp({
52
- slug: 'dated-app',
53
- displayName: 'Dated App',
54
- createdAt: iso(30 * DAY),
55
- })
56
- }),
57
- { initialRoute: '/app/dated-app' },
58
- )
59
- await waitFor(() => expect(ui.catalog.isDetailPanelOpen()).toBe(true))
60
- expect(screen.getByText(/Added/i)).toBeInTheDocument()
61
- })
62
-
63
48
  it('shows no freshness line when the app was never scanned', async () => {
64
49
  const { ui } = await given(
65
50
  magazine.full(({ backendCfg }) => {
@@ -152,17 +152,6 @@ export class AppDetailTools {
152
152
  return panel.querySelector('.text-2xl')?.textContent.trim() ?? ''
153
153
  }
154
154
 
155
- /**
156
- * The prominent primary "Open" button in the app detail card.
157
- * Returns the anchor element when the app has an appUrl, null otherwise.
158
- */
159
- getOpenButton(): HTMLAnchorElement | null {
160
- const panel = this.getPanel()
161
- return (
162
- panel.querySelector<HTMLAnchorElement>('[aria-label^="Open "]') ?? null
163
- )
164
- }
165
-
166
155
  private getPanel(): HTMLElement {
167
156
  const closeButton = screen.queryByLabelText('Close details panel')
168
157
  if (!closeButton) {
@@ -166,6 +166,10 @@ export function AccessRequestSection({
166
166
  const isUndocumented =
167
167
  approvalMethod?.type === 'unknown' ||
168
168
  (approvalMethod?.type === 'custom' && !hasWrittenSteps)
169
+ // Two-step: has a primary request action AND follow-up post-approval steps
170
+ const isTwoStep = Boolean(
171
+ hasWrittenSteps && accessRequest.postApprovalInstructions,
172
+ )
169
173
 
170
174
  return (
171
175
  <div className="mt-6 rounded-lg border-[1.5px] border-primary/40 bg-primary/[0.03] p-4 space-y-4">
@@ -186,6 +190,15 @@ export function AccessRequestSection({
186
190
  </p>
187
191
  )}
188
192
 
193
+ {isTwoStep && (
194
+ <div className="flex items-center gap-2">
195
+ <span className="text-xs font-bold text-primary bg-primary/10 rounded-full px-2.5 py-0.5">
196
+ Step 1
197
+ </span>
198
+ <span className="text-xs text-muted-foreground">Request access</span>
199
+ </div>
200
+ )}
201
+
189
202
  {/* Approval Method */}
190
203
  {approvalMethod && approvalMethod.type !== 'custom' && (
191
204
  <div className="flex items-center gap-2">
@@ -315,26 +328,45 @@ export function AccessRequestSection({
315
328
  </div>
316
329
  )}
317
330
 
318
- {/* Post-Approval Instructions - Collapsible (secondary info) */}
319
- {accessRequest.postApprovalInstructions && (
320
- <Accordion type="single" collapsible>
321
- <AccordionItem
322
- value="post-approval"
323
- className="border rounded-lg px-4"
324
- >
325
- <AccordionTrigger className="text-sm hover:no-underline py-3">
326
- Post-Approval Instructions
327
- </AccordionTrigger>
328
- <AccordionContent className="pb-3">
329
- <div className="text-sm text-muted-foreground prose prose-sm max-w-none">
330
- <ReactMarkdown components={{ a: MarkdownLink }}>
331
- {accessRequest.postApprovalInstructions}
332
- </ReactMarkdown>
333
- </div>
334
- </AccordionContent>
335
- </AccordionItem>
336
- </Accordion>
337
- )}
331
+ {/* Post-Approval Instructions */}
332
+ {accessRequest.postApprovalInstructions &&
333
+ (isTwoStep ? (
334
+ // Two-step apps: show Step 2 prominently, always expanded
335
+ <div className="space-y-2">
336
+ <div className="flex items-center gap-2">
337
+ <span className="text-xs font-bold text-primary bg-primary/10 rounded-full px-2.5 py-0.5">
338
+ Step 2
339
+ </span>
340
+ <span className="text-xs text-muted-foreground">
341
+ After approval
342
+ </span>
343
+ </div>
344
+ <div className="text-sm text-muted-foreground prose prose-sm max-w-none [&_ol]:list-decimal [&_ol]:pl-5 [&_ul]:list-disc [&_ul]:pl-5">
345
+ <ReactMarkdown components={{ a: MarkdownLink }}>
346
+ {accessRequest.postApprovalInstructions}
347
+ </ReactMarkdown>
348
+ </div>
349
+ </div>
350
+ ) : (
351
+ // Single-step apps: keep collapsible accordion (secondary info)
352
+ <Accordion type="single" collapsible>
353
+ <AccordionItem
354
+ value="post-approval"
355
+ className="border rounded-lg px-4"
356
+ >
357
+ <AccordionTrigger className="text-sm hover:no-underline py-3">
358
+ Post-Approval Instructions
359
+ </AccordionTrigger>
360
+ <AccordionContent className="pb-3">
361
+ <div className="text-sm text-muted-foreground prose prose-sm max-w-none [&_ol]:list-decimal [&_ol]:pl-5 [&_ul]:list-disc [&_ul]:pl-5">
362
+ <ReactMarkdown components={{ a: MarkdownLink }}>
363
+ {accessRequest.postApprovalInstructions}
364
+ </ReactMarkdown>
365
+ </div>
366
+ </AccordionContent>
367
+ </AccordionItem>
368
+ </Accordion>
369
+ ))}
338
370
  </div>
339
371
  )
340
372
  }