From ab696832df7fbb5669dbb30632a9e68f778279a8 Mon Sep 17 00:00:00 2001 From: azdak Date: Thu, 11 Dec 2025 13:28:42 -0600 Subject: [PATCH 1/8] Fontend: Build Audit: fixed error in save & run audit --- apps/frontend/src/routes/BuildAudit.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/frontend/src/routes/BuildAudit.tsx b/apps/frontend/src/routes/BuildAudit.tsx index 26c660dd..cd4fc941 100644 --- a/apps/frontend/src/routes/BuildAudit.tsx +++ b/apps/frontend/src/routes/BuildAudit.tsx @@ -75,7 +75,9 @@ export const BuildAudit = () => { window.alert(`You need to add at least 1 URL to your audit.`); return; } - const formData = new FormData(e.currentTarget as HTMLFormElement); + const form = (e.currentTarget as HTMLElement).closest("form"); + if (!form) return; + const formData = new FormData(form); const auditData = buildAuditData(formData); console.log("Audit Data (Save & Run):", JSON.stringify(auditData)); const response = (await ( From 860da542adc404cccfa8267981fc9d914b31e70f Mon Sep 17 00:00:00 2001 From: azdak Date: Thu, 11 Dec 2025 13:33:07 -0600 Subject: [PATCH 2/8] Frontend: Audit: Fixed Add URL component not showing when last URL deleted --- apps/frontend/src/routes/Audit.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/apps/frontend/src/routes/Audit.tsx b/apps/frontend/src/routes/Audit.tsx index ee08fde6..aa339c2f 100644 --- a/apps/frontend/src/routes/Audit.tsx +++ b/apps/frontend/src/routes/Audit.tsx @@ -152,7 +152,7 @@ export const Audit = () => { const handleUrlInput = async (_changedPages: Page[]) => { // just here to have a void function to hand to AuditPagesInput - console.log("Url Input..."); + //console.log("Url Input..."); }; const addUrls = async (changedPages: Page[]) => { @@ -656,7 +656,6 @@ export const Audit = () => {
- {pages.length > 0 && ( { returnMutation isShared={isShared} /> - )}
From c2276934747dff800a2d6729cb3af2f6513dec86 Mon Sep 17 00:00:00 2001 From: Christopher Aitken Date: Fri, 12 Dec 2025 11:52:37 -0500 Subject: [PATCH 3/8] Fixed PDF scans by adding scanId, hooked up loading state to rescan button --- apps/backend/routes/public/scanWebhook.ts | 60 +++++++++++++++---- apps/frontend/src/components/AuditHeader.tsx | 31 ++++++---- .../src/components/StyledButton.module.scss | 22 +++++++ apps/frontend/src/components/StyledButton.tsx | 19 ++++-- services/aws-lambda-scan-pdf/package.json | 3 +- services/aws-lambda-scan-pdf/src/lambda.ts | 14 ++++- 6 files changed, 118 insertions(+), 31 deletions(-) diff --git a/apps/backend/routes/public/scanWebhook.ts b/apps/backend/routes/public/scanWebhook.ts index 294599d0..152b506f 100644 --- a/apps/backend/routes/public/scanWebhook.ts +++ b/apps/backend/routes/public/scanWebhook.ts @@ -3,7 +3,34 @@ import { db, event, hashStringToUuid, normalizeHtmlWithVdom, generateShortId } f export const scanWebhook = async () => { console.log(JSON.stringify(event)); const { auditId, scanId, urlId, url, blockers, status, error } = event.body; + + // Validate required fields + if (!auditId || !urlId) { + console.error("Missing required fields: auditId or urlId", { auditId, urlId }); + return { success: false, message: 'Missing required fields: auditId and urlId are required' }; + } + + if (!scanId) { + console.error("Missing scanId in webhook payload - looking up from audit", { auditId, urlId }); + } + await db.connect(); + + // If scanId is missing, look it up from the most recent scan for this audit + let effectiveScanId = scanId; + if (!effectiveScanId) { + const scanResult = await db.query({ + text: `SELECT id FROM scans WHERE audit_id = $1 ORDER BY created_at DESC LIMIT 1`, + values: [auditId], + }); + if (scanResult?.rows?.[0]?.id) { + effectiveScanId = scanResult.rows[0].id; + console.log(`Resolved scanId from audit: ${effectiveScanId}`); + } else { + console.error("Could not resolve scanId for audit", { auditId }); + // Continue without scanId - we can still save blockers + } + } const ignoredBlockerHashes = (await db.query({ text: `SELECT b.content_hash_id FROM ignored_blockers as ib LEFT OUTER JOIN blockers as b ON ib.blocker_id = b.id WHERE ib.audit_id=$1`, @@ -22,16 +49,24 @@ export const scanWebhook = async () => { }; // Atomic append using PostgreSQL's jsonb_concat (||) operator - await db.query({ - text: `UPDATE "scans" SET "errors" = COALESCE("errors", '[]'::jsonb) || $1::jsonb WHERE "id"=$2`, - values: [JSON.stringify([errorEntry]), scanId], - }); + if (effectiveScanId) { + await db.query({ + text: `UPDATE "scans" SET "errors" = COALESCE("errors", '[]'::jsonb) || $1::jsonb WHERE "id"=$2`, + values: [JSON.stringify([errorEntry]), effectiveScanId], + }); + } console.log(`Scan error logged: ${errorType} - ${errorMessage}`); }; // Helper function to update scan progress and status (atomic operation) const updateScanProgress = async () => { + // Skip if no scanId available + if (!effectiveScanId) { + console.warn("Cannot update scan progress: no scanId available"); + return { percentage: 0, isComplete: false, scannedCount: 0, totalPages: 0 }; + } + // Use a single atomic UPDATE with RETURNING to avoid race conditions // This atomically adds urlId to processed_pages if not present, then calculates progress const result = (await db.query({ @@ -49,9 +84,14 @@ export const scanWebhook = async () => { "processed_pages", jsonb_array_length(COALESCE("processed_pages", '[]'::jsonb)) as scanned_count `, - values: [JSON.stringify([urlId]), scanId], + values: [JSON.stringify([urlId]), effectiveScanId], })).rows[0]; + if (!result) { + console.error("Scan not found for id:", effectiveScanId); + return { percentage: 0, isComplete: false, scannedCount: 0, totalPages: 0 }; + } + const totalPages = result.pages?.length || 0; const scannedCount = result.scanned_count || 0; const percentage = totalPages > 0 ? Math.min(Math.round((scannedCount / totalPages) * 100), 100) : 0; @@ -60,7 +100,7 @@ export const scanWebhook = async () => { // Second atomic update for percentage and status await db.query({ text: `UPDATE "scans" SET "percentage"=$1, "status"=$2 WHERE "id"=$3`, - values: [percentage, isComplete ? 'complete' : 'processing', scanId], + values: [percentage, isComplete ? 'complete' : 'processing', effectiveScanId], }); return { percentage, isComplete, scannedCount, totalPages }; @@ -86,10 +126,10 @@ export const scanWebhook = async () => { // Only mark audit as failed if this is the only/last page, otherwise let other pages continue if (isComplete) { // Check if there were any successful pages by looking at blockers - const hasSuccessfulPages = (await db.query({ + const hasSuccessfulPages = effectiveScanId ? (await db.query({ text: `SELECT COUNT(*) FROM "blockers" WHERE "scan_id"=$1`, - values: [scanId], - })).rows[0].count > 0; + values: [effectiveScanId], + })).rows[0].count > 0 : false; await db.query({ text: `UPDATE "audits" SET "status"=$1, "response"=$2 WHERE "id"=$3`, @@ -132,7 +172,7 @@ export const scanWebhook = async () => { VALUES ($1, $2, $3, $4, $5, $6, $7, $8) RETURNING "id" `, - values: [auditId, JSON.stringify([]), blocker.node, contentNormalized, contentHashId, shortId, urlId, scanId], + values: [auditId, JSON.stringify([]), blocker.node, contentNormalized, contentHashId, shortId, urlId, effectiveScanId], })).rows[0].id; if (ignoredBlockerHashes.includes(contentHashId.replaceAll('-', ''))) { diff --git a/apps/frontend/src/components/AuditHeader.tsx b/apps/frontend/src/components/AuditHeader.tsx index 4446d044..7c663eb1 100644 --- a/apps/frontend/src/components/AuditHeader.tsx +++ b/apps/frontend/src/components/AuditHeader.tsx @@ -1,4 +1,4 @@ -import { ReactNode } from "react"; +import { ReactNode, useState } from "react"; import styles from "./AuditHeader.module.scss"; import { Link, useNavigate } from "react-router-dom"; import { StyledButton } from "./StyledButton"; @@ -24,6 +24,7 @@ export const AuditHeader = ({ }: AuditHeaderProps) => { const navigate = useNavigate(); const { setAnnounceMessage } = useGlobalStore(); + const [isScanning, setIsScanning] = useState(false); const copyCurrentLocationToClipboard = async () => { try { @@ -62,17 +63,22 @@ export const AuditHeader = ({ }; const rescanAudit = async () => { if (confirm(`Are you sure you want to re-scan this audit?`)) { - const response = await ( - await API.post({ - apiName: "auth", - path: "/rescanAudit", - options: { body: { id: auditId! } }, - }).response - ).body.json(); - //console.log(response); - await queryClient.refetchQueries({ queryKey: ["audits"] }); - // aria & logging - setAnnounceMessage(`Scanning audit ${audit.name}...`); + setIsScanning(true); + try { + const response = await ( + await API.post({ + apiName: "auth", + path: "/rescanAudit", + options: { body: { id: auditId! } }, + }).response + ).body.json(); + //console.log(response); + await queryClient.refetchQueries({ queryKey: ["audits"] }); + // aria & logging + setAnnounceMessage(`Scanning audit ${audit.name}...`); + } finally { + setIsScanning(false); + } return; } }; @@ -130,6 +136,7 @@ export const AuditHeader = ({ label="Scan Now" icon={} variant="dark" + loading={isScanning} /> )} { const iconOnly = !showLabel ? styles["icon-only"] + " icon-only":""; - const isDisabled = disabled ? styles["disabled"]: ""; + const isDisabled = disabled || loading ? styles["disabled"]: ""; + const isLoading = loading ? styles["loading"]: ""; return ( ); diff --git a/services/aws-lambda-scan-pdf/package.json b/services/aws-lambda-scan-pdf/package.json index 8ad5b4c1..41081682 100644 --- a/services/aws-lambda-scan-pdf/package.json +++ b/services/aws-lambda-scan-pdf/package.json @@ -13,7 +13,8 @@ "prefix": "./", "scripts": { "build": "rm -rf dist && esbuild --bundle --minify --keep-names --sourcemap --sources-content=false --main-fields=moudule,main --target=node22 --platform=node --packages=bundle --outfile=dist/lambda.js src/lambda.ts", - "dist": "tsc && npm run build" + "dist": "tsc && npm run build", + "deploy": "tsc && npm run build && cd dist && zip -r lambda.zip lambda.* > /dev/null && aws --profile equalifyuic lambda update-function-code --function-name aws-lambda-scan-pdf --zip-file \"fileb://lambda.zip\" > /dev/null && rm -rf lambda.zip" }, "devDependencies": { "@babel/core": "^7.28.3", diff --git a/services/aws-lambda-scan-pdf/src/lambda.ts b/services/aws-lambda-scan-pdf/src/lambda.ts index f1fac4e4..4f9dfb03 100644 --- a/services/aws-lambda-scan-pdf/src/lambda.ts +++ b/services/aws-lambda-scan-pdf/src/lambda.ts @@ -35,6 +35,12 @@ const recordHandler = async (record: SQSRecord): Promise => { const payloadParsed = JSON.parse(payload) as sqsPayload; const job = payloadParsed.data; + + // Validate job has required fields + if (!job.scanId) { + logger.warn("Job is missing scanId!", { auditId: job.auditId, urlId: job.urlId }); + } + if (payload) { try { metrics.addMetric("scansStarted", MetricUnit.Count, 1); @@ -66,18 +72,20 @@ const recordHandler = async (record: SQSRecord): Promise => { }); if(sendResultsResponse.ok){ + const responseData = await sendResultsResponse.json(); logger.info( "PDF-scan Results sent to API results webhook!", - JSON.stringify(sendResultsResponse.json()) + JSON.stringify(responseData) ); }else{ + const errorData = await sendResultsResponse.text(); logger.error( "Error sending results to API results webhook!", sendResultsResponse.statusText ); logger.error( - "Failed to send:", - JSON.stringify(sendResultsResponse.json()) + "Failed to send. Response body:", + errorData ) } From 66771d58cda6e18b2210bfdd74346ba3a2ffce95 Mon Sep 17 00:00:00 2001 From: Christopher Aitken Date: Fri, 12 Dec 2025 11:59:20 -0500 Subject: [PATCH 4/8] Hooked up loading skeleton for audits --- .../src/components/Skeleton.module.scss | 107 ++++++++++++++++++ apps/frontend/src/components/Skeleton.tsx | 71 ++++++++++++ apps/frontend/src/components/index.ts | 3 +- apps/frontend/src/routes/Audits.tsx | 11 +- 4 files changed, 188 insertions(+), 4 deletions(-) create mode 100644 apps/frontend/src/components/Skeleton.module.scss create mode 100644 apps/frontend/src/components/Skeleton.tsx diff --git a/apps/frontend/src/components/Skeleton.module.scss b/apps/frontend/src/components/Skeleton.module.scss new file mode 100644 index 00000000..0e203b40 --- /dev/null +++ b/apps/frontend/src/components/Skeleton.module.scss @@ -0,0 +1,107 @@ +@use "../global-styles/variables.module.scss"; + +@keyframes shimmer { + 0% { + background-position: -200% 0; + } + 100% { + background-position: 200% 0; + } +} + +.skeleton { + background: linear-gradient( + 90deg, + variables.$gray 0%, + variables.$paper 50%, + variables.$gray 100% + ); + background-size: 200% 100%; + animation: shimmer 1.5s ease-in-out infinite; + border-radius: calc(variables.$spacing / 2); +} + +.text { + height: 1em; + width: 100%; +} + +.title { + height: 1.25em; + width: 60%; +} + +.circle { + border-radius: 50%; +} + +.rounded { + border-radius: variables.$spacing; +} + +.skeletonCard { + background: variables.$white; + border: 1px solid variables.$gray; + border-radius: variables.$spacing; + padding: calc(variables.$spacing * 3) calc(variables.$spacing * 2); +} + +.skeletonCardHeader { + display: flex; + align-items: center; + gap: variables.$spacing; + margin-bottom: calc(variables.$spacing * 2); +} + +.skeletonIcon { + width: 20px; + height: 20px; + flex-shrink: 0; +} + +.skeletonTitle { + height: 1.25em; + flex: 1; + max-width: 70%; +} + +.skeletonDataRow { + display: flex; + justify-content: space-between; + align-items: center; + padding: calc(variables.$spacing / 2); + background: variables.$white; + border-bottom: 1px solid variables.$gray; + + &:first-of-type { + border-bottom: 2px solid variables.$gray; + } + + &:last-child { + border-bottom: none; + } +} + +.skeletonKey { + height: 0.875em; + width: 30%; +} + +.skeletonValue { + height: 0.875em; + width: 25%; +} + +.skeletonGrid { + display: grid; + gap: calc(variables.$spacing * 2); + grid-template-columns: repeat(3, 1fr); + + @media (max-width: 1024px) { + grid-template-columns: repeat(2, 1fr); + } + + @media (max-width: 640px) { + grid-template-columns: 1fr; + } +} diff --git a/apps/frontend/src/components/Skeleton.tsx b/apps/frontend/src/components/Skeleton.tsx new file mode 100644 index 00000000..f84f8e7d --- /dev/null +++ b/apps/frontend/src/components/Skeleton.tsx @@ -0,0 +1,71 @@ +import styles from "./Skeleton.module.scss"; + +interface SkeletonProps { + width?: string | number; + height?: string | number; + variant?: "text" | "title" | "circle" | "rounded" | "rect"; + className?: string; + style?: React.CSSProperties; +} + +export const Skeleton = ({ + width, + height, + variant = "rect", + className = "", + style = {}, +}: SkeletonProps) => { + const variantClass = variant === "rect" ? "" : styles[variant]; + + return ( +
- {audits?.map((row: any, index: number) => ( + {isLoading ? ( + + ) : ( + audits?.map((row: any, index: number) => ( { {/* {row.scans[0].percentage}% */}
- ))} + )) + )} ); From 27cdae51909120770f97f60a8a31d856b8e7179d Mon Sep 17 00:00:00 2001 From: azdak Date: Fri, 12 Dec 2025 13:25:13 -0600 Subject: [PATCH 5/8] Frontend: Accessibility fixes (see tickets #561/#557) --- apps/frontend/src/App.tsx | 2 +- .../AuditEmailSubscriptionInput.tsx | 3 +- .../src/components/AuditPagesInputTable.tsx | 257 ++++++++++-------- apps/frontend/src/components/StyledButton.tsx | 2 +- apps/frontend/src/routes/BuildAudit.tsx | 110 ++++---- 5 files changed, 190 insertions(+), 184 deletions(-) diff --git a/apps/frontend/src/App.tsx b/apps/frontend/src/App.tsx index 2872fc39..bd2a8d79 100644 --- a/apps/frontend/src/App.tsx +++ b/apps/frontend/src/App.tsx @@ -177,7 +177,7 @@ export const App = () => { apiKey={import.meta.env.VITE_POSTHOG_KEY} options={{ api_host: "https://us.posthog.com" }} > -
+
{announceMessage}
diff --git a/apps/frontend/src/components/AuditEmailSubscriptionInput.tsx b/apps/frontend/src/components/AuditEmailSubscriptionInput.tsx index 3632baa0..19102eb3 100644 --- a/apps/frontend/src/components/AuditEmailSubscriptionInput.tsx +++ b/apps/frontend/src/components/AuditEmailSubscriptionInput.tsx @@ -41,7 +41,8 @@ export const AuditEmailSubscriptionInput: React.FC = ({ [] ); - const handleAddEmail = () => { + const handleAddEmail = (e:any) => { + e.preventDefault(); setEmails((prevEmails) => [ ...prevEmails, { diff --git a/apps/frontend/src/components/AuditPagesInputTable.tsx b/apps/frontend/src/components/AuditPagesInputTable.tsx index f449f070..6d4cc775 100644 --- a/apps/frontend/src/components/AuditPagesInputTable.tsx +++ b/apps/frontend/src/components/AuditPagesInputTable.tsx @@ -10,12 +10,13 @@ import { useEffect, useMemo, useState } from "react"; import { StyledLabeledInput } from "./StyledLabeledInput"; import { StyledButton } from "./StyledButton"; import styles from "./AuditPagesInputTable.module.scss"; +import * as VisuallyHidden from "@radix-ui/react-visually-hidden"; interface ChildProps { pages: Page[]; isShared: boolean; - removePages: (pagesToRemove: Page[]) => void - updatePageType: (url: string, type: "html" | "pdf") => void + removePages: (pagesToRemove: Page[]) => void; + updatePageType: (url: string, type: "html" | "pdf") => void; } interface Page { @@ -24,11 +25,11 @@ interface Page { id?: string; } -export const AuditPagesInputTable = ({ - pages, +export const AuditPagesInputTable = ({ + pages, removePages, - isShared, - updatePageType + isShared, + updatePageType, }: ChildProps) => { const [pagination, setPagination] = useState({ pageIndex: 0, @@ -45,20 +46,34 @@ export const AuditPagesInputTable = ({ { id: "select-col", header: ({ table }) => ( - + <> + + + + + ), cell: ({ row }) => ( - + <> + + + + + ), }, { @@ -109,115 +124,119 @@ export const AuditPagesInputTable = ({ return ( <> {/* {pages.length > 0 ? ( */} -
- - - {table.getHeaderGroups().map((headerGroup) => ( - - {headerGroup.headers.map((header) => ( - +
+
- {header.isPlaceholder - ? null - : flexRender( - header.column.columnDef.header, - header.getContext() - )} -
+ + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => ( + + ))} + + ))} + + + {table.getRowModel().rows.length === 0 ? ( + + + + ) : ( + table.getRowModel().rows.map((row, index) => ( + + {row.getVisibleCells().map((cell) => ( + ))} - ))} - - - {table.getRowModel().rows.length === 0 ? ( - - - - ) : ( - table.getRowModel().rows.map((row, index) => ( - - {row.getVisibleCells().map((cell) => ( - - ))} - - )) - )} - -
+ {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext() + )} +
No URLs found
+ {flexRender( + cell.column.columnDef.cell, + cell.getContext() + )} +
No URLs found
- {flexRender( - cell.column.columnDef.cell, - cell.getContext() - )} -
+ )) + )} + + - {/* Pagination Controls */} -
-
- {/* Showing {table.getState().pagination.pageSize} of{" "} + {/* Pagination Controls */} +
+
+ {/* Showing {table.getState().pagination.pageSize} of{" "} {pages.length} URLs */} - {!isShared && Object.values(rowSelection).length > 0 ? ( - { - e.preventDefault(); - removePages(table.getSelectedRowModel().flatRows.map(row => row.original)); - table.toggleAllPageRowsSelected(false); - }} - /> - ) : null} -
-
- {pages && - ` Page ${table.getState().pagination.pageIndex + 1} of ${table.getPageCount()}`} - - - - - + {!isShared && Object.values(rowSelection).length > 0 ? ( + { + e.preventDefault(); + removePages( + table + .getSelectedRowModel() + .flatRows.map((row) => row.original) + ); + table.toggleAllPageRowsSelected(false); + }} + /> + ) : null} +
+
+ {pages && + ` Page ${table.getState().pagination.pageIndex + 1} of ${table.getPageCount()}`} - - - + + +
+
{/* ) : (
Loading URLs...
)} */} diff --git a/apps/frontend/src/components/StyledButton.tsx b/apps/frontend/src/components/StyledButton.tsx index 4a3a59ed..61070523 100644 --- a/apps/frontend/src/components/StyledButton.tsx +++ b/apps/frontend/src/components/StyledButton.tsx @@ -46,7 +46,7 @@ export const StyledButton = ({ {loading ? ( ) : ( - icon && {icon} + icon //&& {icon} )} {showLabel ? ( {loading ? "Loading..." : label} diff --git a/apps/frontend/src/routes/BuildAudit.tsx b/apps/frontend/src/routes/BuildAudit.tsx index cd4fc941..f9d73807 100644 --- a/apps/frontend/src/routes/BuildAudit.tsx +++ b/apps/frontend/src/routes/BuildAudit.tsx @@ -35,12 +35,12 @@ export const BuildAudit = () => { const defaultEmailList = { emails: [ - { + /* { id: uuidv4(), email: user?.email ?? "user@uic.edu", frequency: "Weekly", lastSent: "", // we'll populate this on send in buildAuditData - }, + }, */ ], }; const [emailList, setEmailList] = @@ -133,67 +133,53 @@ export const BuildAudit = () => {
{/* ← Go Back */} -

Audit Builder

+

Audit Builder

- -

- - General Info -

- - - - + +

+ + General Info +

+ + + + - - - + + + + + {/* */} - - -
- -

- - Email Notifications: -

- - - setEmailNotifications(!checked)} - aria-label={"Enable email notifications?"} - className={styles["switch"]} - > - - - - - {emailNotifications && ( -
- -
- )} -
+ + +
+ +

+ + Email Notifications: +

+ +
@@ -201,11 +187,11 @@ export const BuildAudit = () => { Add URLs - + />
Date: Mon, 15 Dec 2025 12:30:26 -0600 Subject: [PATCH 6/8] Frontend: 12/15 fixes (see #557) --- .../src/components/BlockersTable.module.scss | 13 +++ .../frontend/src/components/BlockersTable.tsx | 103 +++++++++++------- .../src/components/StyledButton.module.scss | 4 + apps/frontend/src/components/StyledButton.tsx | 5 +- 4 files changed, 87 insertions(+), 38 deletions(-) diff --git a/apps/frontend/src/components/BlockersTable.module.scss b/apps/frontend/src/components/BlockersTable.module.scss index 482dd73d..6ed4492b 100644 --- a/apps/frontend/src/components/BlockersTable.module.scss +++ b/apps/frontend/src/components/BlockersTable.module.scss @@ -30,4 +30,17 @@ white-space: nowrap; } } + .view-code-details { + span { + @include fonts.font-size-small; + } + display: flex; + flex-wrap: wrap; + } + .tooltip-rondel { + color:variables.$white; + background: variables.$black; + border-radius: 999px; + cursor:pointer; + } } diff --git a/apps/frontend/src/components/BlockersTable.tsx b/apps/frontend/src/components/BlockersTable.tsx index 1371a4dd..af9cb7c9 100644 --- a/apps/frontend/src/components/BlockersTable.tsx +++ b/apps/frontend/src/components/BlockersTable.tsx @@ -12,7 +12,7 @@ import { useState, useMemo, ChangeEvent } from "react"; import * as ToggleGroup from "@radix-ui/react-toggle-group"; import { AccessibleIcon } from "@radix-ui/react-accessible-icon"; import Select, { MultiValue } from "react-select"; -import { FaClipboard, FaCode, FaRegFilePdf } from "react-icons/fa"; +import { FaArrowDown, FaArrowUp, FaClipboard, FaCode, FaRegFilePdf } from "react-icons/fa"; import { PiFileHtml } from "react-icons/pi"; import { AiFillFileUnknown, AiOutlineFileUnknown } from "react-icons/ai"; import { Drawer } from "vaul-base"; @@ -209,7 +209,9 @@ export const BlockersTable = ({ auditId, isShared }: BlockersTableProps) => { const getElementTagFromContent = (content: string) => { const parser = new DOMParser(); const extractedElementTag = `<${parser.parseFromString(content, "text/html").body?.firstChild?.nodeName.toLowerCase()}>`; - return extractedElementTag ?? content; + return extractedElementTag !== "" + ? extractedElementTag + : undefined; }; const copyToClipboard = async (val: string) => { @@ -268,18 +270,22 @@ export const BlockersTable = ({ auditId, isShared }: BlockersTableProps) => { { accessorKey: "url", header: () => ( - + )} */} + ), cell: ({ getValue }) => { const url = getValue() as string; @@ -307,11 +313,11 @@ export const BlockersTable = ({ auditId, isShared }: BlockersTableProps) => { {messages[0] || "No message"}
copyToClipboard(shortId)} - icon={} - label={shortId || "N/A"} - variant={"naked"} - /> + onClick={() => copyToClipboard(shortId)} + icon={} + label={shortId || "N/A"} + variant={"naked"} + /> ); }, @@ -323,9 +329,14 @@ export const BlockersTable = ({ auditId, isShared }: BlockersTableProps) => { const content = getValue() as string; return ( <> - - {getElementTagFromContent(content)} - + {getElementTagFromContent(content) && ( +
+ Issue in element: + + {getElementTagFromContent(content)} + +
+ )} { setBackgroundColorOnScale={false} > - View Code + View Code @@ -377,8 +388,8 @@ export const BlockersTable = ({ auditId, isShared }: BlockersTableProps) => { {tags.slice(TAGS_TO_SHOW_IN_TABLE).length > 0 && ( - - +{tags.slice(TAGS_TO_SHOW_IN_TABLE).length} + + {`+${tags.slice(TAGS_TO_SHOW_IN_TABLE).length}`} {
); }, - } /* + }, + { + accessorKey: "categories", + header: "Category", + cell: ({ getValue }) => { + const category = getValue() as string; + return ( +
+ {category} +
+ ); + }, + } + /* { accessorKey: "ignored", header: "Status", @@ -454,23 +478,28 @@ export const BlockersTable = ({ auditId, isShared }: BlockersTableProps) => { const blockerId = getValue() as string; const isIgnored = ignoredBlockers?.has(blockerId) || false; return ( - - { - if (!authenticated) return; - toggleIgnoreMutation.mutate({ - blockerId, - isCurrentlyIgnored: isIgnored, - }); - setAnnounceMessage( - `Blocker ID ${blockerId} set to ignored status: ${isIgnored ? "Ignored" : "Active"}`, "success" - ); - }} - label={isIgnored ? "Ignored" : "Active"} - icon={isIgnored ? : } - variant={isIgnored ? "toggle-ignored" : "toggle"} - /> - + { + if (!authenticated) return; + toggleIgnoreMutation.mutate({ + blockerId, + isCurrentlyIgnored: isIgnored, + }); + setAnnounceMessage( + `Blocker ID ${blockerId} set to ignored status: ${isIgnored ? "Ignored" : "Active"}`, + "success" + ); + }} + label={isIgnored ? "Ignored" : "Active"} + icon={ + isIgnored ? ( + + ) : ( + + ) + } + variant={isIgnored ? "toggle-ignored" : "toggle"} + /> ); }, }, diff --git a/apps/frontend/src/components/StyledButton.module.scss b/apps/frontend/src/components/StyledButton.module.scss index e9a78381..9a9019cb 100644 --- a/apps/frontend/src/components/StyledButton.module.scss +++ b/apps/frontend/src/components/StyledButton.module.scss @@ -76,6 +76,10 @@ opacity: .7; } } + &.rondel { + padding:variables.$spacing; + border-radius: 999px; + } &.red { background: variables.$red; color: variables.$white; diff --git a/apps/frontend/src/components/StyledButton.tsx b/apps/frontend/src/components/StyledButton.tsx index 61070523..627a6bc3 100644 --- a/apps/frontend/src/components/StyledButton.tsx +++ b/apps/frontend/src/components/StyledButton.tsx @@ -12,6 +12,7 @@ interface ButtonProps extends React.PropsWithChildren { disabled?: boolean; loading?: boolean; className?: string; + prependText?: string; } export const StyledButton = ({ @@ -22,7 +23,8 @@ export const StyledButton = ({ showLabel = true, disabled = false, loading = false, - className = "" + className = "", + prependText = "" }: ButtonProps) => { const iconOnly = !showLabel ? styles["icon-only"] + " icon-only":""; const isDisabled = disabled || loading ? styles["disabled"]: ""; @@ -48,6 +50,7 @@ export const StyledButton = ({ ) : ( icon //&& {icon} )} + {prependText} {showLabel ? ( {loading ? "Loading..." : label} ) : ( From b3973d0991a289b783eb03ae6c390d9370563c8e Mon Sep 17 00:00:00 2001 From: Christopher Aitken Date: Thu, 18 Dec 2025 15:21:49 -0500 Subject: [PATCH 7/8] Added loading states to buttons & loading skeletons across the app --- apps/frontend/src/components/AuditHeader.tsx | 71 ++++--- .../frontend/src/components/BlockersTable.tsx | 3 +- apps/frontend/src/components/InvitesTable.tsx | 8 +- .../src/components/Navigation.module.scss | 2 +- .../src/components/Skeleton.module.scss | 15 ++ apps/frontend/src/components/Skeleton.tsx | 173 +++++++++++++++++- .../src/components/StyledButton.module.scss | 2 +- apps/frontend/src/components/UsersTable.tsx | 8 +- apps/frontend/src/global-styles/inputs.scss | 2 +- apps/frontend/src/routes/Account.tsx | 9 +- apps/frontend/src/routes/BuildAudit.tsx | 62 ++++--- apps/frontend/src/routes/Login.module.scss | 4 +- apps/frontend/src/routes/Logs.tsx | 3 +- apps/frontend/src/routes/Signup.module.scss | 4 +- 14 files changed, 293 insertions(+), 73 deletions(-) diff --git a/apps/frontend/src/components/AuditHeader.tsx b/apps/frontend/src/components/AuditHeader.tsx index 7c663eb1..6280655f 100644 --- a/apps/frontend/src/components/AuditHeader.tsx +++ b/apps/frontend/src/components/AuditHeader.tsx @@ -8,6 +8,7 @@ import { createLog } from "#src/utils/createLog.ts"; import { useGlobalStore } from "../utils"; import * as API from "aws-amplify/api"; import { QueryClient, useQueryClient } from "@tanstack/react-query"; +import { SkeletonAuditHeader } from "./Skeleton"; interface AuditHeaderProps extends React.PropsWithChildren { isShared: boolean; @@ -25,6 +26,8 @@ export const AuditHeader = ({ const navigate = useNavigate(); const { setAnnounceMessage } = useGlobalStore(); const [isScanning, setIsScanning] = useState(false); + const [isDeleting, setIsDeleting] = useState(false); + const [isRenaming, setIsRenaming] = useState(false); const copyCurrentLocationToClipboard = async () => { try { @@ -44,20 +47,25 @@ export const AuditHeader = ({ }; const deleteAudit = async () => { if (confirm(`Are you sure you want to delete this audit?`)) { - const response = await ( - await API.post({ - apiName: "auth", - path: "/deleteAudit", - options: { body: { id: auditId! } }, - }).response - ).body.json(); - //console.log(response); - await queryClient.refetchQueries({ queryKey: ["audits"] }); - // aria & logging - setAnnounceMessage(`Deleted audit ${audit.name}.`, "success"); - await createLog(`Deleted audit ${audit.name}.`, auditId); + setIsDeleting(true); + try { + const response = await ( + await API.post({ + apiName: "auth", + path: "/deleteAudit", + options: { body: { id: auditId! } }, + }).response + ).body.json(); + //console.log(response); + await queryClient.refetchQueries({ queryKey: ["audits"] }); + // aria & logging + setAnnounceMessage(`Deleted audit ${audit.name}.`, "success"); + await createLog(`Deleted audit ${audit.name}.`, auditId); - navigate("/audits"); + navigate("/audits"); + } finally { + setIsDeleting(false); + } return; } }; @@ -89,20 +97,31 @@ export const AuditHeader = ({ audit?.name ); if (newName) { - const response = await ( - await API.post({ - apiName: "auth", - path: "/updateAudit", - options: { body: { id: auditId!, name: newName } }, - }).response - ).body.json(); - //console.log(response); - await queryClient.refetchQueries({ queryKey: ["audit", auditId] }); - // aria & logging - setAnnounceMessage(`Audit ${audit.name} renamed to ${newName}`, "success"); + setIsRenaming(true); + try { + const response = await ( + await API.post({ + apiName: "auth", + path: "/updateAudit", + options: { body: { id: auditId!, name: newName } }, + }).response + ).body.json(); + //console.log(response); + await queryClient.refetchQueries({ queryKey: ["audit", auditId] }); + // aria & logging + setAnnounceMessage(`Audit ${audit.name} renamed to ${newName}`, "success"); + } finally { + setIsRenaming(false); + } return; } }; + + // Show skeleton while audit is loading + if (!audit) { + return ; + } + return (
@@ -118,12 +137,16 @@ export const AuditHeader = ({ label="Rename Audit" icon={} showLabel={false} + loading={isRenaming} + disabled={isRenaming || isDeleting} /> } showLabel={false} + loading={isDeleting} + disabled={isRenaming || isDeleting} />
)} diff --git a/apps/frontend/src/components/BlockersTable.tsx b/apps/frontend/src/components/BlockersTable.tsx index af9cb7c9..04a8a0b0 100644 --- a/apps/frontend/src/components/BlockersTable.tsx +++ b/apps/frontend/src/components/BlockersTable.tsx @@ -27,6 +27,7 @@ import { a11yDark as prism } from "react-syntax-highlighter/dist/esm/styles/pris import { StyledButton } from "./StyledButton"; import { TbEye, TbEyeX } from "react-icons/tb"; import style from "./BlockersTable.module.scss"; +import { SkeletonBlockersTable } from "./Skeleton"; SyntaxHighlighter.registerLanguage("jsx", jsx); const apiClient = API.generateClient(); @@ -726,7 +727,7 @@ export const BlockersTable = ({ auditId, isShared }: BlockersTableProps) => {
{isLoading ? ( -
Loading blockers...
+ ) : ( <>
diff --git a/apps/frontend/src/components/InvitesTable.tsx b/apps/frontend/src/components/InvitesTable.tsx index f5d68b22..5e37f06e 100644 --- a/apps/frontend/src/components/InvitesTable.tsx +++ b/apps/frontend/src/components/InvitesTable.tsx @@ -8,6 +8,7 @@ import { import * as API from "aws-amplify/api"; import { useState, useMemo } from "react"; import { useGlobalStore } from "../utils"; +import { SkeletonTable } from "./Skeleton"; const apiClient = API.generateClient(); @@ -134,10 +135,11 @@ export const InvitesTable = () => { return ( ); }, @@ -192,7 +194,7 @@ export const InvitesTable = () => {
{isLoading ? ( -
Loading invites...
+ ) : ( <>
diff --git a/apps/frontend/src/components/Navigation.module.scss b/apps/frontend/src/components/Navigation.module.scss index 09a25748..3708bca0 100644 --- a/apps/frontend/src/components/Navigation.module.scss +++ b/apps/frontend/src/components/Navigation.module.scss @@ -30,7 +30,7 @@ opacity:.7; transition: background 0.12s ease-out, opacity 0.12s ease-out; &:hover { - border-radius: variables.$spacing/2; + border-radius: calc(variables.$spacing / 2); background-color: variables.$paper-dark; opacity:1; } diff --git a/apps/frontend/src/components/Skeleton.module.scss b/apps/frontend/src/components/Skeleton.module.scss index 0e203b40..b7b77e72 100644 --- a/apps/frontend/src/components/Skeleton.module.scss +++ b/apps/frontend/src/components/Skeleton.module.scss @@ -105,3 +105,18 @@ grid-template-columns: 1fr; } } + +// Text group for multi-line skeletons +.skeletonTextGroup { + display: flex; + flex-direction: column; + gap: calc(variables.$spacing / 2); +} + +// Table row skeleton +.skeletonTableRow { + td { + padding: variables.$spacing calc(variables.$spacing * 2); + border-bottom: 1px solid variables.$gray; + } +} diff --git a/apps/frontend/src/components/Skeleton.tsx b/apps/frontend/src/components/Skeleton.tsx index f84f8e7d..d8fa7861 100644 --- a/apps/frontend/src/components/Skeleton.tsx +++ b/apps/frontend/src/components/Skeleton.tsx @@ -8,6 +8,9 @@ interface SkeletonProps { style?: React.CSSProperties; } +/** + * Base skeleton component + */ export const Skeleton = ({ width, height, @@ -30,6 +33,30 @@ export const Skeleton = ({ ); }; +/** + * Inline text skeleton - great for replacing text content + */ +export const SkeletonText = ({ + width = "100%", + lines = 1 +}: { + width?: string | number; + lines?: number; +}) => ( + +); + +/** + * Skeleton for data rows (key-value pairs) + */ export const SkeletonDataRow = ({ isFirst = false }: { isFirst?: boolean }) => (
(
); +/** + * Skeleton that matches Audit Card layout + */ export const SkeletonAuditCard = () => (
@@ -56,11 +86,10 @@ export const SkeletonAuditCard = () => (
); -interface SkeletonAuditGridProps { - count?: number; -} - -export const SkeletonAuditGrid = ({ count = 6 }: SkeletonAuditGridProps) => ( +/** + * Grid of skeleton audit cards + */ +export const SkeletonAuditGrid = ({ count = 6 }: { count?: number }) => ( <> {Array.from({ length: count }).map((_, i) => ( @@ -68,4 +97,138 @@ export const SkeletonAuditGrid = ({ count = 6 }: SkeletonAuditGridProps) => ( ); +/** + * Table row skeleton + */ +export const SkeletonTableRow = ({ columns = 4 }: { columns?: number }) => ( + + {Array.from({ length: columns }).map((_, i) => ( + + + + ))} + +); + +/** + * Full table skeleton with header and rows + */ +export const SkeletonTable = ({ + columns = 4, + rows = 5, + headers = [], +}: { + columns?: number; + rows?: number; + headers?: string[]; +}) => ( +
+ + + + {(headers.length > 0 ? headers : Array.from({ length: columns })).map((header, i) => ( + + ))} + + + + {Array.from({ length: rows }).map((_, i) => ( + + ))} + + +
+); + +/** + * Account page skeleton + */ +export const SkeletonAccount = () => ( + +); + +/** + * Blockers table skeleton with realistic column widths + */ +export const SkeletonBlockersTable = ({ rows = 5 }: { rows?: number }) => ( +
+ + + + + + + + + + + + + {Array.from({ length: rows }).map((_, i) => ( + + + + + + + + + ))} + + +
+); + +/** + * Audit header skeleton + */ +export const SkeletonAuditHeader = () => ( + +); + +/** + * Chart skeleton for audit page + */ +export const SkeletonChart = () => ( + +); + export { styles as skeletonStyles }; diff --git a/apps/frontend/src/components/StyledButton.module.scss b/apps/frontend/src/components/StyledButton.module.scss index 9a9019cb..1662a8dd 100644 --- a/apps/frontend/src/components/StyledButton.module.scss +++ b/apps/frontend/src/components/StyledButton.module.scss @@ -8,7 +8,7 @@ color: variables.$white; padding: variables.$spacing*2; background: variables.$green; - border-radius: variables.$spacing/2; + border-radius: calc(variables.$spacing / 2); border: 1px solid variables.$green-dark; box-shadow: variables.$shadow-small; cursor: pointer; diff --git a/apps/frontend/src/components/UsersTable.tsx b/apps/frontend/src/components/UsersTable.tsx index 16d46468..6a31d51e 100644 --- a/apps/frontend/src/components/UsersTable.tsx +++ b/apps/frontend/src/components/UsersTable.tsx @@ -8,6 +8,7 @@ import { import * as API from "aws-amplify/api"; import { useState, useMemo } from "react"; import { useGlobalStore } from "../utils"; +import { SkeletonTable } from "./Skeleton"; const apiClient = API.generateClient(); @@ -175,10 +176,11 @@ export const UsersTable = () => { deleteUserMutation.mutate(userId); } }} - className="px-3 py-1 bg-red-500 text-white rounded hover:bg-red-600 text-sm" + className="px-3 py-1 bg-red-500 text-white rounded hover:bg-red-600 text-sm disabled:opacity-50" aria-label={`Remove user ${row.original.email}`} + disabled={deleteUserMutation.isPending} > - Remove + {deleteUserMutation.isPending ? "Removing..." : "Remove"} ); }, @@ -208,7 +210,7 @@ export const UsersTable = () => {
{isLoading ? ( -
Loading users...
+ ) : ( <>
diff --git a/apps/frontend/src/global-styles/inputs.scss b/apps/frontend/src/global-styles/inputs.scss index 5c0cc8b0..08308ed5 100644 --- a/apps/frontend/src/global-styles/inputs.scss +++ b/apps/frontend/src/global-styles/inputs.scss @@ -6,7 +6,7 @@ input, select, textarea { color: variables.$black; background-color: variables.$white; border: 1px solid variables.$gray; - border-radius: variables.$spacing/2; + border-radius: calc(variables.$spacing / 2); box-shadow: variables.$shadow-inset; transition: 80ms cubic-bezier(0.33, 1, 0.68, 1); transition-property: box-shadow, border-color; diff --git a/apps/frontend/src/routes/Account.tsx b/apps/frontend/src/routes/Account.tsx index 65e36cfc..4cf78303 100644 --- a/apps/frontend/src/routes/Account.tsx +++ b/apps/frontend/src/routes/Account.tsx @@ -1,19 +1,22 @@ import { Link } from "react-router-dom" import { useUser } from "../queries" import { InvitesTable, UsersTable } from "../components" +import { SkeletonAccount } from "#src/components/Skeleton.tsx" export const Account = () => { - const { data: user } = useUser(); + const { data: user, isLoading } = useUser(); const isAdmin = user?.type === 'admin'; return

Account

- {user && ( + {isLoading ? ( + + ) : user ? (

Name: {user.name}

Email: {user.email}

- )} + ) : null} Logout {isAdmin && ( diff --git a/apps/frontend/src/routes/BuildAudit.tsx b/apps/frontend/src/routes/BuildAudit.tsx index f9d73807..687058c1 100644 --- a/apps/frontend/src/routes/BuildAudit.tsx +++ b/apps/frontend/src/routes/BuildAudit.tsx @@ -32,6 +32,8 @@ export const BuildAudit = () => { const [emailNotifications, setEmailNotifications] = useState(false); const [pages, setPages] = useState([]); const [auditNameValid, setAuditNameValid] = useState(false); + const [isSaving, setIsSaving] = useState(false); + const [isSavingAndRunning, setIsSavingAndRunning] = useState(false); const defaultEmailList = { emails: [ @@ -80,18 +82,21 @@ export const BuildAudit = () => { const formData = new FormData(form); const auditData = buildAuditData(formData); console.log("Audit Data (Save & Run):", JSON.stringify(auditData)); - const response = (await ( - await API.post({ - apiName: "auth", - path: "/saveAudit", - options: { body: { ...auditData, saveAndRun: true } }, - }).response - ).body.json()) as { id: string }; - setAnnounceMessage(`Audit saved and audit run started!`, "success"); - await createLog(`Audit created and audit run started!`, response.id); - - navigate(`/audits/${response?.id}`); - return; + setIsSavingAndRunning(true); + try { + const response = (await ( + await API.post({ + apiName: "auth", + path: "/saveAudit", + options: { body: { ...auditData, saveAndRun: true } }, + }).response + ).body.json()) as { id: string }; + setAnnounceMessage(`Audit saved and audit run started!`, "success"); + await createLog(`Audit created and audit run started!`, response.id); + navigate(`/audits/${response?.id}`); + } finally { + setIsSavingAndRunning(false); + } }; const saveAudit = async (e: FormEvent) => { @@ -106,18 +111,21 @@ export const BuildAudit = () => { const auditData = buildAuditData(formData); console.log("Audit Data (Save):", JSON.stringify(auditData)); - - const response = (await ( - await API.post({ - apiName: "auth", - path: "/saveAudit", - options: { body: { ...auditData, saveAndRun: false } }, - }).response - ).body.json()) as { id: string }; - setAnnounceMessage(`Audit saved!`, "success"); - await createLog(`Audit created!`, response.id); - navigate(`/audits/${response?.id}`); - return; + setIsSaving(true); + try { + const response = (await ( + await API.post({ + apiName: "auth", + path: "/saveAudit", + options: { body: { ...auditData, saveAndRun: false } }, + }).response + ).body.json()) as { id: string }; + setAnnounceMessage(`Audit saved!`, "success"); + await createLog(`Audit created!`, response.id); + navigate(`/audits/${response?.id}`); + } finally { + setIsSaving(false); + } }; const validateAuditName = (e: React.ChangeEvent) => { @@ -198,14 +206,16 @@ export const BuildAudit = () => { icon={} onClick={saveAudit} label="Save Audit" - disabled={pages.length < 1 || !auditNameValid} + disabled={pages.length < 1 || !auditNameValid || isSaving || isSavingAndRunning} + loading={isSaving} /> } onClick={saveAndRunAudit} label="Save & Run Audit" variant="red" - disabled={pages.length < 1 || !auditNameValid} + disabled={pages.length < 1 || !auditNameValid || isSaving || isSavingAndRunning} + loading={isSavingAndRunning} />
diff --git a/apps/frontend/src/routes/Login.module.scss b/apps/frontend/src/routes/Login.module.scss index 909fbc80..6574c002 100644 --- a/apps/frontend/src/routes/Login.module.scss +++ b/apps/frontend/src/routes/Login.module.scss @@ -20,7 +20,7 @@ label{ display: block; margin-top: variables.$spacing*2; - margin-bottom: variables.$spacing/4; + margin-bottom: calc(variables.$spacing / 4); } button{ @@ -32,7 +32,7 @@ } .error{ color: variables.$red; - border-radius: variables.$spacing/2; + border-radius: calc(variables.$spacing / 2); background: variables.$red-light; border: 1px solid variables.$red-dark; padding: variables.$spacing*2; diff --git a/apps/frontend/src/routes/Logs.tsx b/apps/frontend/src/routes/Logs.tsx index dd065fef..e482a97e 100644 --- a/apps/frontend/src/routes/Logs.tsx +++ b/apps/frontend/src/routes/Logs.tsx @@ -9,6 +9,7 @@ import { getCoreRowModel, useReactTable, } from "@tanstack/react-table"; +import { SkeletonTable } from "#src/components/Skeleton.tsx"; export const Logs = () => { const [page, setPage] = useState(0); @@ -97,7 +98,7 @@ export const Logs = () => {
Error Loading Logs Data
)} {isLoading ? ( -
Loading Logs...
+ ) : ( <>
diff --git a/apps/frontend/src/routes/Signup.module.scss b/apps/frontend/src/routes/Signup.module.scss index 7aeeba5e..083ab106 100644 --- a/apps/frontend/src/routes/Signup.module.scss +++ b/apps/frontend/src/routes/Signup.module.scss @@ -20,7 +20,7 @@ label{ display: block; margin-top: variables.$spacing*2; - margin-bottom: variables.$spacing/4; + margin-bottom: calc(variables.$spacing / 4); } .terms{ margin-top: variables.$spacing*2; @@ -37,7 +37,7 @@ } .error{ color: variables.$red; - border-radius: variables.$spacing/2; + border-radius: calc(variables.$spacing / 2); background: variables.$red-light; border: 1px solid variables.$red-dark; padding: variables.$spacing*2; From a684ec5ea466f643812fa644b4cd96ee854335d4 Mon Sep 17 00:00:00 2001 From: Christopher Aitken Date: Thu, 18 Dec 2025 15:52:55 -0500 Subject: [PATCH 8/8] Add unformatId utility, now sending scan responses to both staging/prod webhooks --- apps/frontend/src/routes/Audit.tsx | 5 +++-- apps/frontend/src/utils/index.ts | 1 + apps/frontend/src/utils/unformatId.ts | 4 ++++ services/aws-lambda-scan-html/src/lambda.ts | 2 +- services/aws-lambda-scan-pdf/src/lambda.ts | 2 +- 5 files changed, 10 insertions(+), 4 deletions(-) create mode 100644 apps/frontend/src/utils/unformatId.ts diff --git a/apps/frontend/src/routes/Audit.tsx b/apps/frontend/src/routes/Audit.tsx index aa339c2f..be0afc01 100644 --- a/apps/frontend/src/routes/Audit.tsx +++ b/apps/frontend/src/routes/Audit.tsx @@ -1,5 +1,5 @@ import { useQuery, useQueryClient } from "@tanstack/react-query"; -import { formatDate, useGlobalStore } from "../utils"; +import { formatDate, useGlobalStore, unformatId } from "../utils"; import * as API from "aws-amplify/api"; import { Link, useLocation, useNavigate, useParams } from "react-router-dom"; const apiClient = API.generateClient(); @@ -70,7 +70,8 @@ const formatErrorType = (type: string): string => { }; export const Audit = () => { - const { auditId } = useParams(); + const { auditId: rawAuditId } = useParams(); + const auditId = rawAuditId ? unformatId(rawAuditId) : undefined; const queryClient = useQueryClient(); //const navigate = useNavigate(); const location = useLocation(); diff --git a/apps/frontend/src/utils/index.ts b/apps/frontend/src/utils/index.ts index 5debf11b..4af09068 100644 --- a/apps/frontend/src/utils/index.ts +++ b/apps/frontend/src/utils/index.ts @@ -5,4 +5,5 @@ export * from './emptyUuid' export * from './formatDate' export * from './useDebounce' export * from './formatId' +export * from './unformatId' export * from './jwtErrorHandler' \ No newline at end of file diff --git a/apps/frontend/src/utils/unformatId.ts b/apps/frontend/src/utils/unformatId.ts new file mode 100644 index 00000000..ef43b9f2 --- /dev/null +++ b/apps/frontend/src/utils/unformatId.ts @@ -0,0 +1,4 @@ +export const unformatId = (value: string) => + value.length === 32 + ? value.substr(0, 8) + '-' + value.substr(8, 4) + '-' + value.substr(12, 4) + '-' + value.substr(16, 4) + '-' + value.substr(20, 12) + : value diff --git a/services/aws-lambda-scan-html/src/lambda.ts b/services/aws-lambda-scan-html/src/lambda.ts index e41a3016..39f1a038 100644 --- a/services/aws-lambda-scan-html/src/lambda.ts +++ b/services/aws-lambda-scan-html/src/lambda.ts @@ -14,7 +14,7 @@ import scan from "./scan.ts"; import convertToEqualifyV2 from "../../../shared/convertors/AxeToEqualify2.ts" const processor = new BatchProcessor(EventType.SQS); -const RESULTS_ENDPOINT = "https://api-staging.equalifyapp.com/public/scanWebhook"; +const RESULTS_ENDPOINT = process.env.RESULTS_ENDPOINT || "https://api.equalifyapp.com/public/scanWebhook"; // Process a single SQS Record const recordHandler = async (record: SQSRecord): Promise => { diff --git a/services/aws-lambda-scan-pdf/src/lambda.ts b/services/aws-lambda-scan-pdf/src/lambda.ts index 4f9dfb03..7f5cbe04 100644 --- a/services/aws-lambda-scan-pdf/src/lambda.ts +++ b/services/aws-lambda-scan-pdf/src/lambda.ts @@ -18,7 +18,7 @@ import { SqsScanJob } from "../../../shared/types/sqsScanJob.ts"; //import convertToEqualifyV2 from "../../../shared/convertors/VeraToEqualify2.ts" const processor = new BatchProcessor(EventType.SQS); -const RESULTS_ENDPOINT = "https://api-staging.equalifyapp.com/public/scanWebhook"; +const RESULTS_ENDPOINT = process.env.RESULTS_ENDPOINT || "https://api.equalifyapp.com/public/scanWebhook"; // {"data":{"auditId":"51a5077e-f8e6-4f75-939e-9c91b00a1f2e","urlId":"ea350f8f-5e56-4361-8cd5-570fcea0025d","url":"http://decubing.com/wp-content/uploads/2025/05/zombieplan.pdf","type":"pdf"}} interface sqsPayload {