Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,4 @@ npm run dev
## Jest Testing
```bash
npm run test
```

```
57 changes: 13 additions & 44 deletions app/student/forms/components/FormSigningLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -184,17 +184,6 @@ export function FormSigningLayout({
window.removeEventListener("resize", updateCompactSigningLayout);
}, []);

const fieldOwnerByName = useMemo(() => {
const ownerMap = new Map<string, string>();
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(
() =>
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -346,18 +327,7 @@ export function FormSigningLayout({
}
return resolved;
}, [previewValuesWithDerived, refs]);
const previewValuesForViewer = useMemo(() => {
if (noEsign) return previewValuesResolved;
const filtered: Record<string, string> = {};
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],
Expand All @@ -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) =>
Expand Down Expand Up @@ -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={
Expand Down
2 changes: 0 additions & 2 deletions app/student/forms/myforms.ctx.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
1 change: 0 additions & 1 deletion app/student/forms/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@ export default function FormsPage() {
);

useEffect(() => {
console.log("PORFIE", profile.data);
if (profile.isPending) return;

setHasFormsAccess((currentAccess) => {
Expand Down
34 changes: 21 additions & 13 deletions components/features/student/forms/FormFillerRenderer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
12 changes: 9 additions & 3 deletions components/features/student/forms/SignatureFieldRenderer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -83,8 +85,10 @@ export const SignatureFieldRenderer = <T extends any[]>({

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;
};
Expand Down Expand Up @@ -318,7 +322,9 @@ export const SignatureFieldRenderer = <T extends any[]>({
};

const signatureImageSrc = getSignatureImageSrc(signatureImage);
const isSavedSignatureImage = signatureImage?.image.storage === "bucket";
const isSavedSignatureImage = signatureImage
? isBucketSignatureImagePayload(signatureImage.image)
: false;

return (
<div className="space-y-1.5">
Expand Down
2 changes: 0 additions & 2 deletions components/forms/FormHistoryView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,6 @@ export function FormHistoryView({ forms, formLabel }: FormHistoryViewProps) {
[forms, formLabel],
);

console.log("here are the forms to be displayed: ", filteredForms);

return (
<div className="w-full mx-auto">
<div className="animate-fade-in">
Expand Down
13 changes: 13 additions & 0 deletions hooks/use-my-autofill.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions lib/db/use-bi-moa-backend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ const db = new Kysely<DB>({
dialect: new PostgresDialect({
pool: new Pool({
connectionString: DATABASE_URL,
max: 1,
}),
}),
});
Expand Down
1 change: 1 addition & 0 deletions lib/db/use-refs-backend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ const db = new Kysely<DB>({
dialect: new PostgresDialect({
pool: new Pool({
connectionString: DATABASE_URL,
max: 1,
}),
}),
});
Expand Down
8 changes: 4 additions & 4 deletions lib/saved-signature-image.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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;
Expand Down
25 changes: 17 additions & 8 deletions lib/signature-image-submit.ts
Original file line number Diff line number Diff line change
@@ -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:";

Expand All @@ -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);
Expand Down
43 changes: 40 additions & 3 deletions lib/signed-url.ts
Original file line number Diff line number Diff line change
@@ -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 =>
Expand Down Expand Up @@ -30,11 +36,14 @@ const resolveFromServer = async (
};

export const resolveSignedUrl = (url: string): Promise<string> => {
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;
Expand Down Expand Up @@ -126,3 +135,31 @@ export const useSignedUrls = (urls: string[]) => {

return { urls: resolved, loading };
};

export const resolveSignatureImageValue = async (
value: string,
): Promise<string> => {
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<FormValues> => {
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;
};
2 changes: 1 addition & 1 deletion next.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading