This commit is contained in:
natib21
2026-07-17 13:10:27 +00:00
parent 6f126a2345
commit 0d98ccb04b
8 changed files with 151 additions and 82 deletions

View File

@@ -39,6 +39,18 @@ interface DraftField extends CreateFileUploadFieldDto {
let draftCounter = 0;
const nextKey = () => `draft-${Date.now()}-${++draftCounter}`;
/**
* Extensions offered as checkboxes. Mirrors DOC_EXTENSIONS in the API's
* file-upload-settings seeder — the only formats the document flows accept.
*
* The API validates `allowedExtensions` as plain strings, so free text let
* typos ("pd") through silently and the field then rejected every real upload.
* A fixed list makes that unrepresentable.
*/
const FILE_EXTENSION_OPTIONS = ["pdf", "jpg", "jpeg", "png"] as const;
const KNOWN_EXTENSIONS = new Set<string>(FILE_EXTENSION_OPTIONS);
function makeEmptyDraft(idx: number): DraftField {
return {
key: nextKey(),
@@ -93,13 +105,19 @@ export default function ManageFileUploadFieldsDialog({
}),
);
const updateExtensions = (i: number, raw: string) => {
const list = raw
.split(",")
.map((s) => s.trim().toLowerCase().replace(/^\./, ""))
.filter(Boolean);
update(i, { allowedExtensions: list });
};
const toggleExtension = (i: number, ext: string, checked: boolean) =>
setFields((prev) =>
prev.map((f, idx) => {
if (idx !== i) return f;
const current = f.allowedExtensions;
if (checked) {
return current.includes(ext)
? f
: { ...f, allowedExtensions: [...current, ext] };
}
return { ...f, allowedExtensions: current.filter((e) => e !== ext) };
}),
);
const remove = (i: number) =>
setFields((prev) => prev.filter((_, idx) => idx !== i));
@@ -214,7 +232,9 @@ export default function ManageFileUploadFieldsDialog({
index={i}
total={fields.length}
onChange={(patch) => update(i, patch)}
onChangeExtensions={(raw) => updateExtensions(i, raw)}
onToggleExtension={(ext, checked) =>
toggleExtension(i, ext, checked)
}
onMove={(dir) => move(i, dir)}
onRemove={() => remove(i)}
/>
@@ -258,7 +278,7 @@ function FieldEditor({
index,
total,
onChange,
onChangeExtensions,
onToggleExtension,
onMove,
onRemove,
}: {
@@ -266,13 +286,23 @@ function FieldEditor({
index: number;
total: number;
onChange: (patch: Partial<DraftField>) => void;
onChangeExtensions: (raw: string) => void;
onToggleExtension: (ext: string, checked: boolean) => void;
onMove: (dir: -1 | 1) => void;
onRemove: () => void;
}) {
const minFiles = getMinFiles(field);
const effectiveMax = field.isMultiple ? field.maxFiles : 1;
// A field saved before this list existed can hold anything the old free-text
// box accepted (e.g. the typo "pd"). Show those alongside the standard ones so
// they stay visible and removable instead of silently vanishing on save.
const extensionChoices = [
...FILE_EXTENSION_OPTIONS,
...field.allowedExtensions.filter(
(ext) => !KNOWN_EXTENSIONS.has(ext),
),
];
return (
<div className="rounded-2xl border border-slate-200 bg-white p-4">
<div className="mb-3 flex items-center justify-between">
@@ -348,16 +378,33 @@ function FieldEditor({
</div>
<div className="space-y-1.5 md:col-span-2">
<Label className="text-xs">Allowed Extensions</Label>
<Input
value={field.allowedExtensions.join(", ")}
onChange={(e) => onChangeExtensions(e.target.value)}
placeholder="pdf, docx, jpg"
className="font-mono"
/>
<p className="text-xs text-slate-500">
Comma-separated, no leading dot.
</p>
<Label className="text-xs">Allowed Extensions *</Label>
<div className="flex flex-wrap items-center gap-x-4 gap-y-2">
{extensionChoices.map((ext) => (
<label
key={ext}
className="flex items-center gap-2 text-sm text-slate-700"
>
<input
type="checkbox"
checked={field.allowedExtensions.includes(ext)}
onChange={(e) => onToggleExtension(ext, e.target.checked)}
className="h-4 w-4 rounded border-slate-300 text-[#10B981] focus:ring-[#10B981]/20"
/>
<span className="font-mono">{ext}</span>
{!KNOWN_EXTENSIONS.has(ext) ? (
<span className="text-xs text-amber-600">(unrecognised)</span>
) : null}
</label>
))}
</div>
{field.allowedExtensions.length === 0 ? (
<p className="text-xs text-red-500">Pick at least one extension.</p>
) : (
<p className="text-xs text-slate-500">
Uploads are rejected unless the file matches one of these.
</p>
)}
</div>
<div className="space-y-1.5">