feat(fields): adds new features for FHIR Support capability + Minor Enhancements#88
Merged
Conversation
+ added Open Choice Field, with Radio Field functionality + an additional "other" field that can have custom input. + Added File Field Type (which includes, max file, max file size, accepted file type) + stores as base64. + Remove selection / highlighting from occuring when double clicking on all field Options
+ fix response handling for fileField
This was
linked to
issues
Jul 1, 2026
+ adds maxfiles properties for file + add OtherLabel for openChoice
- Add question-based slug generation for field IDs with collision-safe fallback. - Rewrite field references when IDs change, including rules, expressions, display content, and calculations. - Seed computed text values into responses so they behave like editable defaults. - Show a suggested ID in the editor UI and add tests for the new behavior.
There was a problem hiding this comment.
Pull request overview
This PR extends the eSheet Nx monorepo to support FHIR-required questionnaire capabilities for file uploads and open-choice responses by introducing new file and openchoice field types and wiring them through the core type system, registry, builder/renderer default registrations, condition logic, validation/hydration, and FHIR/SurveyJS adapters.
Changes:
- Added new
FileFieldandOpenChoiceFieldcomponents and registered them in builder/renderer defaults and package exports. - Extended core types/schemas/registry and condition + validation + hydration logic for
fileDataand open-choice custom values; introducedsetValueeffect plumbing into the renderer (computed values). - Updated FHIR + SurveyJS + MCP adapters, builder UI logic tooling (LogicEditor), ID generation/suggestion (slugify), and added/updated related tests.
Reviewed changes
Copilot reviewed 46 out of 48 changed files in this pull request and generated 10 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/renderer/src/lib/renderer.ts | Adds setValue computedValue resolution into render tree and helper to apply computed values back into responses. |
| packages/renderer/src/lib/register-defaults.ts | Registers openchoice and file renderer field components by default. |
| packages/renderer/src/index.ts | Re-exports render tree builder utilities/types. |
| packages/fields/src/index.ts | Exports OpenChoiceField and FileField from the fields package API. |
| packages/fields/src/icons.tsx | Adds UploadIcon for file upload UI. |
| packages/fields/src/fields/text/TextField.tsx | Seeds unanswered text responses from computedValue (setValue effects). |
| packages/fields/src/fields/selection/RadioField.tsx | Adds ms:select-none to improve selection UX. |
| packages/fields/src/fields/selection/OpenChoiceField.tsx | Introduces the new open-choice field UI (predefined options + “other” free text). |
| packages/fields/src/fields/selection/index.ts | Re-exports OpenChoiceField. |
| packages/fields/src/fields/selection/CheckField.tsx | Adds ms:select-none to improve selection UX. |
| packages/fields/src/fields/rich/index.ts | Re-exports FileField. |
| packages/fields/src/fields/rich/FileField.tsx | Introduces file upload field UI with drag/drop, constraints, and builder configuration UI. |
| packages/fields/src/fields/matrix/SingleMatrixField.tsx | Adds ms:select-none for matrix option labels. |
| packages/fields/src/fields/matrix/MultiMatrixField.tsx | Adds ms:select-none for matrix option labels. |
| packages/fields/src/fields/index.ts | Wires new fields into the umbrella fields export. |
| packages/core/src/lib/types.ts | Adds file/openchoice types + schemas, adds fileData + attachment size, adds setValue conditional effect, removes readOnly from schema (TODO). |
| packages/core/src/lib/types.spec.ts | Updates built-in field type count expectations for new types. |
| packages/core/src/lib/stores/form-store.ts | Adds cross-field reference rewrite on rename; updates calculation behavior; disables readOnly behavior (TODO). |
| packages/core/src/lib/stores/form-store.spec.ts | Adds rename/reference rewrite test coverage. |
| packages/core/src/lib/registry.ts | Registers metadata for the new built-in field types. |
| packages/core/src/lib/registry.spec.ts | Updates registry counts for new types. |
| packages/core/src/lib/logic/validate.ts | Treats fileData as non-empty for required/emptiness logic. |
| packages/core/src/lib/logic/resolve.ts | Adds resolveSetValue (value effect) and refactors boolean effect handling. |
| packages/core/src/lib/logic/conditions.ts | Adds expression normalization helper; adds openchoice/file support for expression/value extraction. |
| packages/core/src/lib/logic/comprehensive-conditions.spec.ts | Adds integration tests for openchoice and file condition behavior. |
| packages/core/src/lib/functions/ids.ts | Adds slugifyQuestion and enhances generateFieldId to use question-based slugs. |
| packages/core/src/lib/functions/ids.spec.ts | Adds tests for slug-based ID generation and slugify behavior. |
| packages/core/src/lib/functions/hydrate-response.ts | Ensures attachment hydration can return fileData for media answers. |
| packages/core/src/lib/field-component-props.ts | Adds computedValue to field component props. |
| packages/core/src/index.ts | Re-exports new types/functions (slugify/normalizeExpression/resolveSetValue, etc.). |
| packages/builder/tsconfig.lib.json | Adds renderer project reference ordering/update. |
| packages/builder/tsconfig.json | Adds renderer reference (and adjusts ordering). |
| packages/builder/src/lib/register-defaults.ts | Registers openchoice and file builder field components by default. |
| packages/builder/src/lib/components/FieldWrapper.tsx | Plumbs computedValue through wrapper render props. |
| packages/builder/src/lib/components/edit-panel/LogicEditor.tsx | Adds setValue effect UI, function chips, and openchoice/file operator handling. |
| packages/builder/src/lib/components/edit-panel/EditPanel.tsx | Removes readOnly prop wiring (readOnly not implemented). |
| packages/builder/src/lib/components/edit-panel/CommonEditor.tsx | Adds suggested ID button using slugifyQuestion(question). |
| packages/builder/src/lib/components/Canvas.tsx | Builds a render tree to compute and pass setValue computedValue into field rendering. |
| packages/builder/src/index.ts | Normalizes exports (type exports inline). |
| packages/builder/package.json | Adds dependency on @esheet/renderer. |
| packages/adapters/src/lib/surveyjs-converter.ts | Maps SurveyJS select → openchoice and file → file, and attempts to preserve readOnly. |
| packages/adapters/src/lib/mcp.ts | Adds MCP export handling/metadata for openchoice and file fields. |
| packages/adapters/src/fhir/utils.ts | Maps FHIR attachment/open-choice itemControl to file/openchoice field types. |
| packages/adapters/src/fhir/fhir-adapter.ts | Imports FHIR expressions as setValue rules; maps file/openchoice answers to FHIR; updates attachment handling. |
| packages/adapters/src/fhir/fhir-adapter.spec.ts | Updates tests for attachment mapping and open-choice mapping. |
| eslint.config.mjs | Ignores apps/docs/static/** in ESLint. |
| apps/docs/static/loco.min.js | Modifies a bundled/minified static asset. |
Comment on lines
454
to
462
| // Base field | ||
| const base = { | ||
| id, | ||
| question: element.title ?? element.name, | ||
| required: element.isRequired ?? false, | ||
| ...(element.readOnly ? { readOnly: element.readOnly } : {}), | ||
| rules, | ||
| _sourceData, | ||
| }; |
Comment on lines
132
to
138
| // Build base field | ||
| const base = { | ||
| id: item.linkId, | ||
| question: item.text, | ||
| required: item.required, | ||
| ...(item.readOnly ? { readOnly: item.readOnly } : {}), | ||
| }; |
Comment on lines
+79
to
+82
| for (const rule of rules) { | ||
| if (evaluateRule(rule, normalized, responses, dangerouslyAllowJS)) { | ||
| // Rule matched; now extract and evaluate the expression condition to get the value. | ||
| // Typically, a setValue rule will have ONE expression condition that produces the value. |
Comment on lines
+2245
to
+2250
| it('equals — matches __other__ when user types custom text', () => { | ||
| const { normalized, field } = setup(openChoiceDef); | ||
| field.rules = [fieldRule('visible', 'other-1', 'equals', '__other__')]; | ||
| const resp: FieldResponse = { | ||
| selected: { id: '__other__', value: 'custom text' }, | ||
| }; |
Comment on lines
+2280
to
+2300
| it('expression — returns user-typed text for __other__', () => { | ||
| const driver: FieldDefinition = { | ||
| id: 'other-5', | ||
| fieldType: 'openchoice', | ||
| question: 'Source', | ||
| options: [{ id: 'other-6', value: 'X' }], | ||
| }; | ||
| const target: FieldDefinition = { | ||
| id: 'other-7', | ||
| fieldType: 'text', | ||
| question: 'Target', | ||
| rules: [exprRule('visible', "{other-5} == 'custom text'")], | ||
| }; | ||
| const normalized = normalizeDefinition([driver, target]); | ||
| const resp: FieldResponse = { | ||
| selected: { id: '__other__', value: 'custom text' }, | ||
| }; | ||
| expect( | ||
| resolveEffect('visible', target, normalized, { 'other-5': resp }) | ||
| ).toBe(true); | ||
| }); |
Comment on lines
+121
to
+138
| <div className="ms:mt-2"> | ||
| <input | ||
| id={`${instanceId}-openchoice-other-${def.id}`} | ||
| type="text" | ||
| value={otherText} | ||
| onChange={(e) => { | ||
| 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' | ||
| }`} | ||
| /> | ||
| </div> |
Comment on lines
+378
to
+389
| <div> | ||
| <label className="ms:block ms:text-sm ms:font-medium ms:text-mstextmuted ms:mb-2"> | ||
| Accepted types | ||
| </label> | ||
| <input | ||
| type="text" | ||
| placeholder="Search file types..." | ||
| value={typeSearch} | ||
| onChange={(e) => setTypeSearch(e.target.value)} | ||
| 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 ms:mb-2" | ||
| /> | ||
| <div className="ms:space-y-2 ms:h-28 ms:overflow-y-auto ms:border ms:border-msborder ms:rounded-lg ms:p-3 ms:bg-mssurface"> |
Comment on lines
+420
to
+442
| <div> | ||
| <label className="ms:block ms:text-sm ms:font-medium ms:text-mstextmuted ms:mb-1"> | ||
| Max file size | ||
| </label> | ||
| <div className="ms:flex ms:gap-2"> | ||
| <input | ||
| type="number" | ||
| value={ | ||
| def.maxFileSize | ||
| ? convertBytesToUnit(def.maxFileSize, sizeUnit) | ||
| : '' | ||
| } | ||
| onChange={(e) => { | ||
| 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" | ||
| /> |
Comment on lines
+443
to
+455
| <select | ||
| value={sizeUnit} | ||
| onChange={(e) => { | ||
| const newUnit = e.target.value as SizeUnit; | ||
| setSizeUnit(newUnit); | ||
| }} | ||
| className="ms:px-3 ms:py-2 ms:h-10 ms:border ms:border-msborder ms:bg-mssurface ms:text-mstext ms:rounded-lg" | ||
| > | ||
| <option value="KB">KB</option> | ||
| <option value="MB">MB</option> | ||
| <option value="GB">GB</option> | ||
| </select> | ||
| </div> |
Comment on lines
+462
to
+479
| <input | ||
| type="number" | ||
| min="1" | ||
| value={def.maxFiles ?? ''} | ||
| onChange={(e) => | ||
| 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" | ||
| /> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Overview
This PR adds FHIR-required questionnaire support for file upload and open-choice responses.
It introduces two new field types,
fileandopenchoice, and wires them through the core type system, field registry, builder, renderer, condition logic, response hydration, validation, and FHIR adapter. These changes allow questionnaires to capture attachment-based answers and choice answers that can include user-entered custom values.What Changed
Added a new
FileFieldcomponent for single or multiple file uploads.Added a new
OpenChoiceFieldcomponent for predefined options with custom user-entered values.Extended core field definitions, Zod schemas, and field type metadata for
fileandopenchoice.Added
fileDataresponse support for attachment answers.Updated the field registry with metadata for the new field types.
Registered the new fields across the builder and renderer default field sets.
Added icons and exports for the new field components.
Updated condition evaluation to support:
Updated validation so uploaded files are treated as non-empty responses.
Improved response hydration for file/media attachment answers.
Updated the FHIR adapter to map:
valueAttachmentAdded expanded test coverage for condition behavior and adapter conversion.
How to Test
filefield.openchoicefield.Breaking Changes
No breaking changes.