From 1d4c675f1a6d1df9b06118cef67bdf8e052a32fa Mon Sep 17 00:00:00 2001 From: vitPinchuk Date: Wed, 6 Nov 2024 11:00:53 +0300 Subject: [PATCH 01/18] add section and remove section functions added --- src/components/FormPanel/FormPanel.tsx | 53 +++++++++++++++++++++++++- src/utils/code-parameters.ts | 16 ++++++++ 2 files changed, 67 insertions(+), 2 deletions(-) diff --git a/src/components/FormPanel/FormPanel.tsx b/src/components/FormPanel/FormPanel.tsx index d350ba53..99f65654 100644 --- a/src/components/FormPanel/FormPanel.tsx +++ b/src/components/FormPanel/FormPanel.tsx @@ -25,6 +25,7 @@ import { useDashboardRefresh, useDatasourceRequest } from '@volkovlabs/component import { CustomButtonsRow } from 'components/CustomButtonsRow'; import { isEqual } from 'lodash'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import { v4 as uuidv4 } from 'uuid'; import { ConfirmationElementDisplayMode, @@ -128,6 +129,46 @@ export const FormPanel: React.FC = ({ [onOptionsChange, options] ); + /** + * Add new section + */ + const addSection = useCallback( + (name: string) => { + const newSections = [ + ...options.layout.sections, + { + id: uuidv4(), + name: name, + }, + ]; + onOptionsChange({ + ...options, + layout: { + ...options.layout, + sections: newSections, + }, + }); + }, + [onOptionsChange, options] + ); + + /** + * Remove Section + */ + const removeSection = useCallback( + (id: string) => { + const newSections = options.layout.sections.filter((section) => section.id !== id); + onOptionsChange({ + ...options, + layout: { + ...options.layout, + sections: newSections, + }, + }); + }, + [onOptionsChange, options] + ); + /** * Form Elements */ @@ -287,6 +328,8 @@ export const FormPanel: React.FC = ({ response, enableSubmit: () => setSubmitEnabled(true), disableSubmit: () => setSubmitEnabled(false), + addSection: (name: string) => addSection(name), + removeSection: (id: string) => removeSection(id), collapseSection: (id: string) => onChangeSectionExpandedState(id, false), expandSection: (id: string) => onChangeSectionExpandedState(id, true), toggleSection: (id: string) => onChangeSectionExpandedState(id, !sectionsExpandedState[id]), @@ -312,7 +355,6 @@ export const FormPanel: React.FC = ({ notifyWarning, eventBus, appEvents, - refreshDashboard, options, data, onOptionsChange, @@ -323,6 +365,9 @@ export const FormPanel: React.FC = ({ getFormValue, setInitial, sectionsExpandedState, + refreshDashboard, + addSection, + removeSection, onChangeSectionExpandedState, ] ); @@ -872,6 +917,8 @@ export const FormPanel: React.FC = ({ collapseSection: (id: string) => onChangeSectionExpandedState(id, false), expandSection: (id: string) => onChangeSectionExpandedState(id, true), toggleSection: (id: string) => onChangeSectionExpandedState(id, !sectionsExpandedState[id]), + addSection: (name: string) => addSection(name), + removeSection: (id: string) => removeSection(id), sectionsExpandedState, initial: initialRef.current, setError, @@ -898,7 +945,6 @@ export const FormPanel: React.FC = ({ notifyWarning, eventBus, appEvents, - refreshDashboard, data, onOptionsChange, onChangeElements, @@ -908,7 +954,10 @@ export const FormPanel: React.FC = ({ sectionsExpandedState, initialRef, initialRequest, + refreshDashboard, onChangeSectionExpandedState, + addSection, + removeSection, ] ); diff --git a/src/utils/code-parameters.ts b/src/utils/code-parameters.ts index 9d6896cd..660f683e 100644 --- a/src/utils/code-parameters.ts +++ b/src/utils/code-parameters.ts @@ -65,6 +65,14 @@ export const requestCodeParameters = new CodeParametersBuilder({ CodeEditorSuggestionItemKind.Method ), sectionsExpandedState: new CodeParameterItem>('Sections Expanded State'), + addSection: new CodeParameterItem<(name: string) => void>( + 'Add new section', + CodeEditorSuggestionItemKind.Method + ), + removeSection: new CodeParameterItem<(id: string) => void>( + 'Add new section', + CodeEditorSuggestionItemKind.Method + ), }, }, grafana: { @@ -172,6 +180,14 @@ export const elementValueChangedCodeParameters = new CodeParametersBuilder({ CodeEditorSuggestionItemKind.Method ), sectionsExpandedState: new CodeParameterItem>('Sections Expanded State'), + addSection: new CodeParameterItem<(name: string) => void>( + 'Add new section', + CodeEditorSuggestionItemKind.Method + ), + removeSection: new CodeParameterItem<(id: string) => void>( + 'Add new section', + CodeEditorSuggestionItemKind.Method + ), }, }, grafana: { From cbfd6b4fa47f8f4a65f8355c77c8eea17186c93a Mon Sep 17 00:00:00 2001 From: vitPinchuk Date: Wed, 6 Nov 2024 11:06:28 +0300 Subject: [PATCH 02/18] remove copy test --- src/components/FormPanel/FormPanel.test.tsx | 33 --------------------- 1 file changed, 33 deletions(-) diff --git a/src/components/FormPanel/FormPanel.test.tsx b/src/components/FormPanel/FormPanel.test.tsx index c910c58a..de956c8b 100644 --- a/src/components/FormPanel/FormPanel.test.tsx +++ b/src/components/FormPanel/FormPanel.test.tsx @@ -1285,39 +1285,6 @@ describe('Panel', () => { expect(fetch).toHaveBeenCalledTimes(2); }); - it('Should make initial request once', async () => { - let fetchCalledOptions: any = {}; - jest.mocked(fetch).mockImplementationOnce((url, options) => { - fetchCalledOptions = options; - return Promise.resolve({ - json: Promise.resolve({}), - } as any); - }); - - /** - * Render - */ - await act(async () => - render( - getComponent({ - props: {}, - }) - ) - ); - - /** - * Check if fetch is called - */ - expect(fetch).toHaveBeenCalledTimes(1); - expect(fetch).toHaveBeenCalledWith( - 'some-url', - expect.objectContaining({ - method: RequestMethod.POST, - }) - ); - expect(fetchCalledOptions.headers.get('customHeader')).toEqual('123'); - }); - it('Should enable submit from code', async () => { /** * Render From 507712d2a9ea2f0ccdf784d304b952dca4bbd907 Mon Sep 17 00:00:00 2001 From: vitPinchuk Date: Wed, 6 Nov 2024 11:29:37 +0300 Subject: [PATCH 03/18] test cases added --- src/components/FormPanel/FormPanel.test.tsx | 127 ++++++++++++++++++++ src/components/FormPanel/FormPanel.tsx | 21 ++-- 2 files changed, 139 insertions(+), 9 deletions(-) diff --git a/src/components/FormPanel/FormPanel.test.tsx b/src/components/FormPanel/FormPanel.test.tsx index de956c8b..6b92b44d 100644 --- a/src/components/FormPanel/FormPanel.test.tsx +++ b/src/components/FormPanel/FormPanel.test.tsx @@ -2042,6 +2042,133 @@ describe('Panel', () => { expect(elementsSelectors.fieldNumber()).toHaveValue(15); }); + it('Should execute code on initial request with addSection correctly', async () => { + /** + * Render + */ + const replaceVariables = jest.fn((code) => code); + const onOptionsChange = jest.fn(); + + await act(async () => + render( + getComponent({ + props: { + replaceVariables, + onOptionsChange, + }, + options: { + elements: [ + { id: 'test', type: FormElementType.STRING, value: '111' }, + { id: 'test2', type: FormElementType.NUMBER, value: 10 }, + ], + initial: { + method: RequestMethod.NONE, + code: `context.panel.addSection('test');`, + }, + }, + }) + ) + ); + expect(replaceVariables).toHaveBeenCalledWith(`context.panel.addSection('test');`); + expect(onOptionsChange).toHaveBeenCalledWith( + expect.objectContaining({ + layout: expect.objectContaining({ + sections: expect.arrayContaining([ + expect.objectContaining({ + name: 'test', + }), + ]), + }), + }) + ); + }); + + it('Should execute code on initial request with removeSection correctly', async () => { + /** + * Render + */ + const replaceVariables = jest.fn((code) => code); + const onOptionsChange = jest.fn(); + + await act(async () => + render( + getComponent({ + props: { + replaceVariables, + onOptionsChange, + }, + options: { + elements: [ + { id: 'test', type: FormElementType.STRING, value: '111' }, + { id: 'test2', type: FormElementType.NUMBER, value: 10 }, + ], + layout: { + sections: [ + { + name: 'test', + id: 'id1', + }, + { + name: 'test2', + id: 'id2', + }, + ], + }, + initial: { + method: RequestMethod.NONE, + code: `context.panel.removeSection('id2');`, + }, + }, + }) + ) + ); + + expect(replaceVariables).toHaveBeenCalledWith(`context.panel.removeSection('id2');`); + expect(onOptionsChange).toHaveBeenCalledWith( + expect.objectContaining({ + layout: expect.objectContaining({ + sections: expect.arrayContaining([ + { + name: 'test', + id: 'id1', + }, + ]), + }), + }) + ); + }); + + it('Should nat call change options with removeSections if sections not specified', async () => { + /** + * Render + */ + const replaceVariables = jest.fn((code) => code); + const onOptionsChange = jest.fn(); + + await act(async () => + render( + getComponent({ + props: { + replaceVariables, + onOptionsChange, + }, + options: { + elements: [ + { id: 'test', type: FormElementType.STRING, value: '111' }, + { id: 'test2', type: FormElementType.NUMBER, value: 10 }, + ], + initial: { + method: RequestMethod.NONE, + code: `context.panel.removeSection('test');`, + }, + }, + }) + ) + ); + expect(replaceVariables).toHaveBeenCalledWith(`context.panel.removeSection('test');`); + expect(onOptionsChange).not.toHaveBeenCalled(); + }); + it('Should execute code on initial request with setFormValue correctly', async () => { /** * Render diff --git a/src/components/FormPanel/FormPanel.tsx b/src/components/FormPanel/FormPanel.tsx index 99f65654..18d3d0ac 100644 --- a/src/components/FormPanel/FormPanel.tsx +++ b/src/components/FormPanel/FormPanel.tsx @@ -134,8 +134,9 @@ export const FormPanel: React.FC = ({ */ const addSection = useCallback( (name: string) => { + const sections = options.layout.sections || []; const newSections = [ - ...options.layout.sections, + ...sections, { id: uuidv4(), name: name, @@ -157,14 +158,16 @@ export const FormPanel: React.FC = ({ */ const removeSection = useCallback( (id: string) => { - const newSections = options.layout.sections.filter((section) => section.id !== id); - onOptionsChange({ - ...options, - layout: { - ...options.layout, - sections: newSections, - }, - }); + if (options.layout.sections && options.layout.sections.length) { + const newSections = options.layout.sections.filter((section) => section.id !== id); + onOptionsChange({ + ...options, + layout: { + ...options.layout, + sections: newSections, + }, + }); + } }, [onOptionsChange, options] ); From 8bde67aac1fed3f7e5edf47ad37f3fec180577df Mon Sep 17 00:00:00 2001 From: vitPinchuk Date: Wed, 6 Nov 2024 12:24:44 +0300 Subject: [PATCH 04/18] test cases for element value change code --- src/components/FormPanel/FormPanel.test.tsx | 94 +++++++++++++++++++++ 1 file changed, 94 insertions(+) diff --git a/src/components/FormPanel/FormPanel.test.tsx b/src/components/FormPanel/FormPanel.test.tsx index 6b92b44d..bef43157 100644 --- a/src/components/FormPanel/FormPanel.test.tsx +++ b/src/components/FormPanel/FormPanel.test.tsx @@ -3351,6 +3351,100 @@ describe('Panel', () => { expect(result.onRefresh).toHaveBeenCalledTimes(1); }); + it('Should execute code on element change with removeSection correctly', async () => { + /** + * Render + */ + const onOptionsChange = jest.fn(); + + await act(async () => + render( + getComponent({ + props: { + onOptionsChange, + }, + options: { + elements: [ + { + ...element, + value: '3', + }, + ], + layout: { + sections: [ + { + name: 'test', + id: 'id1', + }, + ], + }, + elementValueChanged: ` + context.panel.removeSection('id1') + `, + }, + }) + ) + ); + + /** + * Change value + */ + await act(async () => fireEvent.change(elementsSelectors.fieldString(), { target: { value: '11' } })); + + expect(onOptionsChange).toHaveBeenCalledWith( + expect.objectContaining({ + layout: expect.objectContaining({ + sections: expect.arrayContaining([]), + }), + }) + ); + }); + + it('Should execute code on element change with addSection correctly', async () => { + /** + * Render + */ + const onOptionsChange = jest.fn(); + + await act(async () => + render( + getComponent({ + props: { + onOptionsChange, + }, + options: { + elements: [ + { + ...element, + value: '1', + }, + ], + elementValueChanged: ` + context.panel.addSection('test15'); + `, + }, + }) + ) + ); + + /** + * Change value + */ + await act(async () => fireEvent.change(elementsSelectors.fieldString(), { target: { value: '11' } })); + + expect(onOptionsChange).toHaveBeenCalledWith( + expect.objectContaining({ + layout: expect.objectContaining({ + sections: expect.arrayContaining([ + expect.objectContaining({ + name: 'test15', + }), + ]), + }), + }) + ); + }); + it('Should allow to refresh dashboard', async () => { await act(async () => render( From 7db54662d45255b9d7b0f5c0edbe67405eb89c29 Mon Sep 17 00:00:00 2001 From: vitPinchuk Date: Thu, 7 Nov 2024 17:59:10 +0300 Subject: [PATCH 05/18] add sections update --- src/components/FormPanel/FormPanel.test.tsx | 7 ++++--- src/components/FormPanel/FormPanel.tsx | 23 +++++++-------------- src/utils/code-parameters.ts | 8 +++---- 3 files changed, 15 insertions(+), 23 deletions(-) diff --git a/src/components/FormPanel/FormPanel.test.tsx b/src/components/FormPanel/FormPanel.test.tsx index bef43157..18e0e959 100644 --- a/src/components/FormPanel/FormPanel.test.tsx +++ b/src/components/FormPanel/FormPanel.test.tsx @@ -2063,19 +2063,20 @@ describe('Panel', () => { ], initial: { method: RequestMethod.NONE, - code: `context.panel.addSection('test');`, + code: `context.panel.addSections([{name:'test', id:'test-id'}]);`, }, }, }) ) ); - expect(replaceVariables).toHaveBeenCalledWith(`context.panel.addSection('test');`); + expect(replaceVariables).toHaveBeenCalledWith(`context.panel.addSections([{name:'test', id:'test-id'}]);`); expect(onOptionsChange).toHaveBeenCalledWith( expect.objectContaining({ layout: expect.objectContaining({ sections: expect.arrayContaining([ expect.objectContaining({ name: 'test', + id: 'test-id', }), ]), }), @@ -3420,7 +3421,7 @@ describe('Panel', () => { }, ], elementValueChanged: ` - context.panel.addSection('test15'); + context.panel.addSections([{name:'test15', id:'test-id'}]); `, }, }) diff --git a/src/components/FormPanel/FormPanel.tsx b/src/components/FormPanel/FormPanel.tsx index 18d3d0ac..15c7f445 100644 --- a/src/components/FormPanel/FormPanel.tsx +++ b/src/components/FormPanel/FormPanel.tsx @@ -25,7 +25,6 @@ import { useDashboardRefresh, useDatasourceRequest } from '@volkovlabs/component import { CustomButtonsRow } from 'components/CustomButtonsRow'; import { isEqual } from 'lodash'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; -import { v4 as uuidv4 } from 'uuid'; import { ConfirmationElementDisplayMode, @@ -132,21 +131,13 @@ export const FormPanel: React.FC = ({ /** * Add new section */ - const addSection = useCallback( - (name: string) => { - const sections = options.layout.sections || []; - const newSections = [ - ...sections, - { - id: uuidv4(), - name: name, - }, - ]; + const addSections = useCallback( + (sections: Array<{ name: string; id: string }>) => { onOptionsChange({ ...options, layout: { ...options.layout, - sections: newSections, + sections: sections, }, }); }, @@ -331,7 +322,7 @@ export const FormPanel: React.FC = ({ response, enableSubmit: () => setSubmitEnabled(true), disableSubmit: () => setSubmitEnabled(false), - addSection: (name: string) => addSection(name), + addSections, removeSection: (id: string) => removeSection(id), collapseSection: (id: string) => onChangeSectionExpandedState(id, false), expandSection: (id: string) => onChangeSectionExpandedState(id, true), @@ -369,7 +360,7 @@ export const FormPanel: React.FC = ({ setInitial, sectionsExpandedState, refreshDashboard, - addSection, + addSections, removeSection, onChangeSectionExpandedState, ] @@ -920,7 +911,7 @@ export const FormPanel: React.FC = ({ collapseSection: (id: string) => onChangeSectionExpandedState(id, false), expandSection: (id: string) => onChangeSectionExpandedState(id, true), toggleSection: (id: string) => onChangeSectionExpandedState(id, !sectionsExpandedState[id]), - addSection: (name: string) => addSection(name), + addSections, removeSection: (id: string) => removeSection(id), sectionsExpandedState, initial: initialRef.current, @@ -959,7 +950,7 @@ export const FormPanel: React.FC = ({ initialRequest, refreshDashboard, onChangeSectionExpandedState, - addSection, + addSections, removeSection, ] ); diff --git a/src/utils/code-parameters.ts b/src/utils/code-parameters.ts index 660f683e..8f042209 100644 --- a/src/utils/code-parameters.ts +++ b/src/utils/code-parameters.ts @@ -65,8 +65,8 @@ export const requestCodeParameters = new CodeParametersBuilder({ CodeEditorSuggestionItemKind.Method ), sectionsExpandedState: new CodeParameterItem>('Sections Expanded State'), - addSection: new CodeParameterItem<(name: string) => void>( - 'Add new section', + addSections: new CodeParameterItem<(sections: Array<{ name: string; id: string }>) => void>( + 'Add new sections', CodeEditorSuggestionItemKind.Method ), removeSection: new CodeParameterItem<(id: string) => void>( @@ -180,8 +180,8 @@ export const elementValueChangedCodeParameters = new CodeParametersBuilder({ CodeEditorSuggestionItemKind.Method ), sectionsExpandedState: new CodeParameterItem>('Sections Expanded State'), - addSection: new CodeParameterItem<(name: string) => void>( - 'Add new section', + addSections: new CodeParameterItem<(sections: Array<{ name: string; id: string }>) => void>( + 'Add new sections', CodeEditorSuggestionItemKind.Method ), removeSection: new CodeParameterItem<(id: string) => void>( From 39fb52958b0d0d7336bcf76b709d62818b089e62 Mon Sep 17 00:00:00 2001 From: vitPinchuk Date: Thu, 7 Nov 2024 17:59:42 +0300 Subject: [PATCH 06/18] update useFormElements --- .../ElementSections/ElementSections.test.tsx | 6 + .../ElementSections/ElementSections.tsx | 12 +- .../FormElementsEditor.test.tsx | 2 +- .../FormElementsEditor/FormElementsEditor.tsx | 2 +- src/components/FormPanel/FormPanel.test.tsx | 222 ------------------ src/components/FormPanel/FormPanel.tsx | 52 ++-- .../InitialFieldsEditor.tsx | 2 +- .../LayoutSectionsEditor.test.tsx | 60 +---- .../LayoutSectionsEditor.tsx | 21 +- src/hooks/useFormElements.ts | 135 ++++++++++- src/utils/code-parameters.ts | 26 +- 11 files changed, 213 insertions(+), 327 deletions(-) diff --git a/src/components/ElementSections/ElementSections.test.tsx b/src/components/ElementSections/ElementSections.test.tsx index 851bf855..e6150086 100644 --- a/src/components/ElementSections/ElementSections.test.tsx +++ b/src/components/ElementSections/ElementSections.test.tsx @@ -48,6 +48,10 @@ describe('Form Elements', () => { }; describe('Render elements', () => { + const sections = [ + { name: 'section1', id: 'section1', expanded: true }, + { name: 'section2', id: 'section2', expanded: true }, + ]; const options: PanelOptions = { sync: true, updateEnabled: UpdateEnabledMode.MANUAL, @@ -82,6 +86,7 @@ describe('Form Elements', () => { render( getComponent({ options, + sections, onChangeElement, initial: { changed: 'bye' }, sectionsExpandedState: { section1: true }, @@ -98,6 +103,7 @@ describe('Form Elements', () => { render( getComponent({ options, + sections, onChangeElement, onChangeSectionExpandedState, initial: { changed: 'bye' }, diff --git a/src/components/ElementSections/ElementSections.tsx b/src/components/ElementSections/ElementSections.tsx index c8bf2568..78eebff4 100644 --- a/src/components/ElementSections/ElementSections.tsx +++ b/src/components/ElementSections/ElementSections.tsx @@ -5,7 +5,7 @@ import { CollapsableSection } from '@volkovlabs/components'; import React from 'react'; import { LayoutOrientation, SectionVariant, TEST_IDS } from '../../constants'; -import { ExecuteCustomCodeParams, LocalFormElement, PanelOptions } from '../../types'; +import { ExecuteCustomCodeParams, LayoutSection, LocalFormElement, PanelOptions } from '../../types'; import { FormElements } from '../FormElements'; import { getStyles } from './ElementSections.styles'; @@ -71,6 +71,13 @@ interface Props { * @type {string} */ timeZone: string; + + /** + * Sections + * + * @type {string} + */ + sections: LayoutSection[]; } /** @@ -87,6 +94,7 @@ export const ElementSections: React.FC = ({ onChangeSectionExpandedState, executeCustomCode, timeZone, + sections, }) => { /** * Theme and Styles @@ -100,7 +108,7 @@ export const ElementSections: React.FC = ({ vertical: options.layout.orientation === LayoutOrientation.VERTICAL, })} > - {options.layout?.sections?.map((section, id) => { + {sections?.map((section, id) => { const isOpen = sectionsExpandedState[section.id]; const renderContainer = (children: React.ReactNode) => { diff --git a/src/components/FormElementsEditor/FormElementsEditor.test.tsx b/src/components/FormElementsEditor/FormElementsEditor.test.tsx index b534e3c0..e83bbbf1 100644 --- a/src/components/FormElementsEditor/FormElementsEditor.test.tsx +++ b/src/components/FormElementsEditor/FormElementsEditor.test.tsx @@ -501,7 +501,7 @@ describe('Form Elements Editor', () => { }); /** - * Multi Select + * File */ it('Should find component with File', () => { const elements = [ diff --git a/src/components/FormElementsEditor/FormElementsEditor.tsx b/src/components/FormElementsEditor/FormElementsEditor.tsx index 5e664a64..9f62673e 100644 --- a/src/components/FormElementsEditor/FormElementsEditor.tsx +++ b/src/components/FormElementsEditor/FormElementsEditor.tsx @@ -54,7 +54,7 @@ export const FormElementsEditor: React.FC = ({ value, onChange, context } onChangeElementOption, onElementRemove, } = useFormElements({ - onChange, + onChangeElementsOption: onChange, value, }); diff --git a/src/components/FormPanel/FormPanel.test.tsx b/src/components/FormPanel/FormPanel.test.tsx index 18e0e959..de956c8b 100644 --- a/src/components/FormPanel/FormPanel.test.tsx +++ b/src/components/FormPanel/FormPanel.test.tsx @@ -2042,134 +2042,6 @@ describe('Panel', () => { expect(elementsSelectors.fieldNumber()).toHaveValue(15); }); - it('Should execute code on initial request with addSection correctly', async () => { - /** - * Render - */ - const replaceVariables = jest.fn((code) => code); - const onOptionsChange = jest.fn(); - - await act(async () => - render( - getComponent({ - props: { - replaceVariables, - onOptionsChange, - }, - options: { - elements: [ - { id: 'test', type: FormElementType.STRING, value: '111' }, - { id: 'test2', type: FormElementType.NUMBER, value: 10 }, - ], - initial: { - method: RequestMethod.NONE, - code: `context.panel.addSections([{name:'test', id:'test-id'}]);`, - }, - }, - }) - ) - ); - expect(replaceVariables).toHaveBeenCalledWith(`context.panel.addSections([{name:'test', id:'test-id'}]);`); - expect(onOptionsChange).toHaveBeenCalledWith( - expect.objectContaining({ - layout: expect.objectContaining({ - sections: expect.arrayContaining([ - expect.objectContaining({ - name: 'test', - id: 'test-id', - }), - ]), - }), - }) - ); - }); - - it('Should execute code on initial request with removeSection correctly', async () => { - /** - * Render - */ - const replaceVariables = jest.fn((code) => code); - const onOptionsChange = jest.fn(); - - await act(async () => - render( - getComponent({ - props: { - replaceVariables, - onOptionsChange, - }, - options: { - elements: [ - { id: 'test', type: FormElementType.STRING, value: '111' }, - { id: 'test2', type: FormElementType.NUMBER, value: 10 }, - ], - layout: { - sections: [ - { - name: 'test', - id: 'id1', - }, - { - name: 'test2', - id: 'id2', - }, - ], - }, - initial: { - method: RequestMethod.NONE, - code: `context.panel.removeSection('id2');`, - }, - }, - }) - ) - ); - - expect(replaceVariables).toHaveBeenCalledWith(`context.panel.removeSection('id2');`); - expect(onOptionsChange).toHaveBeenCalledWith( - expect.objectContaining({ - layout: expect.objectContaining({ - sections: expect.arrayContaining([ - { - name: 'test', - id: 'id1', - }, - ]), - }), - }) - ); - }); - - it('Should nat call change options with removeSections if sections not specified', async () => { - /** - * Render - */ - const replaceVariables = jest.fn((code) => code); - const onOptionsChange = jest.fn(); - - await act(async () => - render( - getComponent({ - props: { - replaceVariables, - onOptionsChange, - }, - options: { - elements: [ - { id: 'test', type: FormElementType.STRING, value: '111' }, - { id: 'test2', type: FormElementType.NUMBER, value: 10 }, - ], - initial: { - method: RequestMethod.NONE, - code: `context.panel.removeSection('test');`, - }, - }, - }) - ) - ); - expect(replaceVariables).toHaveBeenCalledWith(`context.panel.removeSection('test');`); - expect(onOptionsChange).not.toHaveBeenCalled(); - }); - it('Should execute code on initial request with setFormValue correctly', async () => { /** * Render @@ -3352,100 +3224,6 @@ describe('Panel', () => { expect(result.onRefresh).toHaveBeenCalledTimes(1); }); - it('Should execute code on element change with removeSection correctly', async () => { - /** - * Render - */ - const onOptionsChange = jest.fn(); - - await act(async () => - render( - getComponent({ - props: { - onOptionsChange, - }, - options: { - elements: [ - { - ...element, - value: '3', - }, - ], - layout: { - sections: [ - { - name: 'test', - id: 'id1', - }, - ], - }, - elementValueChanged: ` - context.panel.removeSection('id1') - `, - }, - }) - ) - ); - - /** - * Change value - */ - await act(async () => fireEvent.change(elementsSelectors.fieldString(), { target: { value: '11' } })); - - expect(onOptionsChange).toHaveBeenCalledWith( - expect.objectContaining({ - layout: expect.objectContaining({ - sections: expect.arrayContaining([]), - }), - }) - ); - }); - - it('Should execute code on element change with addSection correctly', async () => { - /** - * Render - */ - const onOptionsChange = jest.fn(); - - await act(async () => - render( - getComponent({ - props: { - onOptionsChange, - }, - options: { - elements: [ - { - ...element, - value: '1', - }, - ], - elementValueChanged: ` - context.panel.addSections([{name:'test15', id:'test-id'}]); - `, - }, - }) - ) - ); - - /** - * Change value - */ - await act(async () => fireEvent.change(elementsSelectors.fieldString(), { target: { value: '11' } })); - - expect(onOptionsChange).toHaveBeenCalledWith( - expect.objectContaining({ - layout: expect.objectContaining({ - sections: expect.arrayContaining([ - expect.objectContaining({ - name: 'test15', - }), - ]), - }), - }) - ); - }); - it('Should allow to refresh dashboard', async () => { await act(async () => render( diff --git a/src/components/FormPanel/FormPanel.tsx b/src/components/FormPanel/FormPanel.tsx index 15c7f445..13005891 100644 --- a/src/components/FormPanel/FormPanel.tsx +++ b/src/components/FormPanel/FormPanel.tsx @@ -41,6 +41,7 @@ import { useFormElements, useMutableState } from '@/hooks'; import { ButtonVariant, FormElement, + LayoutSection, LocalFormElement, ModalColumnName, PanelOptions, @@ -129,10 +130,10 @@ export const FormPanel: React.FC = ({ ); /** - * Add new section + * Change Sections Options */ - const addSections = useCallback( - (sections: Array<{ name: string; id: string }>) => { + const onChangeSectionsOption = useCallback( + (sections: LayoutSection[]) => { onOptionsChange({ ...options, layout: { @@ -144,25 +145,6 @@ export const FormPanel: React.FC = ({ [onOptionsChange, options] ); - /** - * Remove Section - */ - const removeSection = useCallback( - (id: string) => { - if (options.layout.sections && options.layout.sections.length) { - const newSections = options.layout.sections.filter((section) => section.id !== id); - onOptionsChange({ - ...options, - layout: { - ...options.layout, - sections: newSections, - }, - }); - } - }, - [onOptionsChange, options] - ); - /** * Form Elements */ @@ -178,11 +160,18 @@ export const FormPanel: React.FC = ({ patchFormValue, setFormValue, getFormValue, + addSections, + removeSection, + onChangeSections, + sections, + onChangeLayout, } = useFormElements({ - onChange: onChangeOptions, + onChangeElementsOption: onChangeOptions, value: options.elements, isAutoSave: false, - sections: options.layout.sections, + layoutSections: options.layout.sections, + options, + onChangeSectionsOption: onChangeSectionsOption, }); /** @@ -322,7 +311,10 @@ export const FormPanel: React.FC = ({ response, enableSubmit: () => setSubmitEnabled(true), disableSubmit: () => setSubmitEnabled(false), - addSections, + addSections: (sections: LayoutSection[]) => addSections(sections), + onChangeSections: (sections: LayoutSection[]) => onChangeSections(sections), + onChangeLayout: (elements: LocalFormElement[], sections?: LayoutSection[]) => + onChangeLayout(elements, sections), removeSection: (id: string) => removeSection(id), collapseSection: (id: string) => onChangeSectionExpandedState(id, false), expandSection: (id: string) => onChangeSectionExpandedState(id, true), @@ -361,6 +353,8 @@ export const FormPanel: React.FC = ({ sectionsExpandedState, refreshDashboard, addSections, + onChangeSections, + onChangeLayout, removeSection, onChangeSectionExpandedState, ] @@ -911,7 +905,10 @@ export const FormPanel: React.FC = ({ collapseSection: (id: string) => onChangeSectionExpandedState(id, false), expandSection: (id: string) => onChangeSectionExpandedState(id, true), toggleSection: (id: string) => onChangeSectionExpandedState(id, !sectionsExpandedState[id]), - addSections, + addSections: (sections: LayoutSection[]) => addSections(sections), + onChangeSections: (sections: LayoutSection[]) => onChangeSections(sections), + onChangeLayout: (elements: LocalFormElement[], sections?: LayoutSection[]) => + onChangeLayout(elements, sections), removeSection: (id: string) => removeSection(id), sectionsExpandedState, initial: initialRef.current, @@ -951,6 +948,8 @@ export const FormPanel: React.FC = ({ refreshDashboard, onChangeSectionExpandedState, addSections, + onChangeSections, + onChangeLayout, removeSection, ] ); @@ -1015,6 +1014,7 @@ export const FormPanel: React.FC = ({ data={data} options={options} elements={elements} + sections={sections} onChangeElement={onChangeElement} initial={initial} replaceVariables={replaceVariables} diff --git a/src/components/InitialFieldsEditor/InitialFieldsEditor.tsx b/src/components/InitialFieldsEditor/InitialFieldsEditor.tsx index d57b99cf..c52444a6 100644 --- a/src/components/InitialFieldsEditor/InitialFieldsEditor.tsx +++ b/src/components/InitialFieldsEditor/InitialFieldsEditor.tsx @@ -19,7 +19,7 @@ export const InitialFieldsEditor: React.FC = ({ value, onChange, context * Form Elements State */ const { elements, isChanged, onSaveUpdates, onChangeElement } = useFormElements({ - onChange, + onChangeElementsOption: onChange, value, }); diff --git a/src/components/LayoutSectionsEditor/LayoutSectionsEditor.test.tsx b/src/components/LayoutSectionsEditor/LayoutSectionsEditor.test.tsx index de464c1d..e17690cb 100644 --- a/src/components/LayoutSectionsEditor/LayoutSectionsEditor.test.tsx +++ b/src/components/LayoutSectionsEditor/LayoutSectionsEditor.test.tsx @@ -1,4 +1,4 @@ -import { fireEvent, render, screen, within } from '@testing-library/react'; +import { act, fireEvent, render, screen, within } from '@testing-library/react'; import React from 'react'; import { LayoutOrientation, SectionVariant } from '../../constants'; @@ -72,7 +72,7 @@ describe('Layout Sections Editor', () => { /** * Change id */ - it('Should change id value', () => { + it('Should change id value', async () => { const sections = [ { id: '1', name: 'Section' }, { id: '2', name: '' }, @@ -94,18 +94,9 @@ describe('Layout Sections Editor', () => { * Change section name */ const sectionSelectors = getLayoutSectionsEditorSelectors(within(section)); - fireEvent.change(sectionSelectors.fieldId(), { target: { value: '11' } }); - - /** - * Check if id is changed - */ - expect(onChange).toHaveBeenCalledWith( - expect.arrayContaining([ - expect.objectContaining({ - id: '2', - }), - ]) - ); + expect(sectionSelectors.fieldId()).toHaveValue('1'); + await act(() => fireEvent.change(sectionSelectors.fieldId(), { target: { value: '11' } })); + expect(sectionSelectors.fieldId()).toHaveValue('11'); }); it('Should clean id value', () => { @@ -126,19 +117,11 @@ describe('Layout Sections Editor', () => { /** * Change section name */ + const sectionSelectors = getLayoutSectionsEditorSelectors(within(section)); + expect(sectionSelectors.fieldId()).toHaveValue('1'); fireEvent.change(sectionSelectors.fieldId(), { target: { value: '' } }); - - /** - * Check if id is changed - */ - expect(onChange).toHaveBeenCalledWith( - expect.arrayContaining([ - expect.objectContaining({ - id: '', - }), - ]) - ); + expect(sectionSelectors.fieldId()).toHaveValue(''); }); it('Should not allow use already existing id', () => { @@ -190,18 +173,10 @@ describe('Layout Sections Editor', () => { * Change section name */ const sectionSelectors = getLayoutSectionsEditorSelectors(within(section)); - fireEvent.change(sectionSelectors.fieldName(), { target: { value: 'newName' } }); + expect(sectionSelectors.fieldName()).toHaveValue('Section'); - /** - * Check if name is changed - */ - expect(onChange).toHaveBeenCalledWith( - expect.arrayContaining([ - expect.objectContaining({ - name: 'newName', - }), - ]) - ); + fireEvent.change(sectionSelectors.fieldName(), { target: { value: 'newName' } }); + expect(sectionSelectors.fieldName()).toHaveValue('newName'); }); it('Should change expanded value for collapsable section variable', () => { @@ -232,18 +207,9 @@ describe('Layout Sections Editor', () => { */ const sectionSelectors = getLayoutSectionsEditorSelectors(within(section)); expect(sectionSelectors.fieldExpanded()).toBeInTheDocument(); + expect(sectionSelectors.fieldExpanded()).not.toBeChecked(); fireEvent.click(sectionSelectors.fieldExpanded()); - - /** - * Check if expanded is changed - */ - expect(onChange).toHaveBeenCalledWith( - expect.arrayContaining([ - expect.objectContaining({ - expanded: true, - }), - ]) - ); + expect(sectionSelectors.fieldExpanded()).toBeChecked(); }); it('Should hide expanded field if no collapsable variant', () => { diff --git a/src/components/LayoutSectionsEditor/LayoutSectionsEditor.tsx b/src/components/LayoutSectionsEditor/LayoutSectionsEditor.tsx index bcbf4cf4..701cb4e3 100644 --- a/src/components/LayoutSectionsEditor/LayoutSectionsEditor.tsx +++ b/src/components/LayoutSectionsEditor/LayoutSectionsEditor.tsx @@ -3,6 +3,7 @@ import { Button, Checkbox, InlineField, InlineFieldRow, Input, useStyles2 } from import React, { ChangeEvent, useCallback, useMemo } from 'react'; import { LayoutOrientation, SectionVariant, TEST_IDS } from '../../constants'; +import { useFormElements } from '../../hooks'; import { LayoutSection, PanelOptions } from '../../types'; import { isSectionCollisionExists } from '../../utils'; import { getStyles } from './LayoutSectionsEditor.styles'; @@ -24,21 +25,19 @@ export const LayoutSectionsEditor: React.FC = ({ value, onChange, context /** * Sections */ - const sections = useMemo(() => { - if (Array.isArray(value)) { - return value; - } - return []; - }, [value]); + const { sections, onChangeSections } = useFormElements({ + onChangeSectionsOption: onChange, + layoutSections: value, + }); /** * Change Section */ const onChangeSection = useCallback( (updatedSection: LayoutSection, id = updatedSection.id) => { - onChange(sections.map((section) => (section.id === id ? updatedSection : section))); + onChangeSections(sections.map((section) => (section.id === id ? updatedSection : section))); }, - [onChange, sections] + [onChangeSections, sections] ); /** @@ -46,9 +45,9 @@ export const LayoutSectionsEditor: React.FC = ({ value, onChange, context */ const onRemoveSection = useCallback( (removedSection: LayoutSection) => { - onChange(sections.filter((section) => section.id !== removedSection.id)); + onChangeSections(sections.filter((section) => section.id !== removedSection.id)); }, - [onChange, sections] + [onChangeSections, sections] ); /** @@ -136,7 +135,7 @@ export const LayoutSectionsEditor: React.FC = ({ value, onChange, context