+
+ {def.question || 'Upload file'}
+ {(isRequired || isSoftRequired) && (
+
+ *
+
+ )}
+ {maxFiles > 1 && (
+
+ ({fileDataArr.length}/{maxFiles})
+
+ )}
+
+
+ {errorMsg && (
+
+ {errorMsg}
+
+ )}
+
+ {fileDataArr.length > 0 && (
+
+ {fileDataArr.map((file, index) => (
+
+
+
+ {file.title || 'Unnamed file'}
+
+
+ {file.contentType}
+ {file.size && •}
+ {file.size && {formatFileSize(file.size)}}
+
+
+ {!isReadOnly && isEnabled && (
+
+ )}
+
+ ))}
+
+ )}
+
+ {canAddMore && (
+
+
1}
+ onClick={(e) => {
+ (e.target as HTMLInputElement).value = '';
+ }}
+ onChange={(e) => handleFiles(e.target.files)}
+ disabled={!isEnabled || isReadOnly}
+ className="ms:hidden"
+ />
+
+
+ )}
+
+ );
+ }
+
+ // Builder mode
+ return (
+
+
+
+ onUpdate({ question: e.target.value })}
+ placeholder="Enter question"
+ className="ms:px-3 ms:py-2 ms:h-10 ms:w-full ms:border ms:border-msborder ms:bg-mssurface ms:text-mstext ms:rounded-lg"
+ />
+
+
+
+
+
+
+
+ {
+ const val = e.target.value ? Number(e.target.value) : undefined;
+ onUpdate({
+ maxFileSize: val
+ ? convertUnitToBytes(val, sizeUnit)
+ : undefined,
+ });
+ }}
+ placeholder="Optional"
+ className="ms:px-3 ms:py-2 ms:h-10 ms:flex-1 ms:border ms:border-msborder ms:bg-mssurface ms:text-mstext ms:rounded-lg"
+ />
+
+
+
+
+
+
+
+ onUpdate({
+ maxFiles: e.target.value ? Number(e.target.value) : undefined,
+ })
+ }
+ onBlur={(e) => {
+ const val = e.target.value ? Number(e.target.value) : 0;
+ if (val < 1) {
+ onUpdate({ maxFiles: 1 });
+ }
+ }}
+ placeholder="1"
+ className="ms:px-3 ms:py-2 ms:h-10 ms:w-full ms:border ms:border-msborder ms:bg-mssurface ms:text-mstext ms:rounded-lg"
+ />
+
+
+ );
+});
+
+export default FileField;
diff --git a/packages/fields/src/fields/rich/index.ts b/packages/fields/src/fields/rich/index.ts
index 3d1db22..af60f99 100644
--- a/packages/fields/src/fields/rich/index.ts
+++ b/packages/fields/src/fields/rich/index.ts
@@ -9,6 +9,7 @@ export type {
} from './DrawingPad.js';
export { DiagramField } from './DiagramField.js';
export { DisplayField } from './DisplayField.js';
+export { FileField } from './FileField.js';
export { HtmlField } from './HtmlField.js';
export { ImageField } from './ImageField.js';
export { SignatureField } from './SignatureField.js';
diff --git a/packages/fields/src/fields/selection/CheckField.tsx b/packages/fields/src/fields/selection/CheckField.tsx
index 0dda14f..05291ce 100644
--- a/packages/fields/src/fields/selection/CheckField.tsx
+++ b/packages/fields/src/fields/selection/CheckField.tsx
@@ -55,7 +55,7 @@ export const CheckField = React.memo(function CheckField({
htmlFor={`${instanceId}-check-answer-${def.id}-${option.id}`}
className={`ms:flex ms:items-center ms:gap-2 ms:rounded ms:transition-colors ms:py-1 ms:px-1 ${
isEnabled
- ? 'ms:cursor-pointer ms:hover:bg-msprimary/5'
+ ? 'ms:cursor-pointer ms:hover:bg-msprimary/5 ms:select-none'
: 'ms:cursor-not-allowed'
}`}
>
diff --git a/packages/fields/src/fields/selection/OpenChoiceField.tsx b/packages/fields/src/fields/selection/OpenChoiceField.tsx
new file mode 100644
index 0000000..e46c1cd
--- /dev/null
+++ b/packages/fields/src/fields/selection/OpenChoiceField.tsx
@@ -0,0 +1,228 @@
+import React from 'react';
+import type {
+ FieldComponentProps,
+ SelectedOption,
+ OpenChoiceFieldDefinition,
+ FieldOption,
+} from '@esheet/core';
+import { RadioGroup, Radio } from '@mieweb/ui';
+import { TrashIcon, PlusIcon } from '../../icons.js';
+
+export const OpenChoiceField = React.memo(function OpenChoiceField({
+ field,
+ form,
+ isPreview,
+ isEnabled,
+ isRequired,
+ isSoftRequired,
+ response,
+ onUpdate,
+ onResponse,
+}: FieldComponentProps) {
+ const def = field.definition as OpenChoiceFieldDefinition & {
+ question?: string;
+ };
+ const instanceId = form.getState().instanceId;
+ const options = def.options || [];
+ const otherLabel = def.otherLabel || 'Other, please Specify:';
+ const otherOptionId = `${def.id}-other`;
+ const selected =
+ (response?.selected as SelectedOption | undefined) ?? undefined;
+ const selectedId = selected?.id ?? null;
+ const isOtherSelected = selectedId === otherOptionId;
+
+ if (isPreview) {
+ const otherText =
+ selectedId === otherOptionId && selected?.value ? selected.value : '';
+
+ return (
+
+
+ {def.question || 'Question'}
+ {(isRequired || isSoftRequired) && (
+
+ *
+
+ )}
+
+
+
{
+ if (!val) {
+ onResponse({ selected: undefined });
+ return;
+ }
+
+ if (val === otherOptionId) {
+ onResponse({ selected: { id: otherOptionId, value: '' } });
+ return;
+ }
+
+ const opt = options.find((o) => o.id === val);
+ if (opt) {
+ onResponse({ selected: { id: opt.id, value: opt.value } });
+ }
+ }}
+ disabled={!isEnabled}
+ orientation="vertical"
+ className="ms:space-y-2"
+ >
+ {options.map((option: FieldOption) => (
+
+ ))}
+
+
+
+
+
+ {
+ onResponse({
+ selected: { id: otherOptionId, value: e.target.value },
+ });
+ }}
+ disabled={!isOtherSelected}
+ className={`ms:w-full ms:px-3 ms:py-2 ms:h-9 ms:rounded-lg ms:transition-all ${
+ isOtherSelected
+ ? 'ms:border ms:border-msborder ms:focus:border-msprimary ms:focus:ring-1 ms:focus:ring-msprimary/30 ms:outline-none ms:bg-mssurface ms:text-mstext ms:cursor-text'
+ : 'ms:border ms:border-dashed ms:border-msborder ms:bg-mssurface ms:text-mstextmuted ms:opacity-40 ms:cursor-not-allowed'
+ }`}
+ />
+
+
+
+ );
+ }
+
+ // Builder mode
+ return (
+
+
+
+ onUpdate({ question: e.target.value })}
+ placeholder="Enter question"
+ className="ms:px-3 ms:py-2 ms:h-10 ms:w-full ms:border ms:border-msborder ms:bg-mssurface ms:text-mstext ms:rounded-lg"
+ />
+
+
+
+
+ Options
+
+
+ {options.map((option: FieldOption) => (
+
+
+
+ form
+ .getState()
+ .updateOption(def.id, option.id, e.target.value)
+ }
+ placeholder="Option text"
+ className="ms:flex-1 ms:min-w-0 ms:outline-none ms:bg-transparent ms:text-mstext"
+ />
+
+
+ ))}
+
+
+
+
+
+
+
+ onUpdate({ otherLabel: e.target.value })}
+ placeholder="Other, please Specify :"
+ className="ms:px-3 ms:py-2 ms:h-10 ms:w-full ms:border ms:border-msborder ms:bg-mssurface ms:text-mstext ms:rounded-lg"
+ />
+
+
+ );
+});
+
+export default OpenChoiceField;
diff --git a/packages/fields/src/fields/selection/RadioField.tsx b/packages/fields/src/fields/selection/RadioField.tsx
index d625006..95b8176 100644
--- a/packages/fields/src/fields/selection/RadioField.tsx
+++ b/packages/fields/src/fields/selection/RadioField.tsx
@@ -54,7 +54,7 @@ export const RadioField = React.memo(function RadioField({
htmlFor={`${instanceId}-radio-answer-${def.id}-${option.id}`}
className={`ms:flex ms:items-center ms:gap-2 ms:rounded ms:transition-colors ms:py-1 ms:px-1 ${
isEnabled
- ? 'ms:cursor-pointer ms:hover:bg-msprimary/5'
+ ? 'ms:cursor-pointer ms:hover:bg-msprimary/5 ms:select-none'
: 'ms:cursor-not-allowed'
}`}
>
diff --git a/packages/fields/src/fields/selection/index.ts b/packages/fields/src/fields/selection/index.ts
index 2f39a13..5ae4320 100644
--- a/packages/fields/src/fields/selection/index.ts
+++ b/packages/fields/src/fields/selection/index.ts
@@ -3,3 +3,4 @@ export { CheckField } from './CheckField.js';
export { BooleanField } from './BooleanField.js';
export { DropdownField } from './DropdownField.js';
export { MultiSelectDropdownField } from './MultiSelectDropdownField.js';
+export { OpenChoiceField } from './OpenChoiceField.js';
diff --git a/packages/fields/src/icons.tsx b/packages/fields/src/icons.tsx
index 04dfecf..e340da2 100644
--- a/packages/fields/src/icons.tsx
+++ b/packages/fields/src/icons.tsx
@@ -156,3 +156,23 @@ export const XIcon = React.memo(
),
eq
);
+
+export const UploadIcon = React.memo(
+ ({ className = '' }: IconProps) => (
+