@arquimedes.co/eureka-forms 3.0.63 → 3.0.64-entity-step-filter

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.
@@ -158,6 +158,14 @@ export const useSetupApp = (isEmbedded, props) => {
158
158
  idCurrentAgent,
159
159
  handleConfirmed,
160
160
  ]);
161
+ // The host may resolve the classifiers asynchronously (the admin fetches them while the form is
162
+ // already mounted). `loadData` only copies the prop it saw when it ran, so patch the form whenever a
163
+ // new dictionary arrives — without re-running the load, which would reset what the user typed.
164
+ useEffect(() => {
165
+ if (!classifiers || !form || form.classifiers === classifiers)
166
+ return;
167
+ setForm({ ...form, classifiers });
168
+ }, [classifiers, form]);
161
169
  const reload = useCallback(() => {
162
170
  if (!form)
163
171
  return;
@@ -81,6 +81,14 @@ describe('AppHooks', function () {
81
81
  expect(result.current.organization).toBeNull();
82
82
  expect(result.current.branding).toBeUndefined();
83
83
  });
84
+ test('picks up classifiers that arrive after the first render', async () => {
85
+ const classifier = { _id: 'tipo', name: 'Tipo de solicitud', children: [] };
86
+ const { result, rerender } = renderHookWithProviders((props) => useSetupApp(false, { formData: BaseForm, internal: true, classifiers: props.classifiers }), { initialProps: { classifiers: {} } });
87
+ await waitFor(() => expect(result.current.form).toBeTruthy());
88
+ expect(result.current.form?.classifiers).toEqual({});
89
+ rerender({ classifiers: { tipo: classifier } });
90
+ await waitFor(() => expect(result.current.form?.classifiers?.tipo).toEqual(classifier));
91
+ });
84
92
  test('Apikey Default', async () => {
85
93
  mocks.get.mockImplementation((a) => {
86
94
  if (a === '/organization') {
@@ -0,0 +1 @@
1
+ import '@testing-library/jest-dom';
@@ -0,0 +1,70 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { render, screen } from '@testing-library/react';
3
+ import userEvent from '@testing-library/user-event';
4
+ import { describe, expect, it } from 'vitest';
5
+ import '@testing-library/jest-dom';
6
+ import { Provider } from 'react-redux';
7
+ import { FormProvider, useForm } from 'react-hook-form';
8
+ import { configureStore } from '@reduxjs/toolkit';
9
+ import { EurekaFormsReducer, defaultRootState } from '../../Utils/store';
10
+ import { RootApi } from '../../Utils/_api';
11
+ import FormStepTypes from '../../constants/FormStepTypes';
12
+ import { StoreContext } from '../../Utils/StoreContext';
13
+ import FormContext, { IdFormContext } from '../../Contexts/FormContext';
14
+ import SectionContext from '../../Contexts/SectionContext';
15
+ import MaterialProviders from '../../Utils/MaterialProviders';
16
+ import InternalFormStyle from '../../constants/InternalFormStyle';
17
+ import ClassifierSelectorStep from './ClassifierSelectorStep';
18
+ const ID_FORM = 'LATE_CLASSIFIERS_FORM';
19
+ const step = {
20
+ id: 'tipo-step',
21
+ idSection: 'SECTION_1',
22
+ stepPath: ['tipo-step'],
23
+ type: FormStepTypes.CLASSIFIER_SELECTOR,
24
+ idClassifier: 'tipo',
25
+ label: 'Tipo de solicitud',
26
+ description: '',
27
+ searchable: true,
28
+ options: {},
29
+ required: false,
30
+ size: 2,
31
+ };
32
+ /** The project's classifiers, as the host resolves them (asynchronously) and passes them to the form. */
33
+ const classifiers = {
34
+ tipo: { _id: 'tipo', name: 'Tipo de solicitud', children: ['bug', 'mejora'] },
35
+ bug: { _id: 'bug', name: 'Error / Bug', children: [] },
36
+ mejora: { _id: 'mejora', name: 'Propuesta de mejora', children: [] },
37
+ };
38
+ const form = {
39
+ firstSection: 'SECTION_1',
40
+ sections: {
41
+ SECTION_1: { id: 'SECTION_1', name: 'Sección 1', steps: ['tipo-step'], nextSection: null },
42
+ },
43
+ steps: { 'tipo-step': step },
44
+ confirmationMessage: { blocks: [], entityMap: {} },
45
+ showLink: false,
46
+ size: { blockSize: 200, blockNum: 4, spacingSize: 10 },
47
+ };
48
+ function makeStore() {
49
+ return configureStore({
50
+ reducer: { forms: EurekaFormsReducer, [RootApi.reducerPath]: RootApi.reducer },
51
+ preloadedState: { forms: { [ID_FORM]: { ...defaultRootState } } },
52
+ middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(RootApi.middleware),
53
+ });
54
+ }
55
+ function Harness({ store, formClassifiers, }) {
56
+ const methods = useForm({ mode: 'onTouched' });
57
+ return (_jsx(MaterialProviders, { formStyle: InternalFormStyle, children: _jsx(Provider, { store: store, context: StoreContext, children: _jsx(IdFormContext.Provider, { value: ID_FORM, children: _jsx(FormContext.Provider, { value: { ...form, classifiers: formClassifiers }, children: _jsx(SectionContext.Provider, { value: "SECTION_1", children: _jsx(FormProvider, { ...methods, children: _jsx(ClassifierSelectorStep, { step: step, editable: true }) }) }) }) }) }) }));
58
+ }
59
+ describe('ClassifierSelectorStep with classifiers that arrive after the first render', () => {
60
+ it('renders nothing while the classifiers are unknown, then the selector with its options', async () => {
61
+ const user = userEvent.setup();
62
+ const store = makeStore();
63
+ const { rerender } = render(_jsx(Harness, { store: store, formClassifiers: undefined }));
64
+ expect(screen.queryByRole('combobox')).not.toBeInTheDocument();
65
+ rerender(_jsx(Harness, { store: store, formClassifiers: classifiers }));
66
+ await user.click(await screen.findByRole('combobox'));
67
+ expect(await screen.findByRole('option', { name: 'Error / Bug' })).toBeInTheDocument();
68
+ expect(screen.getByRole('option', { name: 'Propuesta de mejora' })).toBeInTheDocument();
69
+ });
70
+ });
@@ -50,8 +50,10 @@ function ClassifierSelectorStep({ step, editable }) {
50
50
  };
51
51
  }) ?? []);
52
52
  },
53
+ // The classifiers dictionary can arrive after the first render (see useSetupApp), so the
54
+ // options must follow it, not only the dependency values.
53
55
  // eslint-disable-next-line react-hooks/exhaustive-deps
54
- [dependenciesValues]);
56
+ [dependenciesValues, classifier, form.classifiers]);
55
57
  useEffect(() => {
56
58
  if (value && !options?.find((option) => option.value === value?.value)) {
57
59
  onChange('');
@@ -13,6 +13,7 @@ import { evaluateCondition } from '../../StepFunctions';
13
13
  import StepComponent from '../../Step';
14
14
  import ErkValueTypes from '../../../constants/ErkValueTypes';
15
15
  import { IntegrationsApi } from '../../../Services/IntegrationService';
16
+ import { entityValueDependencyValue } from '../entityValueDependencyValue';
16
17
  function EntityValuePickerStep({ step, editable }) {
17
18
  const form = useContext(FormContext);
18
19
  const subscribe = useApiSubscribe();
@@ -173,12 +174,9 @@ const getEntityValueOptions = async (step, dependencyStore, { idOrganization, id
173
174
  break;
174
175
  }
175
176
  case EntityValueDataTypes.STEP: {
176
- const currentValue = dependencyStore[filter.idStep]?.value;
177
- if (currentValue) {
178
- if (typeof currentValue === 'string')
179
- params.set(filter.idProperty, currentValue);
180
- else
181
- params.set(filter.idProperty, currentValue._id ?? currentValue.id);
177
+ const currentValue = entityValueDependencyValue(dependencyStore[filter.idStep]);
178
+ if (currentValue !== undefined) {
179
+ params.set(filter.idProperty, currentValue);
182
180
  }
183
181
  else if (filter.required) {
184
182
  return null;
@@ -0,0 +1,12 @@
1
+ import { StepDependency } from '../../Form/Form';
2
+ /**
3
+ * Serializes the current value of a dependency step into the string an entity-picker STEP filter
4
+ * sends as `values.<idProperty>` query param.
5
+ *
6
+ * Entity values (and any id-bearing object) resolve to their id. The answer of a Selector or
7
+ * Classifier step is a `{ label, value }` option: a Selector keeps `value === label`, so its `value`
8
+ * is the text the entity property stores; a Classifier's `value` is the classifier id, which no
9
+ * entity property holds, so it resolves to its `label`. Returns undefined when the dependency has
10
+ * no usable value, so the caller treats the filter as unresolved.
11
+ */
12
+ export declare function entityValueDependencyValue(dependency: StepDependency | undefined): string | undefined;
@@ -0,0 +1,35 @@
1
+ import FormStepTypes from '../../constants/FormStepTypes';
2
+ /**
3
+ * Serializes the current value of a dependency step into the string an entity-picker STEP filter
4
+ * sends as `values.<idProperty>` query param.
5
+ *
6
+ * Entity values (and any id-bearing object) resolve to their id. The answer of a Selector or
7
+ * Classifier step is a `{ label, value }` option: a Selector keeps `value === label`, so its `value`
8
+ * is the text the entity property stores; a Classifier's `value` is the classifier id, which no
9
+ * entity property holds, so it resolves to its `label`. Returns undefined when the dependency has
10
+ * no usable value, so the caller treats the filter as unresolved.
11
+ */
12
+ export function entityValueDependencyValue(dependency) {
13
+ const currentValue = dependency?.value;
14
+ if (currentValue === null || currentValue === undefined || currentValue === '')
15
+ return undefined;
16
+ if (typeof currentValue === 'string')
17
+ return currentValue;
18
+ if (typeof currentValue === 'number' || typeof currentValue === 'boolean')
19
+ return String(currentValue);
20
+ if (currentValue instanceof Date)
21
+ return currentValue.toISOString();
22
+ if (typeof currentValue !== 'object')
23
+ return undefined;
24
+ const record = currentValue;
25
+ // A Selector's `value` is the stored text; every other step's `value` may be an id, so its `label` wins.
26
+ const keys = dependency?.type === FormStepTypes.SELECTOR ? ['_id', 'id', 'value', 'label'] : ['_id', 'id', 'label', 'value'];
27
+ for (const key of keys) {
28
+ const candidate = record[key];
29
+ if (typeof candidate === 'string' && candidate !== '')
30
+ return candidate;
31
+ if (typeof candidate === 'number')
32
+ return String(candidate);
33
+ }
34
+ return undefined;
35
+ }
@@ -0,0 +1,31 @@
1
+ import { describe, expect, test } from 'vitest';
2
+ import FormStepTypes from '../../constants/FormStepTypes';
3
+ import { entityValueDependencyValue } from './entityValueDependencyValue';
4
+ const dep = (type, value) => ({
5
+ idOriginal: 'dep',
6
+ dependents: [],
7
+ type,
8
+ value,
9
+ });
10
+ describe('entityValueDependencyValue', () => {
11
+ test('an unanswered dependency is unresolved', () => {
12
+ expect(entityValueDependencyValue(undefined)).toBeUndefined();
13
+ expect(entityValueDependencyValue(dep(FormStepTypes.SELECTOR, null))).toBeUndefined();
14
+ expect(entityValueDependencyValue(dep(FormStepTypes.SELECTOR, ''))).toBeUndefined();
15
+ expect(entityValueDependencyValue(dep(FormStepTypes.SELECTOR, {}))).toBeUndefined();
16
+ });
17
+ test('plain answers are sent as text', () => {
18
+ expect(entityValueDependencyValue(dep(FormStepTypes.TEXTINPUT, 'Torre 3'))).toBe('Torre 3');
19
+ expect(entityValueDependencyValue(dep(FormStepTypes.TEXTINPUT, 7))).toBe('7');
20
+ expect(entityValueDependencyValue(dep(FormStepTypes.DATEPICKER, new Date('2026-01-02T00:00:00.000Z')))).toBe('2026-01-02T00:00:00.000Z');
21
+ });
22
+ test('an entity value resolves to its id', () => {
23
+ expect(entityValueDependencyValue(dep(FormStepTypes.ENTITYVALUEPICKER, { _id: 'ev-1', label: 'Morros' }))).toBe('ev-1');
24
+ expect(entityValueDependencyValue(dep(FormStepTypes.API_SELECTOR, { id: 'api-1', label: 'X' }))).toBe('api-1');
25
+ });
26
+ test('a selector option resolves to its value, a classifier option to its label', () => {
27
+ expect(entityValueDependencyValue(dep(FormStepTypes.SELECTOR, { label: 'Piso 2', value: 'Piso 2' }))).toBe('Piso 2');
28
+ expect(entityValueDependencyValue(dep(FormStepTypes.SELECTOR, { label: 'Dos', value: '2' }))).toBe('2');
29
+ expect(entityValueDependencyValue(dep(FormStepTypes.CLASSIFIER_SELECTOR, { label: 'Piso 2', value: '65f0c1a2b3c4d5e6f7a8b9c0' }))).toBe('Piso 2');
30
+ });
31
+ });
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@arquimedes.co/eureka-forms",
3
3
  "repository": "git://github.com/Arquimede5/Eureka-Forms.git",
4
- "version": "3.0.63",
4
+ "version":"3.0.64-entity-step-filter",
5
5
  "scripts": {
6
6
  "watch": "node node_modules/@typescript/native/bin/tsc --noEmit --watch --project tsconfig.app.json",
7
7
  "start": "vite",