@evoke-platform/ui-components 1.10.1-dev.1 → 1.10.1-dev.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,12 +2,14 @@ import { createFilterOptions, List, ListSubheader } from '@mui/material';
2
2
  import { uniq } from 'lodash';
3
3
  import React, { forwardRef, useEffect, useRef, useState } from 'react';
4
4
  import { Clear } from '../../../../icons';
5
+ import { useFormContext } from '../../../../theme/hooks';
5
6
  import { Autocomplete, FormControl, FormControlLabel, IconButton, Radio, RadioGroup, TextField, Typography, } from '../../../core';
6
7
  import { Box } from '../../../layout';
7
8
  import InputFieldComponent from '../InputFieldComponent/InputFieldComponent';
8
9
  const filter = createFilterOptions();
9
10
  const Select = (props) => {
10
11
  const { id, property, defaultValue, error, errorMessage, onBlur, onChange, readOnly, isCombobox, selectOptions, required, size, isOptionEqualToValue, renderOption, getOptionLabel, disableCloseOnSelect, additionalProps, displayOption, sortBy, } = props;
12
+ const { onAutosave } = useFormContext();
11
13
  const otherInputRef = useRef(null);
12
14
  const [isOther, setIsOther] = useState(!!isCombobox &&
13
15
  !!defaultValue &&
@@ -32,7 +34,7 @@ const Select = (props) => {
32
34
  setValue(defaultValue);
33
35
  }, [defaultValue]);
34
36
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
35
- const handleChange = (event, selected) => {
37
+ const handleChange = async (event, selected) => {
36
38
  if (Array.isArray(selected)) {
37
39
  const newValues = selected.map((option) => option.value ?? option);
38
40
  setValue(uniq(newValues));
@@ -56,6 +58,12 @@ const Select = (props) => {
56
58
  onChange && onChange(property.id, selected, property);
57
59
  }
58
60
  }
61
+ try {
62
+ await onAutosave?.(id);
63
+ }
64
+ catch (error) {
65
+ console.error('Autosave failed:', error);
66
+ }
59
67
  };
60
68
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
61
69
  const handleInputValueChange = (event, selectValue) => {
@@ -121,22 +129,29 @@ const Select = (props) => {
121
129
  }
122
130
  setIsOtherFocused(false);
123
131
  } })))))),
124
- displayOption === 'radioButton' && onChange && !readOnly && value && (React.createElement(Box, { sx: {
132
+ onChange && !readOnly && value && (React.createElement(Box, { sx: {
125
133
  ':hover': { cursor: 'pointer' },
126
134
  marginTop: '4px',
127
135
  display: 'flex',
128
136
  alignItems: 'center',
129
137
  } },
130
- React.createElement(IconButton, { "aria-label": `Clear`, onClick: () => {
138
+ React.createElement(IconButton, { "aria-label": `Clear`, onClick: async () => {
131
139
  setValue('');
132
140
  property && onChange(property.id, '');
133
141
  setIsOther(false);
142
+ // Trigger autosave immediately when clearing radio selection
143
+ try {
144
+ await onAutosave?.(id);
145
+ }
146
+ catch (error) {
147
+ console.error('Autosave failed:', error);
148
+ }
134
149
  }, sx: { padding: '3px', marginRight: '6px' } },
135
150
  React.createElement(Clear, { sx: {
136
151
  color: '#637381',
137
152
  fontSize: '1.2rem',
138
153
  } })),
139
- React.createElement(Typography, { variant: "caption" }, "Clear Selection"))))) : (React.createElement(Autocomplete, { multiple: property?.type === 'array' ? true : false, id: id, sortBy: sortBy, renderInput: (params) => (React.createElement(TextField, { ...params, value: value, fullWidth: true, onBlur: onBlur, inputProps: {
154
+ React.createElement(Typography, { variant: "caption" }, "Clear Selection"))))) : (React.createElement(Autocomplete, { multiple: property?.type === 'array', id: id, sortBy: sortBy, renderInput: (params) => (React.createElement(TextField, { ...params, value: value, fullWidth: true, onBlur: onBlur, inputProps: {
140
155
  ...params.inputProps,
141
156
  'aria-describedby': isCombobox ? `${id}-instructions` : undefined,
142
157
  } })), value: value ?? (property?.type === 'array' ? [] : undefined), onChange: handleChange, options: selectOptions ?? property?.enum ?? [], inputValue: inputValue ?? '', error: error, errorMessage: errorMessage, required: required, onInputChange: handleInputValueChange, size: size, filterOptions: (options, params) => {
@@ -169,6 +184,10 @@ const Select = (props) => {
169
184
  '& button.MuiButtonBase-root': {
170
185
  visibility: 'visible',
171
186
  },
187
+ }, slotProps: {
188
+ clearIndicator: {
189
+ 'data-testid': 'autocomplete-clear-button',
190
+ },
172
191
  }, forcePopupIcon: true, ...(isCombobox ? { selectOnFocus: true, handleHomeEndKeys: true, freeSolo: true } : {}), ...(additionalProps ?? {}) }));
173
192
  };
174
193
  export default Select;
@@ -326,12 +326,7 @@ describe('FormRendererContainer', () => {
326
326
  await user.clear(cityField);
327
327
  await user.type(cityField, 'Cambridge');
328
328
  await user.tab(); // Blur the field
329
- // Verify autosave was eventually called and the final call contains the updated city
330
- await waitFor(() => {
331
- expect(autosaveActionSpy).toHaveBeenCalled();
332
- });
333
- const lastCall = autosaveActionSpy.mock.lastCall?.[0];
334
- expect(lastCall).toEqual(expect.objectContaining({
329
+ expect(autosaveActionSpy).toHaveBeenCalledWith(expect.objectContaining({
335
330
  input: expect.objectContaining({
336
331
  address: expect.objectContaining({
337
332
  city: 'Cambridge',
@@ -448,15 +443,7 @@ describe('FormRendererContainer', () => {
448
443
  // Wait for and select the autocomplete option
449
444
  const autocompleteOption = await screen.findByText('456 Oak Street');
450
445
  await user.click(autocompleteOption);
451
- // Verify autosave was eventually called and the final call contains the expected address values
452
- await waitFor(() => {
453
- expect(autosaveActionSpy).toHaveBeenCalled();
454
- });
455
- // The autosave is triggered twice when selecting the autocomplete option,
456
- // once by the selection and once by the onBlur event. We want to verify the last call
457
- // has the correct data.
458
- const lastCall = autosaveActionSpy.mock.lastCall?.[0];
459
- expect(lastCall).toEqual(expect.objectContaining({
446
+ expect(autosaveActionSpy).toHaveBeenCalledWith(expect.objectContaining({
460
447
  input: expect.objectContaining({
461
448
  address: expect.objectContaining({
462
449
  line1: '456 Oak Street',
@@ -531,6 +518,484 @@ describe('FormRendererContainer', () => {
531
518
  // Verify autosave was not triggered
532
519
  expect(autosaveActionSpy).not.toHaveBeenCalled();
533
520
  });
521
+ it('should trigger autosave immediately when a radio button is selected', async () => {
522
+ const user = userEvent.setup();
523
+ const autosaveActionSpy = vi.fn();
524
+ // Create a form with a status field displayed as radio buttons
525
+ const licenseFormWithRadioButtons = {
526
+ id: 'licenseForm',
527
+ name: 'License Form',
528
+ objectId: 'license',
529
+ actionId: '_update',
530
+ autosaveActionId: '_autosave',
531
+ entries: [
532
+ {
533
+ parameterId: 'name',
534
+ type: 'input',
535
+ display: {
536
+ label: 'License Number',
537
+ },
538
+ },
539
+ {
540
+ parameterId: 'status',
541
+ type: 'input',
542
+ display: {
543
+ label: 'Status',
544
+ choicesDisplay: {
545
+ type: 'radioButton',
546
+ },
547
+ },
548
+ },
549
+ ],
550
+ };
551
+ server.use(http.get('/api/data/objects/license/instances/test-license', () => {
552
+ return HttpResponse.json({
553
+ id: 'test-license',
554
+ name: 'RN-123456',
555
+ status: 'Active',
556
+ });
557
+ }), http.get('/api/data/objects/license/instances/test-license/object', () => {
558
+ return HttpResponse.json(licenseObject);
559
+ }), http.get('/api/data/forms/licenseForm', () => {
560
+ return HttpResponse.json(licenseFormWithRadioButtons);
561
+ }), http.post('/api/data/objects/license/instances/test-license/actions', async ({ request }) => {
562
+ const body = (await request.json());
563
+ autosaveActionSpy(body);
564
+ return HttpResponse.json({
565
+ id: 'test-license',
566
+ name: 'RN-123456',
567
+ status: body.input.status,
568
+ });
569
+ }));
570
+ render(React.createElement(FormRendererContainer, { objectId: 'license', formId: 'licenseForm', dataType: 'objectInstances', actionId: '_update', instanceId: 'test-license' }));
571
+ // Wait for the form to load and the radio buttons to appear
572
+ const inactiveRadio = await screen.findByRole('radio', { name: 'Inactive' });
573
+ // Click the "Inactive" radio button
574
+ await user.click(inactiveRadio);
575
+ // Verify autosave was triggered immediately with the new status
576
+ await waitFor(() => {
577
+ expect(autosaveActionSpy).toHaveBeenCalledWith(expect.objectContaining({
578
+ actionId: '_autosave',
579
+ input: expect.objectContaining({
580
+ status: 'Inactive',
581
+ }),
582
+ }));
583
+ });
584
+ });
585
+ it('should trigger autosave when the selection is cleared', async () => {
586
+ const user = userEvent.setup();
587
+ const autosaveActionSpy = vi.fn();
588
+ const licenseFormWithRadioButtons = {
589
+ id: 'licenseForm',
590
+ name: 'License Form',
591
+ objectId: 'license',
592
+ actionId: '_update',
593
+ autosaveActionId: '_autosave',
594
+ entries: [
595
+ {
596
+ parameterId: 'name',
597
+ type: 'input',
598
+ display: {
599
+ label: 'License Number',
600
+ },
601
+ },
602
+ {
603
+ parameterId: 'status',
604
+ type: 'input',
605
+ display: {
606
+ label: 'Status',
607
+ choicesDisplay: {
608
+ type: 'radioButton',
609
+ },
610
+ },
611
+ },
612
+ ],
613
+ };
614
+ server.use(http.get('/api/data/objects/license/instances/test-license', () => {
615
+ return HttpResponse.json({
616
+ id: 'test-license',
617
+ name: 'RN-123456',
618
+ status: 'Active',
619
+ });
620
+ }), http.get('/api/data/objects/license/instances/test-license/object', () => {
621
+ return HttpResponse.json(licenseObject);
622
+ }), http.get('/api/data/forms/licenseForm', () => {
623
+ return HttpResponse.json(licenseFormWithRadioButtons);
624
+ }), http.post('/api/data/objects/license/instances/test-license/actions', async ({ request }) => {
625
+ const body = (await request.json());
626
+ autosaveActionSpy(body);
627
+ return HttpResponse.json({
628
+ id: 'test-license',
629
+ name: 'RN-123456',
630
+ status: body.input.status,
631
+ });
632
+ }));
633
+ render(React.createElement(FormRendererContainer, { objectId: 'license', formId: 'licenseForm', dataType: 'objectInstances', actionId: '_update', instanceId: 'test-license' }));
634
+ const clearButton = await screen.findByRole('button', { name: 'Clear' });
635
+ await user.click(clearButton);
636
+ expect(autosaveActionSpy).toHaveBeenCalledWith(expect.objectContaining({
637
+ actionId: '_autosave',
638
+ input: expect.objectContaining({
639
+ status: null,
640
+ }),
641
+ }));
642
+ });
643
+ it('should trigger autosave immediately when a dropdown option is selected', async () => {
644
+ const user = userEvent.setup();
645
+ const autosaveActionSpy = vi.fn();
646
+ const licenseFormWithDropdown = {
647
+ id: 'licenseForm',
648
+ name: 'License Form',
649
+ objectId: 'license',
650
+ actionId: '_update',
651
+ autosaveActionId: '_autosave',
652
+ entries: [
653
+ {
654
+ parameterId: 'name',
655
+ type: 'input',
656
+ display: { label: 'License Number' },
657
+ },
658
+ {
659
+ parameterId: 'status',
660
+ type: 'input',
661
+ display: {
662
+ label: 'Status',
663
+ choicesDisplay: { type: 'dropdown' },
664
+ },
665
+ },
666
+ ],
667
+ };
668
+ server.use(http.get('/api/data/objects/license/instances/test-license', () => HttpResponse.json({ id: 'test-license', name: 'RN-123456', status: 'Active' })), http.get('/api/data/objects/license/instances/test-license/object', () => HttpResponse.json(licenseObject)), http.get('/api/data/forms/licenseForm', () => HttpResponse.json(licenseFormWithDropdown)), http.post('/api/data/objects/license/instances/test-license/actions', async ({ request }) => {
669
+ const body = (await request.json());
670
+ autosaveActionSpy(body);
671
+ return HttpResponse.json({ id: 'test-license', name: 'RN-123456', status: body.input.status });
672
+ }));
673
+ render(React.createElement(FormRendererContainer, { objectId: 'license', formId: 'licenseForm', dataType: 'objectInstances', actionId: '_update', instanceId: 'test-license' }));
674
+ // Open the dropdown/combobox and choose an option
675
+ const combobox = await screen.findByRole('combobox', { name: 'Status' });
676
+ await user.click(combobox);
677
+ const inactiveOption = await screen.findByRole('option', { name: 'Inactive' });
678
+ await user.click(inactiveOption);
679
+ expect(autosaveActionSpy).toHaveBeenCalledWith({
680
+ actionId: '_autosave',
681
+ input: expect.objectContaining({
682
+ status: 'Inactive',
683
+ }),
684
+ });
685
+ });
686
+ it('should trigger autosave when dropdown selection is cleared', async () => {
687
+ const user = userEvent.setup();
688
+ const autosaveActionSpy = vi.fn();
689
+ const licenseFormWithDropdown = {
690
+ id: 'licenseForm',
691
+ name: 'License Form',
692
+ objectId: 'license',
693
+ actionId: '_update',
694
+ autosaveActionId: '_autosave',
695
+ entries: [
696
+ {
697
+ parameterId: 'name',
698
+ type: 'input',
699
+ display: { label: 'License Number' },
700
+ },
701
+ {
702
+ parameterId: 'status',
703
+ type: 'input',
704
+ display: {
705
+ label: 'Status',
706
+ choicesDisplay: { type: 'dropdown' },
707
+ },
708
+ },
709
+ ],
710
+ };
711
+ server.use(http.get('/api/data/objects/license/instances/test-license', () => HttpResponse.json({ id: 'test-license', name: 'RN-123456', status: 'Active' })), http.get('/api/data/objects/license/instances/test-license/object', () => HttpResponse.json(licenseObject)), http.get('/api/data/forms/licenseForm', () => HttpResponse.json(licenseFormWithDropdown)), http.post('/api/data/objects/license/instances/test-license/actions', async ({ request }) => {
712
+ const body = (await request.json());
713
+ autosaveActionSpy(body);
714
+ return HttpResponse.json({ id: 'test-license', name: 'RN-123456', status: body.input.status });
715
+ }));
716
+ render(React.createElement(FormRendererContainer, { objectId: 'license', formId: 'licenseForm', dataType: 'objectInstances', actionId: '_update', instanceId: 'test-license' }));
717
+ const combobox = await screen.findByRole('combobox', { name: 'Status' });
718
+ await user.click(combobox);
719
+ // Clear button has tabindex="-1" so find it via testid
720
+ const clearButton = await screen.findByTestId('autocomplete-clear-button');
721
+ await user.click(clearButton);
722
+ expect(autosaveActionSpy).toHaveBeenCalledWith(expect.objectContaining({
723
+ actionId: '_autosave',
724
+ input: expect.objectContaining({
725
+ status: null,
726
+ }),
727
+ }));
728
+ });
729
+ it('should trigger autosave for multi-select when selecting an option', async () => {
730
+ const user = userEvent.setup();
731
+ const autosaveActionSpy = vi.fn();
732
+ const licenseFormWithMultiSelect = {
733
+ id: 'licenseForm',
734
+ name: 'License Form',
735
+ objectId: 'license',
736
+ actionId: '_update',
737
+ autosaveActionId: '_autosave',
738
+ entries: [
739
+ {
740
+ parameterId: 'name',
741
+ type: 'input',
742
+ display: { label: 'License Number' },
743
+ },
744
+ {
745
+ type: 'inputField',
746
+ input: {
747
+ id: 'categories',
748
+ type: 'array',
749
+ enum: ['Electrical', 'Plumbing', 'Mechanical', 'Other'],
750
+ },
751
+ display: {
752
+ label: 'Categories',
753
+ choicesDisplay: { type: 'dropdown' },
754
+ },
755
+ },
756
+ ],
757
+ };
758
+ const licenseInstanceInitial = { id: 'test-license', name: 'RN-123456' };
759
+ server.use(http.get('/api/data/objects/license/instances/test-license', () => HttpResponse.json(licenseInstanceInitial)), http.get('/api/data/objects/license/instances/test-license/object', () => HttpResponse.json(licenseObject)), http.get('/api/data/forms/licenseForm', () => HttpResponse.json(licenseFormWithMultiSelect)), http.post('/api/data/objects/license/instances/test-license/actions', async ({ request }) => {
760
+ const body = (await request.json());
761
+ autosaveActionSpy(body);
762
+ return HttpResponse.json({
763
+ id: 'test-license',
764
+ name: body.input.name ?? 'RN-123456',
765
+ categories: body.input.categories,
766
+ });
767
+ }));
768
+ render(React.createElement(FormRendererContainer, { objectId: 'license', formId: 'licenseForm', dataType: 'objectInstances', actionId: '_update', instanceId: 'test-license' }));
769
+ // Open the multi-select combobox and choose an additional option
770
+ const combobox = await screen.findByRole('combobox', { name: 'Categories' });
771
+ await user.click(combobox);
772
+ const plumbingOption = await screen.findByRole('option', { name: 'Electrical' });
773
+ await user.click(plumbingOption);
774
+ expect(autosaveActionSpy).toHaveBeenCalledWith({
775
+ actionId: '_autosave',
776
+ input: expect.objectContaining({ categories: ['Electrical'] }),
777
+ });
778
+ });
779
+ it('should trigger autosave for multi-select when adding an option', async () => {
780
+ const user = userEvent.setup();
781
+ const autosaveActionSpy = vi.fn();
782
+ const licenseFormWithMultiSelect = {
783
+ id: 'licenseForm',
784
+ name: 'License Form',
785
+ objectId: 'license',
786
+ actionId: '_update',
787
+ autosaveActionId: '_autosave',
788
+ entries: [
789
+ {
790
+ parameterId: 'name',
791
+ type: 'input',
792
+ display: { label: 'License Number' },
793
+ },
794
+ {
795
+ type: 'inputField',
796
+ input: {
797
+ id: 'categories',
798
+ type: 'array',
799
+ enum: ['Electrical', 'Plumbing', 'Mechanical', 'Other'],
800
+ },
801
+ display: {
802
+ label: 'Categories',
803
+ choicesDisplay: { type: 'dropdown' },
804
+ },
805
+ },
806
+ ],
807
+ };
808
+ const licenseInstanceInitial = { id: 'test-license', name: 'RN-123456', categories: ['Electrical'] };
809
+ server.use(http.get('/api/data/objects/license/instances/test-license', () => HttpResponse.json(licenseInstanceInitial)), http.get('/api/data/objects/license/instances/test-license/object', () => HttpResponse.json(licenseObject)), http.get('/api/data/forms/licenseForm', () => HttpResponse.json(licenseFormWithMultiSelect)), http.post('/api/data/objects/license/instances/test-license/actions', async ({ request }) => {
810
+ const body = (await request.json());
811
+ autosaveActionSpy(body);
812
+ return HttpResponse.json({
813
+ id: 'test-license',
814
+ name: body.input.name ?? 'RN-123456',
815
+ categories: body.input.categories,
816
+ });
817
+ }));
818
+ render(React.createElement(FormRendererContainer, { objectId: 'license', formId: 'licenseForm', dataType: 'objectInstances', actionId: '_update', instanceId: 'test-license' }));
819
+ // Open the multi-select combobox and choose an additional option
820
+ const combobox = await screen.findByRole('combobox', { name: 'Categories' });
821
+ await user.click(combobox);
822
+ const plumbingOption = await screen.findByRole('option', { name: 'Plumbing' });
823
+ await user.click(plumbingOption);
824
+ expect(autosaveActionSpy).toHaveBeenCalledWith({
825
+ actionId: '_autosave',
826
+ input: expect.objectContaining({
827
+ categories: expect.arrayContaining(['Electrical', 'Plumbing']),
828
+ }),
829
+ });
830
+ });
831
+ it('should trigger autosave when a multi-select option is removed', async () => {
832
+ const user = userEvent.setup();
833
+ const autosaveActionSpy = vi.fn();
834
+ const licenseFormWithMultiSelect = {
835
+ id: 'licenseForm',
836
+ name: 'License Form',
837
+ objectId: 'license',
838
+ actionId: '_update',
839
+ autosaveActionId: '_autosave',
840
+ entries: [
841
+ {
842
+ parameterId: 'name',
843
+ type: 'input',
844
+ display: { label: 'License Number' },
845
+ },
846
+ {
847
+ type: 'inputField',
848
+ input: {
849
+ id: 'categories',
850
+ type: 'array',
851
+ enum: ['Electrical', 'Plumbing', 'Mechanical', 'Other'],
852
+ },
853
+ display: {
854
+ label: 'Categories',
855
+ choicesDisplay: { type: 'dropdown' },
856
+ },
857
+ },
858
+ ],
859
+ };
860
+ server.use(http.get('/api/data/objects/license/instances/test-license', () => HttpResponse.json({
861
+ id: 'test-license',
862
+ name: 'RN-123456',
863
+ categories: ['Electrical', 'Plumbing'],
864
+ })), http.get('/api/data/objects/license/instances/test-license/object', () => HttpResponse.json(licenseObject)), http.get('/api/data/forms/licenseForm', () => HttpResponse.json(licenseFormWithMultiSelect)), http.post('/api/data/objects/license/instances/test-license/actions', async ({ request }) => {
865
+ const body = (await request.json());
866
+ autosaveActionSpy(body);
867
+ return HttpResponse.json({
868
+ id: 'test-license',
869
+ name: 'RN-123456',
870
+ categories: body.input.categories,
871
+ });
872
+ }));
873
+ render(React.createElement(FormRendererContainer, { objectId: 'license', formId: 'licenseForm', dataType: 'objectInstances', actionId: '_update', instanceId: 'test-license' }));
874
+ const electricalChipLabel = await screen.findByRole('button', { name: 'Electrical' });
875
+ // Mui autoamtically provides a test id.
876
+ const deleteIcon = await within(electricalChipLabel).findByTestId('CancelIcon');
877
+ await user.click(deleteIcon);
878
+ // Autosave should be called with only Plumbing remaining
879
+ await waitFor(() => {
880
+ expect(autosaveActionSpy).toHaveBeenCalledWith({
881
+ actionId: '_autosave',
882
+ input: expect.objectContaining({
883
+ categories: ['Plumbing'],
884
+ }),
885
+ });
886
+ });
887
+ });
888
+ it('should trigger autosave when a single multi-select option is removed via the dropdown', async () => {
889
+ const user = userEvent.setup();
890
+ const autosaveActionSpy = vi.fn();
891
+ const licenseFormWithMultiSelect = {
892
+ id: 'licenseForm',
893
+ name: 'License Form',
894
+ objectId: 'license',
895
+ actionId: '_update',
896
+ autosaveActionId: '_autosave',
897
+ entries: [
898
+ {
899
+ parameterId: 'name',
900
+ type: 'input',
901
+ display: { label: 'License Number' },
902
+ },
903
+ {
904
+ type: 'inputField',
905
+ input: {
906
+ id: 'categories',
907
+ type: 'array',
908
+ enum: ['Electrical', 'Plumbing', 'Mechanical', 'Other'],
909
+ },
910
+ display: {
911
+ label: 'Categories',
912
+ choicesDisplay: { type: 'dropdown' },
913
+ },
914
+ },
915
+ ],
916
+ };
917
+ server.use(http.get('/api/data/objects/license/instances/test-license', () => HttpResponse.json({
918
+ id: 'test-license',
919
+ name: 'RN-123456',
920
+ categories: ['Electrical', 'Plumbing'],
921
+ })), http.get('/api/data/objects/license/instances/test-license/object', () => HttpResponse.json(licenseObject)), http.get('/api/data/forms/licenseForm', () => HttpResponse.json(licenseFormWithMultiSelect)), http.post('/api/data/objects/license/instances/test-license/actions', async ({ request }) => {
922
+ const body = (await request.json());
923
+ autosaveActionSpy(body);
924
+ return HttpResponse.json({
925
+ id: 'test-license',
926
+ name: 'RN-123456',
927
+ categories: body.input.categories,
928
+ });
929
+ }));
930
+ render(React.createElement(FormRendererContainer, { objectId: 'license', formId: 'licenseForm', dataType: 'objectInstances', actionId: '_update', instanceId: 'test-license' }));
931
+ // Open the multi-select combobox and unselect the 'Plumbing' option
932
+ const combobox = await screen.findByRole('combobox', { name: 'Categories' });
933
+ await user.click(combobox);
934
+ // Click the option to toggle/unselect it (initially selected)
935
+ const plumbingOption = await screen.findByRole('option', { name: 'Plumbing' });
936
+ await user.click(plumbingOption);
937
+ // Expect autosave called with categories array no longer containing 'Plumbing'
938
+ expect(autosaveActionSpy).toHaveBeenCalledWith({
939
+ actionId: '_autosave',
940
+ input: expect.objectContaining({ categories: ['Electrical'] }),
941
+ });
942
+ });
943
+ it('should trigger autosave when multi-select is cleared', async () => {
944
+ const user = userEvent.setup();
945
+ const autosaveActionSpy = vi.fn();
946
+ const licenseFormWithMultiSelect = {
947
+ id: 'licenseForm',
948
+ name: 'License Form',
949
+ objectId: 'license',
950
+ actionId: '_update',
951
+ autosaveActionId: '_autosave',
952
+ entries: [
953
+ {
954
+ parameterId: 'name',
955
+ type: 'input',
956
+ display: { label: 'License Number' },
957
+ },
958
+ {
959
+ type: 'inputField',
960
+ input: {
961
+ id: 'categories',
962
+ type: 'array',
963
+ enum: ['Electrical', 'Plumbing', 'Mechanical', 'Other'],
964
+ },
965
+ display: {
966
+ label: 'Categories',
967
+ choicesDisplay: { type: 'dropdown' },
968
+ },
969
+ },
970
+ ],
971
+ };
972
+ server.use(http.get('/api/data/objects/license/instances/test-license', () => HttpResponse.json({
973
+ id: 'test-license',
974
+ name: 'RN-123456',
975
+ categories: ['Electrical', 'Plumbing'],
976
+ })), http.get('/api/data/objects/license/instances/test-license/object', () => HttpResponse.json(licenseObject)), http.get('/api/data/forms/licenseForm', () => HttpResponse.json(licenseFormWithMultiSelect)), http.post('/api/data/objects/license/instances/test-license/actions', async ({ request }) => {
977
+ const body = (await request.json());
978
+ autosaveActionSpy(body);
979
+ return HttpResponse.json({
980
+ id: 'test-license',
981
+ name: 'RN-123456',
982
+ categories: body.input.categories,
983
+ });
984
+ }));
985
+ render(React.createElement(FormRendererContainer, { objectId: 'license', formId: 'licenseForm', dataType: 'objectInstances', actionId: '_update', instanceId: 'test-license' }));
986
+ // Open the multi-select combobox and click the clear button
987
+ const combobox = await screen.findByRole('combobox', { name: 'Categories' });
988
+ await user.click(combobox);
989
+ // Clear button has tabindex="-1" so find via testid
990
+ const clearButton = await screen.findByTestId('autocomplete-clear-button');
991
+ await user.click(clearButton);
992
+ expect(autosaveActionSpy).toHaveBeenCalledWith({
993
+ actionId: '_autosave',
994
+ input: expect.objectContaining({
995
+ categories: expect.arrayContaining([]),
996
+ }),
997
+ });
998
+ });
534
999
  });
535
1000
  it('should display a submit button', async () => {
536
1001
  const form = {
@@ -160,6 +160,12 @@ export const licenseObject = {
160
160
  type: 'string',
161
161
  enum: ['Active', 'Inactive'],
162
162
  },
163
+ {
164
+ id: 'categories',
165
+ name: 'Categories',
166
+ type: 'array',
167
+ enum: ['Electrical', 'Plumbing', 'Mechanical', 'Other'],
168
+ },
163
169
  {
164
170
  id: 'licenseType',
165
171
  name: 'License Type',
@@ -188,6 +194,13 @@ export const licenseObject = {
188
194
  id: 'status',
189
195
  name: 'Status',
190
196
  type: 'string',
197
+ enum: ['Active', 'Inactive'],
198
+ },
199
+ {
200
+ id: 'categories',
201
+ name: 'Categories',
202
+ type: 'array',
203
+ enum: ['Electrical', 'Plumbing', 'Mechanical', 'Other'],
191
204
  },
192
205
  {
193
206
  id: 'address.line1',
@@ -236,6 +249,13 @@ export const licenseObject = {
236
249
  id: 'status',
237
250
  name: 'Status',
238
251
  type: 'string',
252
+ enum: ['Active', 'Inactive'],
253
+ },
254
+ {
255
+ id: 'categories',
256
+ name: 'Categories',
257
+ type: 'array',
258
+ enum: ['Electrical', 'Plumbing', 'Mechanical', 'Other'],
239
259
  },
240
260
  {
241
261
  id: 'address.line1',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@evoke-platform/ui-components",
3
- "version": "1.10.1-dev.1",
3
+ "version": "1.10.1-dev.2",
4
4
  "description": "",
5
5
  "main": "dist/published/index.js",
6
6
  "module": "dist/published/index.js",