diff --git a/README.md b/README.md index a22efee5..4fb2f6c2 100644 --- a/README.md +++ b/README.md @@ -15,5 +15,4 @@ npm run dev ## Jest Testing ```bash npm run test -``` - +``` \ No newline at end of file diff --git a/app/student/forms/components/FormSigningLayout.tsx b/app/student/forms/components/FormSigningLayout.tsx index 8c3d2005..0886efc0 100644 --- a/app/student/forms/components/FormSigningLayout.tsx +++ b/app/student/forms/components/FormSigningLayout.tsx @@ -184,17 +184,6 @@ export function FormSigningLayout({ window.removeEventListener("resize", updateCompactSigningLayout); }, []); - const fieldOwnerByName = useMemo(() => { - const ownerMap = new Map(); - const allBlocks = form.formMetadata.getBlocksForEditorService(); - allBlocks.forEach((block) => { - const fieldName = - block.field_schema?.field ?? block.phantom_field_schema?.field; - if (!fieldName || ownerMap.has(fieldName)) return; - ownerMap.set(fieldName, block.signing_party_id); - }); - return ownerMap; - }, [form.formMetadata, form.formName]); const formFilloutProcess = useFormFilloutProcessRunner(); const fromMe = useMemo( () => @@ -287,14 +276,6 @@ export function FormSigningLayout({ [], ); - const previewKeyedFields = useMemo( - () => - (form.keyedFields ?? []).map((field) => ({ - ...field, - signing_party_id: fieldOwnerByName.get(field.field), - })), - [form.keyedFields, fieldOwnerByName], - ); const requiredManualFieldGroups = useMemo(() => { const fields = noEsign ? form.formMetadata.getFieldsForClientService(undefined) @@ -346,18 +327,7 @@ export function FormSigningLayout({ } return resolved; }, [previewValuesWithDerived, refs]); - const previewValuesForViewer = useMemo(() => { - if (noEsign) return previewValuesResolved; - const filtered: Record = {}; - for (const key of Object.keys(previewValuesResolved)) { - const normalized = key.replace(/:default$/i, "").replace(/:auto$/i, ""); - const owner = - fieldOwnerByName.get(normalized) ?? fieldOwnerByName.get(key); - if (owner !== undefined && owner !== "initiator") continue; - filtered[key] = previewValuesResolved[key]; - } - return filtered; - }, [previewValuesResolved, fieldOwnerByName, noEsign]); + const wetSignatureHiddenFieldNames = useMemo( () => (noEsign ? getSignatureDerivedFieldNames(form.formMetadata) : []), [form.formMetadata, noEsign], @@ -366,21 +336,20 @@ export function FormSigningLayout({ const hiddenSet = new Set( wetSignatureHiddenFieldNames.map(normalizeFieldName), ); + const allBlocks = form.formMetadata.getBlocksForEditorService(); const raw = noEsign - ? previewKeyedFields.filter( - (field) => - field.type !== "signature" && - !hiddenSet.has(normalizeFieldName(field.field)), + ? allBlocks.filter( + (block) => + block.block_type === "form_field" && + block.field_schema?.type !== "signature" && + !hiddenSet.has(normalizeFieldName(block.field_schema?.field ?? "")), ) - : previewKeyedFields; - return raw.map((field) => ({ - ...field, - id: - field.id || - field._id || - `${field.field}:${field.page}:${field.x}:${field.y}`, + : allBlocks; + return raw.map((block) => ({ + ...block, + id: block._id, })); - }, [previewKeyedFields, noEsign, wetSignatureHiddenFieldNames]); + }, [form.formMetadata, noEsign, wetSignatureHiddenFieldNames]); const computeRequiredFieldsComplete = useCallback( (nextValues: FormValues) => @@ -895,7 +864,7 @@ export function FormSigningLayout({ key={isMobileLayout ? "mobile-preview" : "desktop-preview"} documentUrl={resolvedDocumentUrl} blocks={previewBlocksForViewer} - values={previewValuesForViewer} + values={previewValuesResolved} fieldErrors={formFiller.errors} selectionTick={selectionTick} autoScrollToSelectedField={ diff --git a/app/student/forms/myforms.ctx.tsx b/app/student/forms/myforms.ctx.tsx index 8275438d..6130489d 100644 --- a/app/student/forms/myforms.ctx.tsx +++ b/app/student/forms/myforms.ctx.tsx @@ -75,8 +75,6 @@ export const MyFormsContextProvider = ({ timestamp: f.timestamp, })) ?? []; - console.log("FORMS FORMS FORMS", forms); - // Helper function to check if form has pending instances const hasPendingInstance = (formLabel: string): boolean => { return mappedForms.some( diff --git a/app/student/forms/page.tsx b/app/student/forms/page.tsx index 40733d5a..41b1e673 100644 --- a/app/student/forms/page.tsx +++ b/app/student/forms/page.tsx @@ -41,7 +41,6 @@ export default function FormsPage() { ); useEffect(() => { - console.log("PORFIE", profile.data); if (profile.isPending) return; setHasFormsAccess((currentAccess) => { diff --git a/components/features/student/forms/FormFillerRenderer.tsx b/components/features/student/forms/FormFillerRenderer.tsx index 3b01143b..b8e74971 100644 --- a/components/features/student/forms/FormFillerRenderer.tsx +++ b/components/features/student/forms/FormFillerRenderer.tsx @@ -104,19 +104,27 @@ export function FormFillerRenderer({ const profileFullName = getFullName(profile.data); useEffect(() => { - const valuesWithPrefilledSignatures = - form.formMetadata.setSignatureValueForSigningParty( - formFiller.getFinalValues(autofillValues), - profileFullName, - "initiator", - ); - const valuesWithSavedSignatureImages = withSavedSignatureImagesForFields({ - values: valuesWithPrefilledSignatures, - signatureFields, - signatureImage: profile.data?.signatureImage, - }); - - formFiller.initializeValues(valuesWithSavedSignatureImages); + (async () => { + const valuesWithPrefilledSignatures = + form.formMetadata.setSignatureValueForSigningParty( + formFiller.getFinalValues(autofillValues), + profileFullName, + "initiator", + ); + const signatureImagePreference = autofillValues.__signature_image_enabled; + const effectiveSignatureImage = + signatureImagePreference === "false" + ? null + : profile.data?.signatureImage; + const valuesWithSavedSignatureImages = + await withSavedSignatureImagesForFields({ + values: valuesWithPrefilledSignatures, + signatureFields, + signatureImage: effectiveSignatureImage, + }); + + formFiller.initializeValues(valuesWithSavedSignatureImages); + })(); }, [ autofillValues, form.formMetadata, diff --git a/components/features/student/forms/SignatureFieldRenderer.tsx b/components/features/student/forms/SignatureFieldRenderer.tsx index bf67713f..d19e19e3 100644 --- a/components/features/student/forms/SignatureFieldRenderer.tsx +++ b/components/features/student/forms/SignatureFieldRenderer.tsx @@ -10,11 +10,13 @@ import { removeSignatureImageBackground } from "@/lib/signature-image-cleanup"; import { createSignatureImageValue, getSignatureImageFieldKey, + isBucketSignatureImagePayload, parseSignatureImageValue, serializeSignatureImageValue, type ClientField, type SignatureImageValue, } from "@betterinternship/core/forms"; +import { BUCKET_PREFIX } from "@/lib/signed-url"; import { ImageUp, PenLine, Trash2, Type, UploadCloud } from "lucide-react"; import { useEffect, useMemo, useRef, useState } from "react"; @@ -83,8 +85,10 @@ export const SignatureFieldRenderer = ({ const getSignatureImageSrc = (signature: SignatureImageValue | null) => { if (!signature) return ""; - if (signature.image.storage === "bucket") { - return signature.image.signedUrl || signature.image.publicUrl || ""; + if (isBucketSignatureImagePayload(signature.image)) { + return ( + signature.image.signedUrl || `${BUCKET_PREFIX}${signature.image.path}` + ); } return signature.image.dataUrl; }; @@ -318,7 +322,9 @@ export const SignatureFieldRenderer = ({ }; const signatureImageSrc = getSignatureImageSrc(signatureImage); - const isSavedSignatureImage = signatureImage?.image.storage === "bucket"; + const isSavedSignatureImage = signatureImage + ? isBucketSignatureImagePayload(signatureImage.image) + : false; return (
diff --git a/components/forms/FormHistoryView.tsx b/components/forms/FormHistoryView.tsx index 5ed18c50..fa69c515 100644 --- a/components/forms/FormHistoryView.tsx +++ b/components/forms/FormHistoryView.tsx @@ -37,8 +37,6 @@ export function FormHistoryView({ forms, formLabel }: FormHistoryViewProps) { [forms, formLabel], ); - console.log("here are the forms to be displayed: ", filteredForms); - return (
diff --git a/hooks/use-my-autofill.tsx b/hooks/use-my-autofill.tsx index ec2b1858..ba7135b6 100644 --- a/hooks/use-my-autofill.tsx +++ b/hooks/use-my-autofill.tsx @@ -6,6 +6,8 @@ import { ClientField, ClientPhantomField, FormValues, + parseSignatureImageValue, + SIGNATURE_IMAGE_FIELD_PREFIX, } from "@betterinternship/core/forms"; import { useCallback, useMemo } from "react"; import { getFreshHistoryCutoffMsFromStorage } from "@/app/student/forms/fresh-history"; @@ -102,6 +104,17 @@ export const useMyAutofillUpdate = () => { } } + const signatureImageKeys = Object.keys(finalValues).filter((key) => + key.startsWith(SIGNATURE_IMAGE_FIELD_PREFIX), + ); + if (signatureImageKeys.length > 0) { + const usedSignatureImage = signatureImageKeys.some((key) => + parseSignatureImageValue(finalValues[key]), + ); + internshipMoaFieldsToSave.shared.__signature_image_enabled = + usedSignatureImage ? "true" : "false"; + } + // Save for future use await update.mutateAsync({ internship_moa_fields: internshipMoaFieldsToSave, diff --git a/lib/db/use-bi-moa-backend.ts b/lib/db/use-bi-moa-backend.ts index 4be06043..b6fe0024 100644 --- a/lib/db/use-bi-moa-backend.ts +++ b/lib/db/use-bi-moa-backend.ts @@ -21,6 +21,7 @@ const db = new Kysely({ dialect: new PostgresDialect({ pool: new Pool({ connectionString: DATABASE_URL, + max: 1, }), }), }); diff --git a/lib/db/use-refs-backend.ts b/lib/db/use-refs-backend.ts index 61108e53..f5e9e88a 100644 --- a/lib/db/use-refs-backend.ts +++ b/lib/db/use-refs-backend.ts @@ -34,6 +34,7 @@ const db = new Kysely({ dialect: new PostgresDialect({ pool: new Pool({ connectionString: DATABASE_URL, + max: 1, }), }), }); diff --git a/lib/saved-signature-image.ts b/lib/saved-signature-image.ts index a7374f25..b7113c42 100644 --- a/lib/saved-signature-image.ts +++ b/lib/saved-signature-image.ts @@ -2,8 +2,9 @@ import { getSignatureImageFieldKey, type FormValues, } from "@betterinternship/core/forms"; +import { resolveSignatureImageValue } from "@/lib/signed-url"; -export const withSavedSignatureImagesForFields = ({ +export const withSavedSignatureImagesForFields = async ({ values, signatureFields, signatureImage, @@ -14,12 +15,11 @@ export const withSavedSignatureImagesForFields = ({ }) => { if (!signatureImage?.trim()) return values; + const resolvedSignatureImage = await resolveSignatureImageValue(signatureImage); const nextValues = { ...values }; for (const signatureField of signatureFields) { const imageFieldKey = getSignatureImageFieldKey(signatureField.field); - if (Object.prototype.hasOwnProperty.call(nextValues, imageFieldKey)) - continue; - nextValues[imageFieldKey] = signatureImage; + nextValues[imageFieldKey] = resolvedSignatureImage; } return nextValues; diff --git a/lib/signature-image-submit.ts b/lib/signature-image-submit.ts index 0b94e4f5..fcaaa20e 100644 --- a/lib/signature-image-submit.ts +++ b/lib/signature-image-submit.ts @@ -1,8 +1,10 @@ import { FormService } from "@/lib/api/services"; import { + isInlineSignatureImagePayload, parseSignatureImageValue, type FormValues, } from "@betterinternship/core/forms"; +import { resolveSignatureImageValue } from "@/lib/signed-url"; const SIGNATURE_IMAGE_PREFIX = "__signatureImage:"; @@ -28,18 +30,25 @@ export async function withSubmittedSignatureImages(values: FormValues): Promise< for (const [field, value] of Object.entries(values)) { const signatureImage = parseSignatureImageValue(value); - if (!signatureImage || signatureImage.image.storage !== "inline") continue; + if (!signatureImage) continue; - const result = await FormService.uploadSignatureImage({ - source: signatureImage.source, - dataUrl: signatureImage.image.dataUrl, - }); + // Inline → upload to server, get bucket reference + if (isInlineSignatureImagePayload(signatureImage.image)) { + const result = await FormService.uploadSignatureImage({ + source: signatureImage.source, + dataUrl: signatureImage.image.dataUrl, + }); - if (!result.value) { - throw new Error("Signature image upload did not return a saved image."); + if (!result.value) { + throw new Error("Signature image upload did not return a saved image."); + } + + nextValues[field] = result.value; + continue; } - nextValues[field] = result.value; + // Bucket → resolve fresh signed URL + nextValues[field] = await resolveSignatureImageValue(value); } removeDuplicateSignatureImageKeys(nextValues); diff --git a/lib/signed-url.ts b/lib/signed-url.ts index 72ab0c33..2c4743b8 100644 --- a/lib/signed-url.ts +++ b/lib/signed-url.ts @@ -1,8 +1,14 @@ "use client"; +import { + isBucketSignatureImagePayload, + parseSignatureImageValue, + serializeSignatureImageValue, + type FormValues, +} from "@betterinternship/core/forms"; import { useEffect, useState } from "react"; -const BUCKET_PREFIX = +export const BUCKET_PREFIX = "https://storage.googleapis.com/better-internship-public-bucket/"; export const isBucketUrl = (url: string): boolean => @@ -30,11 +36,14 @@ const resolveFromServer = async ( }; export const resolveSignedUrl = (url: string): Promise => { - if (!isBucketUrl(url)) return Promise.resolve(url); + if (!isBucketUrl(url)) { + return Promise.resolve(url); + } const cached = cache.get(url); - if (cached && cached.expiresAt > Date.now()) + if (cached && cached.expiresAt > Date.now()) { return Promise.resolve(cached.signedUrl); + } const existing = inflight.get(url); if (existing) return existing; @@ -126,3 +135,31 @@ export const useSignedUrls = (urls: string[]) => { return { urls: resolved, loading }; }; + +export const resolveSignatureImageValue = async ( + value: string, +): Promise => { + const parsed = parseSignatureImageValue(value); + if (!parsed || !isBucketSignatureImagePayload(parsed.image)) { + return value; + } + if (parsed.image.signedUrl) { + return value; + } + const bucketUrl = `${BUCKET_PREFIX}${parsed.image.path}`; + parsed.image.signedUrl = await resolveSignedUrl(bucketUrl); + return serializeSignatureImageValue(parsed); +}; + +export const resolveSignatureImageValues = async ( + values: FormValues, +): Promise => { + const next = { ...values }; + await Promise.all( + Object.entries(values).map(async ([key, value]) => { + if (!key.startsWith("__signatureImage:")) return; + next[key] = await resolveSignatureImageValue(value); + }), + ); + return next; +}; diff --git a/next.config.mjs b/next.config.mjs index cafb7acf..bf0ec988 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -2,7 +2,7 @@ const apiUrls = [ process.env.NEXT_PUBLIC_API_URL, process.env.NEXT_PUBLIC_MOA_API_URL, process.env.NEXT_PUBLIC_API_SERVER_URL, - process.env.NEXT_PUBLIC_SUPABASE_URL, + process.env.NEXT_PUBLIC_ORCA_URL, ].filter(Boolean); const connectOrigins = apiUrls diff --git a/package.json b/package.json index 5482d587..25243658 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ }, "dependencies": { "@betterinternship/components": "1.5.28", - "@betterinternship/core": "^2.14.1", + "@betterinternship/core": "^2.16.6", "pdfjs-dist": "^4.4.168", "@betterinternship/schema": "^0.0.0", "@dnd-kit/core": "^6.3.1", @@ -86,7 +86,7 @@ "number-to-words": "^1.2.4", "pg": "^8.20.0", "pocketbase": "^0.26.1", - "posthog-js": "^1.255.1", + "posthog-js": "^1.386.6", "qrcode.react": "^4.2.0", "react": "^19", "react-countdown": "^2.3.6", @@ -137,5 +137,11 @@ "ts-jest": "^29.4.5", "typescript": "^5", "typescript-eslint": "^8.20.0" + }, + "overrides": { + "js-yaml": "4.1.1", + "nanoid": ">=5.0.9", + "nodemailer": ">=9.0.0", + "tar": ">=7.5.16" } }