From 54b8303bb80ee72715a81a3cab1794ba6c097f5c Mon Sep 17 00:00:00 2001 From: Arpit Date: Fri, 5 Apr 2024 15:19:05 +0530 Subject: [PATCH 01/11] [wip] appbuilder - performance (#9216) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * performance -init * fixes: child components getting replaced * child components should be able to added to target containers * feat: reduce rerender of Dragcontainer (#9217) * fixes component crash for height incremental * clean up * clean up * clean up * removes debouncing of currentState updater * cleanup * controls update on components' referenced changed * fixes: widget crash if references are found * Reduced the load time by combining the appLoad queries loading state * Debounced initial currentState updates * reduce debounced time for updating current state * clean up * on events updation, referenced component via source id or csa would undergo a new re-render * Optimize tab navigation by asynchronously handling event, reducing UI blockage * off load updating app suggestions until UI renders * off load app load an page load event to low priority tasks list, resulting in faster app load * Added blocking updates to macrotask queue and changed Leftsidebar * Revert "Merge branch 'feat/grid-appbuilder-improvement' into core-performance/appbuilder" This reverts commit 2e8374ac31aad607fa891beee59e63f6a19d7fed, reversing changes made to 23a86bfe6e3df9d28ef406bedf2d03ece971afb8. * fixes: component -extra re-rendering issue on current state changes. * fixes: extra re-render for each compoennt dnd updates * reverting computeComponentState back to autoSave callaback * Fixed showing default children * Reverted commit 9b88e9f which causes infinite loop * Removed debuggerStore & unwanted props * Added profiler hoc * Implement batch processing and selective flushing for efficient state… (#9278) * Implement batch processing and selective flushing for efficient state updates in React components, optimizing performance for large-scale applications. * clean up * fix: fixed issues with delay of rerender (#9291) * fix: fixed issues with delay of rerender * fix: removed unused logs * fix: removed unused logs * fix: removed unused logs * clean up --------- Co-authored-by: arpitnath --------- Co-authored-by: Johnson Cherian * clean up * Fixed the crash on Form component * Optimize reference update tracking by sourcing from direct modifications rather than state diff comparisons. * clean up * fix: update appdefinition to editorstore in viewert (#9297) * removing current state deps from editor and processing re-renders. and use new resolver inside components (#9298) * removing current state deps from editor and processing re-renders. Also use new resolver inside components * perf: update position before state update on dnd (#9301) --------- Co-authored-by: Johnson Cherian --------- Co-authored-by: Johnson Cherian Co-authored-by: Kavin Venkatachalam --- frontend/src/Editor/Box.jsx | 401 +------------- frontend/src/Editor/BoxUI.jsx | 175 ++++++ .../src/Editor/CodeBuilder/CodeBuilder.jsx | 1 - .../Editor/CodeBuilder/Elements/BoxShadow.jsx | 2 +- frontend/src/Editor/CodeEditor/utils.js | 20 +- frontend/src/Editor/Comment/index.jsx | 2 +- frontend/src/Editor/Components/Button.jsx | 3 + frontend/src/Editor/Components/Container.jsx | 1 - frontend/src/Editor/Components/FilePicker.jsx | 29 +- frontend/src/Editor/Components/Form/Form.jsx | 43 +- frontend/src/Editor/Components/Map/Map.jsx | 17 +- .../src/Editor/Components/NumberInput.jsx | 21 +- .../src/Editor/Components/PasswordInput.jsx | 17 +- .../src/Editor/Components/Table/Table.jsx | 13 +- frontend/src/Editor/Components/Tabs.jsx | 48 +- frontend/src/Editor/Components/Text.jsx | 13 +- frontend/src/Editor/Components/TextInput.jsx | 19 +- frontend/src/Editor/ConfigHandle.jsx | 5 +- frontend/src/Editor/Container.jsx | 343 +++++++----- .../Editor/ControlledComponentToRender.jsx | 41 ++ frontend/src/Editor/CustomDragLayer.jsx | 2 +- frontend/src/Editor/DragContainer.jsx | 102 ++-- frontend/src/Editor/DraggableBox.jsx | 106 +--- frontend/src/Editor/Editor.jsx | 271 +++++---- frontend/src/Editor/EditorSelecto.jsx | 44 +- frontend/src/Editor/GhostWidget.jsx | 27 + frontend/src/Editor/Header/GlobalSettings.jsx | 3 +- .../src/Editor/Inspector/Components/Chart.jsx | 7 +- .../Inspector/Components/DefaultComponent.jsx | 3 +- .../src/Editor/Inspector/EventManager.jsx | 34 +- frontend/src/Editor/ItemTypes.js | 5 - .../SidebarDebugger/useDebugger.js | 1 - .../LeftSidebar/SidebarPageSelector/index.jsx | 8 +- frontend/src/Editor/LeftSidebar/index.jsx | 184 +++---- .../HydrateWithResolveReferences.jsx | 106 ++++ frontend/src/Editor/SubContainer.jsx | 382 ++++--------- frontend/src/Editor/SubCustomDragLayer.jsx | 4 +- frontend/src/Editor/Viewer.jsx | 185 ++----- frontend/src/Editor/editorConstants.js | 11 + frontend/src/Editor/gridUtils.js | 49 ++ frontend/src/_helpers/appUtils.js | 519 ++++++++++-------- frontend/src/_helpers/editorHelpers.js | 198 +++++++ frontend/src/_helpers/utils.js | 30 +- frontend/src/_hoc/withProfiler.jsx | 18 + frontend/src/_hooks/useRenderCount.js | 18 + frontend/src/_stores/appDataStore.js | 14 +- frontend/src/_stores/currentStateStore.js | 45 +- frontend/src/_stores/dataQueriesStore.js | 72 +-- frontend/src/_stores/editorStore.js | 34 +- frontend/src/_stores/gridStore.js | 3 + frontend/src/_stores/queryPanelStore.js | 2 +- frontend/src/_stores/resolverStore.js | 60 +- frontend/src/_stores/utils.js | 7 +- frontend/src/index.jsx | 4 +- frontend/src/wdyr.js | 8 + server/src/helpers/import_export.helpers.ts | 53 -- .../src/services/app_import_export.service.ts | 84 +-- 57 files changed, 2017 insertions(+), 1900 deletions(-) create mode 100644 frontend/src/Editor/BoxUI.jsx create mode 100644 frontend/src/Editor/ControlledComponentToRender.jsx create mode 100644 frontend/src/Editor/GhostWidget.jsx delete mode 100644 frontend/src/Editor/ItemTypes.js create mode 100644 frontend/src/Editor/Middlewares/HydrateWithResolveReferences.jsx create mode 100644 frontend/src/Editor/editorConstants.js create mode 100644 frontend/src/_helpers/editorHelpers.js create mode 100644 frontend/src/_hoc/withProfiler.jsx create mode 100644 frontend/src/_hooks/useRenderCount.js create mode 100644 frontend/src/wdyr.js delete mode 100644 server/src/helpers/import_export.helpers.ts diff --git a/frontend/src/Editor/Box.jsx b/frontend/src/Editor/Box.jsx index 9b59f4af00..17a16a643f 100644 --- a/frontend/src/Editor/Box.jsx +++ b/frontend/src/Editor/Box.jsx @@ -1,380 +1,35 @@ -import React, { useEffect, useState, useMemo, useContext, memo } from 'react'; -import { Button } from './Components/Button'; -import { Image } from './Components/Image'; -import { Text } from './Components/Text'; -import { Table } from './Components/Table/Table'; -import { TextInput } from './Components/TextInput'; -import { NumberInput } from './Components/NumberInput'; -import { TextArea } from './Components/TextArea'; -import { Container } from './Components/Container'; -import { Tabs } from './Components/Tabs'; -import { RichTextEditor } from './Components/RichTextEditor'; -import { DropDown } from './Components/DropDown'; -import { Checkbox } from './Components/Checkbox'; -import { Datepicker } from './Components/Datepicker'; -import { DaterangePicker } from './Components/DaterangePicker'; -import { Multiselect } from './Components/Multiselect'; -import { Modal } from './Components/Modal'; -import { Chart } from './Components/Chart'; -import { Map } from './Components/Map/Map'; -import { QrScanner } from './Components/QrScanner/QrScanner'; -import { ToggleSwitch } from './Components/Toggle'; -import { RadioButton } from './Components/RadioButton'; -import { StarRating } from './Components/StarRating'; -import { Divider } from './Components/Divider'; -import { FilePicker } from './Components/FilePicker'; -import { PasswordInput } from './Components/PasswordInput'; -import { Calendar } from './Components/Calendar'; -import { Listview } from './Components/Listview'; -import { IFrame } from './Components/IFrame'; -import { CodeEditor } from './Components/CodeEditor'; -import { Timer } from './Components/Timer'; -import { Statistics } from './Components/Statistics'; -import { Pagination } from './Components/Pagination'; -import { Tags } from './Components/Tags'; -import { Spinner } from './Components/Spinner'; -import { CircularProgressBar } from './Components/CirularProgressbar'; -import { renderTooltip, getComponentName } from '@/_helpers/appUtils'; -import { RangeSlider } from './Components/RangeSlider'; -import { Timeline } from './Components/Timeline'; -import { SvgImage } from './Components/SvgImage'; -import { Html } from './Components/Html'; -import { ButtonGroup } from './Components/ButtonGroup'; -import { CustomComponent } from './Components/CustomComponent/CustomComponent'; -import { VerticalDivider } from './Components/verticalDivider'; -import { ColorPicker } from './Components/ColorPicker'; -import { KanbanBoard } from './Components/KanbanBoard/KanbanBoard'; -import { Kanban } from './Components/Kanban/Kanban'; -import { Steps } from './Components/Steps'; -import { TreeSelect } from './Components/TreeSelect'; -import { Icon } from './Components/Icon'; -import { Link } from './Components/Link'; -import { Form } from './Components/Form/Form'; -import { BoundedBox } from './Components/BoundedBox/BoundedBox'; -import OverlayTrigger from 'react-bootstrap/OverlayTrigger'; -import '@/_styles/custom.scss'; -import { validateProperties } from './component-properties-validation'; -import { validateWidget } from '@/_helpers/utils'; -import { componentTypes } from './WidgetManager/components'; -import { - resolveProperties, - resolveStyles, - resolveGeneralProperties, - resolveGeneralStyles, -} from './component-properties-resolution'; +import React from 'react'; +import HydrateWithResolveReferences from './Middlewares/HydrateWithResolveReferences'; +import BoxUI from './BoxUI'; import _ from 'lodash'; -import { EditorContext } from '@/Editor/Context/EditorContextWrapper'; -import { useTranslation } from 'react-i18next'; -import { useCurrentState } from '@/_stores/currentStateStore'; -import { useAppInfo } from '@/_stores/appDataStore'; -import { isPDFSupported } from '@/_stores/utils'; +import { useEditorStore } from '@/_stores/editorStore'; +import { shallow } from 'zustand/shallow'; -export const AllComponents = { - Button, - Image, - Text, - TextInput, - NumberInput, - Table, - TextArea, - Container, - Tabs, - RichTextEditor, - DropDown, - Checkbox, - Datepicker, - DaterangePicker, - Multiselect, - Modal, - Chart, - Map, - QrScanner, - ToggleSwitch, - RadioButton, - StarRating, - Divider, - FilePicker, - PasswordInput, - Calendar, - IFrame, - CodeEditor, - Listview, - Timer, - Statistics, - Pagination, - Tags, - Spinner, - CircularProgressBar, - RangeSlider, - Timeline, - SvgImage, - Html, - ButtonGroup, - CustomComponent, - VerticalDivider, - ColorPicker, - KanbanBoard, - Kanban, - Steps, - TreeSelect, - Link, - Icon, - Form, - BoundedBox, -}; - -/** - * Conditionally importing PDF component since importing it breaks app in older versions of browsers. - * refer: https://github.com/wojtekmaj/react-pdf?tab=readme-ov-file#compatibility - **/ -if (isPDFSupported()) { - AllComponents.PDF = await import('./Components/PDF').then((module) => module.PDF); +function deepEqualityCheckusingLoDash(obj1, obj2) { + return _.isEqual(obj1, obj2); } -export const Box = memo( - ({ - id, - width, - height, - yellow, - preview, - component, - inCanvas, - onComponentClick, - onEvent, - onComponentOptionChanged, - onComponentOptionsChanged, - paramUpdated, - changeCanDrag, - containerProps, - removeComponent, - canvasWidth, - mode, - customResolvables, - parentId, - sideBarDebugger, - readOnly, - childComponents, - isResizing, - adjustHeightBasedOnAlignment, - currentLayout, - }) => { - const { t } = useTranslation(); - const backgroundColor = yellow ? 'yellow' : ''; - const currentState = useCurrentState(); - const { events } = useAppInfo(); - const shouldAddBoxShadowAndVisibility = ['TextInput', 'PasswordInput', 'NumberInput', 'Text']; +export const shouldUpdate = (prevProps, nextProps) => { + return ( + deepEqualityCheckusingLoDash(prevProps?.id, nextProps?.id) && + deepEqualityCheckusingLoDash(prevProps?.component?.definition, nextProps?.component?.definition) && + prevProps?.width === nextProps?.width && + prevProps?.height === nextProps?.height + ); +}; - const componentMeta = useMemo(() => { - return componentTypes.find((comp) => component.component === comp.component); - }, [component]); +export const Box = (props) => { + const { id, component, mode, customResolvables } = props; - const ComponentToRender = AllComponents[component.component]; - const [renderCount, setRenderCount] = useState(0); - const [renderStartTime, setRenderStartTime] = useState(new Date()); - const [resetComponent, setResetStatus] = useState(false); + /** + * !This component does not consume the value returned from the below hook. + * Only purpose of the hook is to force one rerender the component + * */ + useEditorStore((state) => state.componentsNeedsUpdateOnNextRender.find((compId) => compId === id), shallow); - const resolvedProperties = resolveProperties(component, currentState, null, customResolvables); - const [validatedProperties, propertyErrors] = - mode === 'edit' && component.validate - ? validateProperties(resolvedProperties, componentMeta.properties) - : [resolvedProperties, []]; - if (shouldAddBoxShadowAndVisibility.includes(component.component)) { - validatedProperties.visibility = validatedProperties.visibility !== false ? true : false; - } - - const resolvedStyles = resolveStyles(component, currentState, null, customResolvables); - const [validatedStyles, styleErrors] = - mode === 'edit' && component.validate - ? validateProperties(resolvedStyles, componentMeta.styles) - : [resolvedStyles, []]; - if (!shouldAddBoxShadowAndVisibility.includes(component.component)) { - validatedStyles.visibility = validatedStyles.visibility !== false ? true : false; - } - const resolvedGeneralProperties = resolveGeneralProperties(component, currentState, null, customResolvables); - const [validatedGeneralProperties, generalPropertiesErrors] = component.validate - ? validateProperties(resolvedGeneralProperties, componentMeta.general) - : [resolvedGeneralProperties, []]; - - const resolvedGeneralStyles = resolveGeneralStyles(component, currentState, null, customResolvables); - - const [validatedGeneralStyles, generalStylesErrors] = - mode === 'edit' && component.validate - ? validateProperties(resolvedGeneralStyles, componentMeta.generalStyles) - : [resolvedGeneralStyles, []]; - - const darkMode = localStorage.getItem('darkMode') === 'true'; - const { variablesExposedForPreview, exposeToCodeHinter } = useContext(EditorContext) || {}; - - let styles = { - height: '100%', - }; - - if (inCanvas) { - styles = { - ...styles, - }; - } - useEffect(() => { - if (!component?.parent) { - onComponentOptionChanged && onComponentOptionChanged(component, 'id', id); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); /*computeComponentState was not getting the id on initial render therefore exposed variables were not set. - computeComponentState was being executed before addNewWidgetToTheEditor was completed.*/ - - useEffect(() => { - const currentPage = currentState?.page; - const componentName = getComponentName(currentState, id); - const errorLog = Object.fromEntries( - [...propertyErrors, ...styleErrors, ...generalPropertiesErrors, ...generalStylesErrors].map((error) => [ - `${componentName} - ${error.property}`, - { - page: currentPage, - type: 'component', - kind: 'component', - strace: 'page_level', - data: { message: `${error.message}`, status: true }, - resolvedProperties: resolvedProperties, - effectiveProperties: validatedProperties, - }, - ]) - ); - sideBarDebugger?.error(errorLog); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [JSON.stringify({ propertyErrors, styleErrors, generalPropertiesErrors })]); - - useEffect(() => { - setRenderCount(renderCount + 1); - if (renderCount > 10) { - setRenderCount(0); - const currentTime = new Date(); - const timeDifference = Math.abs(currentTime - renderStartTime); - if (timeDifference < 1000) { - throw Error; - } - setRenderStartTime(currentTime); - } - - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [JSON.stringify({ validatedProperties, validatedStyles })]); - - useEffect(() => { - if (customResolvables && !readOnly && mode === 'edit') { - const newCustomResolvable = {}; - newCustomResolvable[id] = { ...customResolvables }; - exposeToCodeHinter((prevState) => ({ ...prevState, ...newCustomResolvable })); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [JSON.stringify(customResolvables), readOnly]); - useEffect(() => { - if (resetComponent) setResetStatus(false); - }, [resetComponent]); - - let exposedVariables = currentState?.components[component.name] ?? {}; - const fireEvent = (eventName, options) => { - if (mode === 'edit' && eventName === 'onClick') { - onComponentClick(id, component); - } - - const componentEvents = events.filter((event) => event.sourceId === id); - - onEvent(eventName, componentEvents, { ...options, customVariables: { ...customResolvables } }); - }; - const validate = (value) => - validateWidget({ - ...{ widgetValue: value }, - ...{ validationObject: component.definition.validation, currentState }, - customResolveObjects: customResolvables, - }); - - const shouldHideWidget = component.component === 'PDF' && !isPDFSupported(); - - return ( - - renderTooltip({ - props, - text: inCanvas - ? `${ - shouldAddBoxShadowAndVisibility.includes(component.component) - ? validatedProperties.tooltip - : validatedGeneralProperties.tooltip - }` - : `${t(`widget.${component.name}.description`, component.description)}`, - }) - } - > -
- {!resetComponent && !shouldHideWidget ? ( - onComponentOptionChanged(component, variable, value, id)} - setExposedVariables={(variableSet) => - onComponentOptionsChanged(component, Object.entries(variableSet), id) - } - fireEvent={fireEvent} - validate={validate} - parentId={parentId} - customResolvables={customResolvables} - variablesExposedForPreview={variablesExposedForPreview} - exposeToCodeHinter={exposeToCodeHinter} - setProperty={(property, value) => { - paramUpdated(id, property, { value }); - }} - mode={mode} - resetComponent={() => setResetStatus(true)} - childComponents={childComponents} - dataCy={`draggable-widget-${String(component.name).toLowerCase()}`} - isResizing={isResizing} - adjustHeightBasedOnAlignment={adjustHeightBasedOnAlignment} - currentLayout={currentLayout} - > - ) : ( - <> - )} -
-
- ); - } -); + return ( + + + + ); +}; diff --git a/frontend/src/Editor/BoxUI.jsx b/frontend/src/Editor/BoxUI.jsx new file mode 100644 index 0000000000..b8b8798e6f --- /dev/null +++ b/frontend/src/Editor/BoxUI.jsx @@ -0,0 +1,175 @@ +import React, { useContext, useEffect } from 'react'; +import ControlledComponentToRender from './ControlledComponentToRender'; +import { renderTooltip, onComponentOptionChanged, onComponentOptionsChanged } from '@/_helpers/appUtils'; +import { useTranslation } from 'react-i18next'; +import OverlayTrigger from 'react-bootstrap/OverlayTrigger'; +import '@/_styles/custom.scss'; +import { EditorContext } from './Context/EditorContextWrapper'; +import { validateWidget } from '@/_helpers/utils'; +import { useCurrentState } from '@/_stores/currentStateStore'; +import { useAppInfo } from '@/_stores/appDataStore'; + +const shouldAddBoxShadowAndVisibility = ['TextInput', 'PasswordInput', 'NumberInput', 'Text']; + +const BoxUI = (props) => { + const { t } = useTranslation(); + + const { + inCanvas, + component, + properties, + styles, + generalProperties, + generalStyles, + mode, + onComponentClick, + onEvent, + id, + getContainerProps, + paramUpdated, + width, + height, + changeCanDrag, + removeComponent, + canvasWidth, + parentId, + customResolvables, + currentLayout, + readOnly, + currentPageId, + } = props; + + const darkMode = localStorage.getItem('darkMode') === 'true'; + + const { variablesExposedForPreview, exposeToCodeHinter } = useContext(EditorContext) || {}; + + const currentState = useCurrentState(); + + const validate = (value) => + validateWidget({ + ...{ widgetValue: value }, + ...{ validationObject: component.definition.validation, currentState }, + customResolveObjects: customResolvables, + }); + + useEffect(() => { + if (customResolvables && !readOnly && mode === 'edit') { + const newCustomResolvable = {}; + newCustomResolvable[id] = { ...customResolvables }; + exposeToCodeHinter((prevState) => ({ ...prevState, ...newCustomResolvable })); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [JSON.stringify(customResolvables), readOnly]); + + let exposedVariables = currentState?.components[component.name] ?? {}; + const { events } = useAppInfo(); + + const fireEvent = (eventName, options) => { + if (mode === 'edit' && eventName === 'onClick') { + onComponentClick(id, component); + } + + const componentEvents = events.filter((event) => event.sourceId === id); + + onEvent(eventName, componentEvents, { ...options, customVariables: { ...customResolvables } }); + }; + + let _styles = { + height: '100%', + }; + + if (inCanvas) { + _styles = { + ..._styles, + }; + } + useEffect(() => { + if (!component?.parent) { + onComponentOptionChanged(component, 'id', id); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + return ( + + renderTooltip({ + props, + text: inCanvas + ? `${ + shouldAddBoxShadowAndVisibility.includes(component.component) + ? properties.tooltip + : generalProperties.tooltip + }` + : `${t(`widget.${component.name}.description`, component.description)}`, + }) + } + > +
+ onComponentOptionChanged(component, variable, value, id)} + setExposedVariables={(variableSet) => onComponentOptionsChanged(component, Object.entries(variableSet), id)} + fireEvent={fireEvent} + validate={validate} + parentId={parentId} + customResolvables={customResolvables} + variablesExposedForPreview={variablesExposedForPreview} + exposeToCodeHinter={exposeToCodeHinter} + setProperty={(property, value) => { + paramUpdated(id, property, { value }); + }} + mode={mode} + // resetComponent={() => setResetStatus(true)} + dataCy={`draggable-widget-${String(component.name).toLowerCase()}`} + currentLayout={currentLayout} + currentState={currentState} + currentPageId={currentPageId} + getContainerProps={component.component === 'Form' ? getContainerProps : null} + /> +
+
+ ); +}; + +export default BoxUI; diff --git a/frontend/src/Editor/CodeBuilder/CodeBuilder.jsx b/frontend/src/Editor/CodeBuilder/CodeBuilder.jsx index d28fb36256..e7e6a10adf 100644 --- a/frontend/src/Editor/CodeBuilder/CodeBuilder.jsx +++ b/frontend/src/Editor/CodeBuilder/CodeBuilder.jsx @@ -95,7 +95,6 @@ export function CodeBuilder({ initialValue, onChange, components }) { return { item: item }; }); } else { - console.log(currentWord); filteredVariables = fuse.search(currentWord); } return filteredVariables.map((variable) => renderVariable(type, key, variable.item.name)); diff --git a/frontend/src/Editor/CodeBuilder/Elements/BoxShadow.jsx b/frontend/src/Editor/CodeBuilder/Elements/BoxShadow.jsx index ec4a41b4c7..77a4644922 100644 --- a/frontend/src/Editor/CodeBuilder/Elements/BoxShadow.jsx +++ b/frontend/src/Editor/CodeBuilder/Elements/BoxShadow.jsx @@ -142,7 +142,7 @@ export const BoxShadow = ({ value, onChange, cyLabel }) => { ); }; - const _value = `#${value.split('#')[1]}`; + const _value = `#${(value || '').split('#')[1]}`; const outerStyles = { width: '142px', height: '32px', diff --git a/frontend/src/Editor/CodeEditor/utils.js b/frontend/src/Editor/CodeEditor/utils.js index 6bf6dbd7ca..7f06064f21 100644 --- a/frontend/src/Editor/CodeEditor/utils.js +++ b/frontend/src/Editor/CodeEditor/utils.js @@ -112,7 +112,7 @@ const resolveWorkspaceVariables = (query, state) => { resolvedStr = resolvedStr.replace(serverMatch, 'HiddenEnvironmentVariable'); error = 'Server variables cannot be resolved in the client.'; } else { - const [resolvedCode, err] = resolveCode(code, state); + const [resolvedCode, err] = resolveCode(code); if (!resolvedCode) { error = err ? err : `Cannot resolve ${query}`; @@ -126,7 +126,7 @@ const resolveWorkspaceVariables = (query, state) => { return [valid, error, resolvedStr]; }; -function resolveCode(code, state, customObjects = {}, withError = true, reservedKeyword, isJsCode) { +function resolveCode(code, customObjects = {}, withError = true, reservedKeyword, isJsCode) { let result = ''; let error; @@ -135,6 +135,7 @@ function resolveCode(code, state, customObjects = {}, withError = true, reserved error = `Cannot resolve function call ${code}`; } else { try { + const state = useCurrentStateStore.getState(); const evalFunction = Function( [ 'variables', @@ -200,7 +201,7 @@ const resolveMultiDynamicReferences = (code, lookupTable) => { resolvedValue = resolvedValue.replace(variable, res); } else { const currentState = useCurrentStateStore.getState(); - const [resolvedCode] = resolveCode(variableToResolve, currentState, {}, true, [], true); + const [resolvedCode] = resolveCode(variableToResolve, {}, true, [], true); resolvedValue = resolvedCode; } @@ -216,11 +217,9 @@ export const resolveReferences = (query, validationSchema, customResolvers = {}, let resolvedValue = query; let error = null; - const currentState = useCurrentStateStore.getState(); - //Todo : remove resolveWorkspaceVariables when workspace variables are removed if (query?.startsWith('%%') && query?.endsWith('%%')) { - return resolveWorkspaceVariables(query, currentState); + return resolveWorkspaceVariables(query); } if ((!validationSchema || isEmpty(validationSchema)) && (!query?.includes('{{') || !query?.includes('}}'))) { @@ -243,10 +242,9 @@ export const resolveReferences = (query, validationSchema, customResolvers = {}, if (fxActive && (value.startsWith('#') || value.includes('table-'))) { value = JSON.stringify(value); } - const { toResolveReference, jsExpression, jsExpMatch } = inferJSExpAndReferences(value, lookupTable.hints); - const isComponentValue = toResolveReference?.startsWith('components.') || false; //!Notes: As we removed the updating of references on currentState changes, exposed variable of components are dynamic and cannot be controlled in any form, so we are resolving only components references with our legacy approach. - if (!isComponentValue && !jsExpMatch && toResolveReference && lookupTable.hints.has(toResolveReference)) { + + if (!jsExpMatch && toResolveReference && lookupTable.hints.has(toResolveReference)) { const idToLookUp = lookupTable.hints.get(toResolveReference); resolvedValue = lookupTable.resolvedRefs.get(idToLookUp); @@ -254,10 +252,10 @@ export const resolveReferences = (query, validationSchema, customResolvers = {}, let jscode = value.replace(toResolveReference, resolvedValue); jscode = value.replace(toResolveReference, `'${resolvedValue}'`); - resolvedValue = resolveCode(jscode, currentState, customResolvers); + resolvedValue = resolveCode(jscode, customResolvers); } } else { - const [resolvedCode, errorRef] = resolveCode(value, currentState, customResolvers, true, [], true); + const [resolvedCode, errorRef] = resolveCode(value, customResolvers, true, [], true); resolvedValue = resolvedCode; error = errorRef || null; diff --git a/frontend/src/Editor/Comment/index.jsx b/frontend/src/Editor/Comment/index.jsx index f35912bc48..d1c4892493 100644 --- a/frontend/src/Editor/Comment/index.jsx +++ b/frontend/src/Editor/Comment/index.jsx @@ -2,7 +2,7 @@ import React from 'react'; import cx from 'classnames'; import { useDrag } from 'react-dnd'; -import { ItemTypes } from '@/Editor/ItemTypes'; +import { ItemTypes } from '@/Editor/editorConstants'; import CommentHeader from '@/Editor/Comment/CommentHeader'; import CommentBody from '@/Editor/Comment/CommentBody'; import CommentFooter from '@/Editor/Comment/CommentFooter'; diff --git a/frontend/src/Editor/Components/Button.jsx b/frontend/src/Editor/Components/Button.jsx index aa49466759..29bbb7ac49 100644 --- a/frontend/src/Editor/Components/Button.jsx +++ b/frontend/src/Editor/Components/Button.jsx @@ -1,8 +1,11 @@ import React, { useEffect, useState } from 'react'; import cx from 'classnames'; const tinycolor = require('tinycolor2'); +import useRenderCount from '@/_hooks/useRenderCount'; export const Button = function Button(props) { + useRenderCount(`Button Main component ${props.id}`); + const { height, properties, styles, fireEvent, id, dataCy, setExposedVariable, setExposedVariables } = props; const { backgroundColor, textColor, borderRadius, loaderColor, disabledState, borderColor, boxShadow } = styles; diff --git a/frontend/src/Editor/Components/Container.jsx b/frontend/src/Editor/Components/Container.jsx index 2311f18cd0..8a107da50c 100644 --- a/frontend/src/Editor/Components/Container.jsx +++ b/frontend/src/Editor/Components/Container.jsx @@ -24,7 +24,6 @@ export const Container = function Container({ border: `1px solid ${borderColor}`, height, display: visibility ? 'flex' : 'none', - // overflow: draggedSubContainer ? 'unset' : 'hidden auto', overflow: 'hidden auto', position: 'relative', boxShadow, diff --git a/frontend/src/Editor/Components/FilePicker.jsx b/frontend/src/Editor/Components/FilePicker.jsx index 0a144162cd..1be05177c7 100644 --- a/frontend/src/Editor/Components/FilePicker.jsx +++ b/frontend/src/Editor/Components/FilePicker.jsx @@ -4,7 +4,7 @@ import { resolveWidgetFieldValue } from '@/_helpers/utils'; import { toast } from 'react-hot-toast'; // eslint-disable-next-line import/no-unresolved import * as XLSX from 'xlsx/xlsx.mjs'; -import { useCurrentState } from '@/_stores/currentStateStore'; + import { useAppInfo } from '@/_stores/appDataStore'; export const FilePicker = ({ @@ -19,7 +19,6 @@ export const FilePicker = ({ setExposedVariable, dataCy, }) => { - const currentState = useCurrentState(); //* properties definitions const instructionText = component.definition.properties.instructionText?.value ?? 'Drag and drop files here or click to select files'; @@ -30,31 +29,25 @@ export const FilePicker = ({ const fileType = component.definition.properties.fileType?.value ?? 'image/*'; const maxSize = component.definition.properties.maxSize?.value ?? 1048576; const minSize = component.definition.properties.minSize?.value ?? 0; - const parseContent = resolveWidgetFieldValue( - component.definition.properties.parseContent?.value ?? false, - currentState - ); + const parseContent = resolveWidgetFieldValue(component.definition.properties.parseContent?.value); const fileTypeFromExtension = component.definition.properties.parseFileType?.value ?? 'auto-detect'; - const parsedEnableDropzone = - typeof enableDropzone !== 'boolean' ? resolveWidgetFieldValue(enableDropzone, currentState) : true; - const parsedEnablePicker = - typeof enablePicker !== 'boolean' ? resolveWidgetFieldValue(enablePicker, currentState) : true; + const parsedEnableDropzone = typeof enableDropzone !== 'boolean' ? resolveWidgetFieldValue(enableDropzone) : true; + const parsedEnablePicker = typeof enablePicker !== 'boolean' ? resolveWidgetFieldValue(enablePicker) : true; - const parsedMaxFileCount = - typeof maxFileCount !== 'number' ? resolveWidgetFieldValue(maxFileCount, currentState) : maxFileCount; + const parsedMaxFileCount = typeof maxFileCount !== 'number' ? resolveWidgetFieldValue(maxFileCount) : maxFileCount; const parsedEnableMultiple = - typeof enableMultiple !== 'boolean' ? resolveWidgetFieldValue(enableMultiple, currentState) : enableMultiple; - const parsedFileType = resolveWidgetFieldValue(fileType, currentState); - const parsedMinSize = typeof fileType !== 'number' ? resolveWidgetFieldValue(minSize, currentState) : minSize; - const parsedMaxSize = typeof fileType !== 'number' ? resolveWidgetFieldValue(maxSize, currentState) : maxSize; + typeof enableMultiple !== 'boolean' ? resolveWidgetFieldValue(enableMultiple) : enableMultiple; + const parsedFileType = resolveWidgetFieldValue(fileType); + const parsedMinSize = typeof fileType !== 'number' ? resolveWidgetFieldValue(minSize) : minSize; + const parsedMaxSize = typeof fileType !== 'number' ? resolveWidgetFieldValue(maxSize) : maxSize; //* styles definitions const widgetVisibility = component.definition.styles?.visibility?.value ?? true; const disabledState = component.definition.styles?.disabledState?.value ?? false; const parsedDisabledState = - typeof disabledState !== 'boolean' ? resolveWidgetFieldValue(disabledState, currentState) : disabledState; + typeof disabledState !== 'boolean' ? resolveWidgetFieldValue(disabledState) : disabledState; const parsedWidgetVisibility = - typeof widgetVisibility !== 'boolean' ? resolveWidgetFieldValue(widgetVisibility, currentState) : widgetVisibility; + typeof widgetVisibility !== 'boolean' ? resolveWidgetFieldValue(widgetVisibility) : widgetVisibility; const { events: allAppEvents } = useAppInfo(); diff --git a/frontend/src/Editor/Components/Form/Form.jsx b/frontend/src/Editor/Components/Form/Form.jsx index 58181929cc..0a9e4615e6 100644 --- a/frontend/src/Editor/Components/Form/Form.jsx +++ b/frontend/src/Editor/Components/Form/Form.jsx @@ -7,7 +7,7 @@ import _, { omit } from 'lodash'; import { Box } from '@/Editor/Box'; import { generateUIComponents } from './FormUtils'; import { useMounted } from '@/_hooks/use-mount'; -import { removeFunctionObjects } from '@/_helpers/appUtils'; +import { onComponentClick, onComponentOptionChanged, removeFunctionObjects } from '@/_helpers/appUtils'; import { useAppInfo } from '@/_stores/appDataStore'; export const Form = function Form(props) { const { @@ -15,7 +15,6 @@ export const Form = function Form(props) { component, width, height, - containerProps, removeComponent, styles, setExposedVariable, @@ -25,15 +24,19 @@ export const Form = function Form(props) { fireEvent, properties, resetComponent, - childComponents, onEvent, dataCy, paramUpdated, - adjustHeightBasedOnAlignment, + currentLayout, + mode, + getContainerProps, + containerProps, } = props; const { events: allAppEvents } = useAppInfo(); + const { childComponents } = containerProps; + const formEvents = allAppEvents.filter((event) => event.target === 'component' && event.sourceId === id); const { visibility, disabledState, borderRadius, borderColor, boxShadow } = styles; const { buttonToSubmit, loadingState, advanced, JSONSchema } = properties; @@ -134,7 +137,7 @@ export const Form = function Form(props) { formattedChildData = extractData(childrenData); childValidation = checkJsonChildrenValidtion(); } else { - Object.keys(childComponents).forEach((childId) => { + Object.keys(childComponents ?? {}).forEach((childId) => { if (childrenData[childId]?.name) { formattedChildData[childrenData[childId].name] = { ...omit(childrenData[childId], 'name'), id: childId }; childValidation = childValidation && (childrenData[childId]?.isValid ?? true); @@ -204,7 +207,7 @@ export const Form = function Form(props) { return Promise.resolve(); } onOptionChange({ component, optionName, value, componentId }); - return containerProps.onComponentOptionChanged(component, optionName, value); + return onComponentOptionChanged(component, optionName, value); } const onOptionChange = ({ component, optionName, value, componentId }) => { @@ -227,7 +230,7 @@ export const Form = function Form(props) { style={computedStyles} onSubmit={handleSubmit} onClick={(e) => { - if (e.target.className === 'real-canvas') containerProps.onComponentClick(id, component); + if (e.target.className === 'real-canvas') onComponentClick(id, component); }} //Hack, should find a better solution - to prevent losing z index+1 when container element is clicked > {loadingState ? ( @@ -244,7 +247,6 @@ export const Form = function Form(props) { parentComponent={component} containerCanvasWidth={width} parent={id} - {...containerProps} parentRef={parentRef} removeComponent={removeComponent} onOptionChange={function ({ component, optionName, value, componentId }) { @@ -252,12 +254,14 @@ export const Form = function Form(props) { onOptionChange({ component, optionName, value, componentId }); } }} + currentPageId={props.currentPageId} + {...props} /> )} @@ -276,26 +280,23 @@ export const Form = function Form(props) { key={index} > ); diff --git a/frontend/src/Editor/Components/Map/Map.jsx b/frontend/src/Editor/Components/Map/Map.jsx index 1867bcf1e5..19748d5003 100644 --- a/frontend/src/Editor/Components/Map/Map.jsx +++ b/frontend/src/Editor/Components/Map/Map.jsx @@ -1,7 +1,7 @@ /* eslint-disable react-hooks/exhaustive-deps */ import React, { useState, useCallback, useEffect } from 'react'; import { GoogleMap, LoadScript, Marker, Autocomplete, Polygon } from '@react-google-maps/api'; -import { resolveReferences, resolveWidgetFieldValue } from '@/_helpers/utils'; +import { resolveWidgetFieldValue } from '@/_helpers/utils'; import { darkModeStyles } from './styles'; import { useTranslation } from 'react-i18next'; @@ -12,7 +12,6 @@ export const Map = function Map({ component, darkMode, onComponentClick, - currentState, onComponentOptionChanged, onComponentOptionsChanged, onEvent, @@ -27,27 +26,27 @@ export const Map = function Map({ const { t } = useTranslation(); const addNewMarkersProp = component.definition.properties.addNewMarkers; - const canAddNewMarkers = addNewMarkersProp ? resolveReferences(addNewMarkersProp.value, currentState) : false; + const canAddNewMarkers = addNewMarkersProp ? resolveWidgetFieldValue(addNewMarkersProp.value) : false; const canSearchProp = component.definition.properties.canSearch; - const canSearch = canSearchProp ? resolveReferences(canSearchProp.value, currentState) : false; + const canSearch = canSearchProp ? resolveWidgetFieldValue(canSearchProp.value) : false; const widgetVisibility = component.definition.styles?.visibility?.value ?? true; const disabledState = component.definition.styles?.disabledState?.value ?? false; const parsedDisabledState = - typeof disabledState !== 'boolean' ? resolveWidgetFieldValue(disabledState, currentState) : disabledState; + typeof disabledState !== 'boolean' ? resolveWidgetFieldValue(disabledState) : disabledState; let parsedWidgetVisibility = widgetVisibility; try { - parsedWidgetVisibility = resolveReferences(parsedWidgetVisibility, currentState, []); + parsedWidgetVisibility = resolveWidgetFieldValue(parsedWidgetVisibility); } catch (err) { console.log(err); } const [gmap, setGmap] = useState(null); const [autoComplete, setAutoComplete] = useState(null); - const [mapCenter, setMapCenter] = useState(resolveReferences(center, currentState)); + const [mapCenter, setMapCenter] = useState(() => resolveWidgetFieldValue(center)); const [markers, setMarkers] = useState(defaultMarkers); const containerStyle = { @@ -97,7 +96,7 @@ export const Map = function Map({ } useEffect(() => { - const resolvedCenter = resolveReferences(center, currentState); + const resolvedCenter = resolveWidgetFieldValue(center); setMapCenter(resolvedCenter); onComponentOptionsChanged(component, [['center', addMapUrlToJson(resolvedCenter)]]); // eslint-disable-next-line react-hooks/exhaustive-deps @@ -128,7 +127,7 @@ export const Map = function Map({ useEffect(() => { setExposedVariable('setLocation', async function (lat, lng) { - if (lat && lng) setMapCenter(resolveReferences({ lat, lng }, currentState)); + if (lat && lng) setMapCenter(resolveWidgetFieldValue({ lat, lng })); }); }, [setMapCenter]); diff --git a/frontend/src/Editor/Components/NumberInput.jsx b/frontend/src/Editor/Components/NumberInput.jsx index df2e9140c2..151440290e 100644 --- a/frontend/src/Editor/Components/NumberInput.jsx +++ b/frontend/src/Editor/Components/NumberInput.jsx @@ -3,8 +3,8 @@ import './numberinput.scss'; import SolidIcon from '@/_ui/Icon/SolidIcons'; import * as Icons from '@tabler/icons-react'; import Loader from '@/ToolJetUI/Loader/Loader'; -import { resolveReferences } from '@/_helpers/utils'; -import { useCurrentState } from '@/_stores/currentStateStore'; +import { resolveWidgetFieldValue } from '@/_helpers/utils'; + const tinycolor = require('tinycolor2'); import Label from '@/_ui/Label'; @@ -19,8 +19,6 @@ export const NumberInput = function NumberInput({ darkMode, dataCy, isResizing, - adjustHeightBasedOnAlignment, - currentLayout, }) { const { loadingState, disabledState, label, placeholder } = properties; const { @@ -40,9 +38,9 @@ export const NumberInput = function NumberInput({ } = styles; const textColor = darkMode && ['#232e3c', '#000000ff'].includes(styles.textColor) ? '#fff' : styles.textColor; - const isMandatory = resolveReferences(component?.definition?.validation?.mandatory?.value, currentState) ?? false; - const minValue = resolveReferences(component?.definition?.validation?.minValue?.value, currentState) ?? null; - const maxValue = resolveReferences(component?.definition?.validation?.maxValue?.value, currentState) ?? null; + const isMandatory = resolveWidgetFieldValue(component?.definition?.validation?.mandatory?.value) ?? false; + const minValue = resolveWidgetFieldValue(component?.definition?.validation?.minValue?.value) ?? null; + const maxValue = resolveWidgetFieldValue(component?.definition?.validation?.maxValue?.value) ?? null; const [visibility, setVisibility] = useState(properties.visibility); const [loading, setLoading] = useState(loadingState); @@ -52,7 +50,7 @@ export const NumberInput = function NumberInput({ const [isFocused, setIsFocused] = useState(false); const inputRef = useRef(null); - const currentState = useCurrentState(); + const [disable, setDisable] = useState(disabledState || loadingState); const labelRef = useRef(); const _width = (width / 100) * 70; // Max width which label can go is 70% for better UX calculate width based on this value @@ -62,13 +60,6 @@ export const NumberInput = function NumberInput({ // eslint-disable-next-line react-hooks/exhaustive-deps }, [label]); - useEffect(() => { - if (alignment == 'top' && ((label?.length > 0 && width > 0) || (auto && width == 0 && label && label?.length != 0))) - adjustHeightBasedOnAlignment(true); - else adjustHeightBasedOnAlignment(false); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [alignment, label?.length, currentLayout, width, auto]); - useEffect(() => { setValue(Number(parseFloat(value).toFixed(properties.decimalPlaces))); // eslint-disable-next-line react-hooks/exhaustive-deps diff --git a/frontend/src/Editor/Components/PasswordInput.jsx b/frontend/src/Editor/Components/PasswordInput.jsx index a6e0cd521f..71b9f99c4d 100644 --- a/frontend/src/Editor/Components/PasswordInput.jsx +++ b/frontend/src/Editor/Components/PasswordInput.jsx @@ -1,6 +1,6 @@ import React, { useEffect, useRef, useState } from 'react'; -import { resolveReferences } from '@/_helpers/utils'; -import { useCurrentState } from '@/_stores/currentStateStore'; +import { resolveWidgetFieldValue } from '@/_helpers/utils'; + import * as Icons from '@tabler/icons-react'; import Loader from '@/ToolJetUI/Loader/Loader'; import SolidIcon from '@/_ui/Icon/SolidIcons'; @@ -17,8 +17,6 @@ export const PasswordInput = function PasswordInput({ darkMode, dataCy, isResizing, - adjustHeightBasedOnAlignment, - currentLayout, }) { const textInputRef = useRef(); const labelRef = useRef(); @@ -46,8 +44,8 @@ export const PasswordInput = function PasswordInput({ const [visibility, setVisibility] = useState(properties.visibility); const { isValid, validationError } = validate(passwordValue); const [showValidationError, setShowValidationError] = useState(false); - const currentState = useCurrentState(); - const isMandatory = resolveReferences(component?.definition?.validation?.mandatory?.value, currentState); + + const isMandatory = resolveWidgetFieldValue(component?.definition?.validation?.mandatory?.value); const [labelWidth, setLabelWidth] = useState(0); const defaultAlignment = alignment === 'side' || alignment === 'top' ? alignment : 'side'; const [iconVisibility, setIconVisibility] = useState(false); @@ -177,13 +175,6 @@ export const PasswordInput = function PasswordInput({ const IconElement = Icons[iconName] == undefined ? Icons['IconHome2'] : Icons[iconName]; // eslint-disable-next-line import/namespace - useEffect(() => { - if (alignment == 'top' && ((label?.length > 0 && width > 0) || (auto && width == 0 && label && label?.length != 0))) - adjustHeightBasedOnAlignment(true); - else adjustHeightBasedOnAlignment(false); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [alignment, label?.length, currentLayout, width, auto]); - useEffect(() => { setExposedVariable('isMandatory', isMandatory); // eslint-disable-next-line react-hooks/exhaustive-deps diff --git a/frontend/src/Editor/Components/Table/Table.jsx b/frontend/src/Editor/Components/Table/Table.jsx index 19cb66c7ef..42604a0926 100644 --- a/frontend/src/Editor/Components/Table/Table.jsx +++ b/frontend/src/Editor/Components/Table/Table.jsx @@ -14,7 +14,12 @@ import { useColumnOrder, } from 'react-table'; import cx from 'classnames'; -import { resolveReferences, validateWidget, determineJustifyContentValue } from '@/_helpers/utils'; +import { + resolveReferences, + validateWidget, + determineJustifyContentValue, + resolveWidgetFieldValue, +} from '@/_helpers/utils'; import { useExportData } from 'react-table-plugins'; import Papa from 'papaparse'; import { Pagination } from './Pagination'; @@ -388,11 +393,11 @@ export function Table({ let tableData = [], dynamicColumn = []; - const useDynamicColumn = resolveReferences(component.definition.properties?.useDynamicColumn?.value, currentState); + const useDynamicColumn = resolveWidgetFieldValue(component.definition.properties?.useDynamicColumn?.value); if (currentState) { - tableData = resolveReferences(component.definition.properties.data.value, currentState, []); + tableData = resolveWidgetFieldValue(component.definition.properties.data.value); dynamicColumn = useDynamicColumn - ? resolveReferences(component.definition.properties?.columnData?.value, currentState, []) ?? [] + ? resolveWidgetFieldValue(component.definition.properties?.columnData?.value) ?? [] : []; if (!Array.isArray(tableData)) { tableData = []; diff --git a/frontend/src/Editor/Components/Tabs.jsx b/frontend/src/Editor/Components/Tabs.jsx index 6c8fb64c21..5407d5a99f 100644 --- a/frontend/src/Editor/Components/Tabs.jsx +++ b/frontend/src/Editor/Components/Tabs.jsx @@ -1,7 +1,8 @@ import React, { useRef, useState, useEffect } from 'react'; import { SubCustomDragLayer } from '../SubCustomDragLayer'; import { SubContainer } from '../SubContainer'; -import { resolveReferences, resolveWidgetFieldValue, isExpectedDataType } from '@/_helpers/utils'; +import { resolveWidgetFieldValue, isExpectedDataType } from '@/_helpers/utils'; +import { handleLowPriorityWork } from '@/_helpers/editorHelpers'; export const Tabs = function Tabs({ id, @@ -9,7 +10,6 @@ export const Tabs = function Tabs({ width, height, containerProps, - currentState, removeComponent, setExposedVariable, setExposedVariables, @@ -24,13 +24,13 @@ export const Tabs = function Tabs({ const disabledState = component.definition.styles?.disabledState?.value ?? false; const defaultTab = component.definition.properties.defaultTab.value; // config for tabs. Includes title - const tabs = isExpectedDataType(resolveReferences(component.definition.properties.tabs.value, currentState), 'array'); + const tabs = isExpectedDataType(resolveWidgetFieldValue(component.definition.properties?.tabs?.value), 'array'); let parsedTabs = tabs; - parsedTabs = resolveWidgetFieldValue(parsedTabs, currentState); + parsedTabs = resolveWidgetFieldValue(parsedTabs); const hideTabs = component.definition.properties?.hideTabs?.value ?? false; - // renderOnlyActiveTab - TRUE (renders only the content of the active tab) - // renderOnlyActiveTab - FALSE (renders all the content irrespective of the active tab to persist value from other tabs) + //* renderOnlyActiveTab - TRUE (renders only the content of the active tab) + //* renderOnlyActiveTab - FALSE (renders all the content irrespective of the active tab to persist value from other tabs) const renderOnlyActiveTab = component.definition.properties?.renderOnlyActiveTab?.value ?? false; // set index as id if id is not provided @@ -39,25 +39,23 @@ export const Tabs = function Tabs({ // Highlight color - for active tab text and border const highlightColor = component.definition.styles?.highlightColor?.value ?? '#f44336'; let parsedHighlightColor = highlightColor; - parsedHighlightColor = resolveWidgetFieldValue(highlightColor, currentState); + parsedHighlightColor = resolveWidgetFieldValue(highlightColor); // Default tab let parsedDefaultTab = defaultTab; - parsedDefaultTab = resolveWidgetFieldValue(parsedDefaultTab, currentState, 1); + parsedDefaultTab = resolveWidgetFieldValue(parsedDefaultTab, 1); const parsedDisabledState = - typeof disabledState !== 'boolean' ? resolveWidgetFieldValue(disabledState, currentState) : disabledState; + typeof disabledState !== 'boolean' ? resolveWidgetFieldValue(disabledState) : disabledState; - const parsedHideTabs = typeof hideTabs !== 'boolean' ? resolveWidgetFieldValue(hideTabs, currentState) : hideTabs; + const parsedHideTabs = typeof hideTabs !== 'boolean' ? resolveWidgetFieldValue(hideTabs) : hideTabs; const parsedRenderOnlyActiveTab = - typeof renderOnlyActiveTab !== 'boolean' - ? resolveWidgetFieldValue(renderOnlyActiveTab, currentState) - : renderOnlyActiveTab; + typeof renderOnlyActiveTab !== 'boolean' ? resolveWidgetFieldValue(renderOnlyActiveTab) : renderOnlyActiveTab; let parsedWidgetVisibility = widgetVisibility; try { - parsedWidgetVisibility = resolveReferences(parsedWidgetVisibility, currentState, []); + parsedWidgetVisibility = resolveWidgetFieldValue(parsedWidgetVisibility); } catch (err) { console.log(err); } @@ -66,6 +64,8 @@ export const Tabs = function Tabs({ const [currentTab, setCurrentTab] = useState(parsedDefaultTab); const [bgColor, setBgColor] = useState('#fff'); + const [tabSwitchingOnProgress, setTabSwitchingOnProgress] = useState(false); + useEffect(() => { setCurrentTab(parsedDefaultTab); }, [parsedDefaultTab]); @@ -74,7 +74,7 @@ export const Tabs = function Tabs({ const currentTabData = parsedTabs.filter((tab) => tab.id === currentTab); setBgColor(currentTabData[0]?.backgroundColor ? currentTabData[0]?.backgroundColor : darkMode ? '#324156' : '#fff'); // eslint-disable-next-line react-hooks/exhaustive-deps - }, [currentState, currentTab]); + }, [currentTab]); function computeTabVisibility(componentId, id) { let tabVisibility = 'hidden'; @@ -131,6 +131,13 @@ export const Tabs = function Tabs({ ); + function shouldRenderTabContent(tab) { + if (tabSwitchingOnProgress || parsedRenderOnlyActiveTab) { + return tab.id === currentTab; + } + return true; // Render by default if no specific conditions are met + } + return (
{ + setTabSwitchingOnProgress(true); + !tab?.disabled && setCurrentTab(tab.id); !tab?.disabled && setExposedVariable('currentTab', tab.id); - fireEvent('onTabSwitch'); + + handleLowPriorityWork(() => { + fireEvent('onTabSwitch'); + setTabSwitchingOnProgress(false); + }); }} key={tab.id} > @@ -188,7 +201,8 @@ export const Tabs = function Tabs({ id={`${id}-${tab.id}`} key={tab.id} > - {parsedRenderOnlyActiveTab ? tab.id === currentTab && renderTabContent(id, tab) : renderTabContent(id, tab)} + {shouldRenderTabContent(tab) && renderTabContent(id, tab)} + {tab.id === currentTab && ( { - if (alignment == 'top' && ((label?.length > 0 && width > 0) || (auto && width == 0 && label && label?.length != 0))) - adjustHeightBasedOnAlignment(true); - else { - adjustHeightBasedOnAlignment(false); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [alignment, label?.length, currentLayout, width, auto]); - useEffect(() => { setExposedVariable('isMandatory', isMandatory); // eslint-disable-next-line react-hooks/exhaustive-deps diff --git a/frontend/src/Editor/ConfigHandle.jsx b/frontend/src/Editor/ConfigHandle.jsx index 25673bb6be..f97ef36b57 100644 --- a/frontend/src/Editor/ConfigHandle.jsx +++ b/frontend/src/Editor/ConfigHandle.jsx @@ -1,3 +1,4 @@ +import { useEditorStore } from '@/_stores/editorStore'; import React from 'react'; export const ConfigHandle = function ConfigHandle({ @@ -15,13 +16,15 @@ export const ConfigHandle = function ConfigHandle({ isVersionReleased, showHandle, }) { + const shouldShowHandle = useEditorStore((state) => state.hoveredComponent === id) || showHandle; + return (
diff --git a/frontend/src/Editor/Container.jsx b/frontend/src/Editor/Container.jsx index 040ae9709d..42c64bfb8f 100644 --- a/frontend/src/Editor/Container.jsx +++ b/frontend/src/Editor/Container.jsx @@ -2,11 +2,11 @@ import React, { useCallback, useState, useEffect, useRef, useMemo } from 'react'; import cx from 'classnames'; import { useDrop, useDragLayer } from 'react-dnd'; -import { ItemTypes } from './ItemTypes'; +import { ItemTypes, EditorConstants } from './editorConstants'; import { DraggableBox } from './DraggableBox'; import update from 'immutability-helper'; import { componentTypes } from './WidgetManager/components'; -import { resolveReferences } from '@/_helpers/utils'; +import { resolveWidgetFieldValue } from '@/_helpers/utils'; import Comments from './Comments'; import { commentsService } from '@/_services'; import config from 'config'; @@ -23,11 +23,13 @@ import _, { cloneDeep, isEmpty } from 'lodash'; import { diff } from 'deep-object-diff'; import DragContainer from './DragContainer'; import { compact, correctBounds } from './gridUtils'; -import { useDraggedSubContainer, useGridStore } from '@/_stores/gridStore'; import { isPDFSupported } from '@/_stores/utils'; import toast from 'react-hot-toast'; +import { isOnlyLayoutUpdate, handleLowPriorityWork } from '@/_helpers/editorHelpers'; +import GhostWidget from './GhostWidget'; +import { useDraggedSubContainer, useGridStore } from '@/_stores/gridStore'; -// const noOfGrids = 24; +const deviceWindowWidth = EditorConstants.deviceWindowWidth; export const Container = ({ canvasWidth, @@ -35,37 +37,32 @@ export const Container = ({ snapToGrid, onComponentClick, onEvent, - appDefinition, appDefinitionChanged, - onComponentOptionChanged, - onComponentOptionsChanged, appLoading, setSelectedComponent, zoomLevel, removeComponent, - deviceWindowWidth, darkMode, socket, handleUndo, handleRedo, - sideBarDebugger, currentPageId, }) => { + const appDefinition = useEditorStore.getState().appDefinition; // Dont update first time to skip // redundant save on app definition load const firstUpdate = useRef(true); - // const [noOfGrids, setNoOfGrids] = useNoOfGrid(); + const noOfGrids = 43; - const [subContainerWidths, setSubContainerWidths] = useState({}); + const draggedSubContainer = useDraggedSubContainer(false); - const { resizingComponentId } = useGridStore( + const { resizingComponentId, isGridDragging } = useGridStore( (state) => ({ resizingComponentId: state?.resizingComponentId, - draggingComponentId: state?.draggingComponentId, + isGridDragging: !!state?.draggingComponentId, }), shallow ); - // const [dragTarget] = useDragTarget(); const { showComments, currentLayout, selectedComponents } = useEditorStore( (state) => ({ @@ -80,7 +77,7 @@ export const Container = ({ const { appId } = useAppInfo(); const currentState = useCurrentState(); - const { appVersionsId, enableReleasedVersionPopupState, isVersionReleased } = useAppVersionStore( + const { appVersionsId, isVersionReleased } = useAppVersionStore( (state) => ({ appVersionsId: state?.editingVersion?.id, enableReleasedVersionPopupState: state.actions.enableReleasedVersionPopupState, @@ -98,12 +95,12 @@ export const Container = ({ const components = useMemo( () => appDefinition.pages[currentPageId]?.components ?? {}, // eslint-disable-next-line react-hooks/exhaustive-deps - [JSON.stringify(appDefinition), currentPageId] + [JSON.stringify(appDefinition.pages[currentPageId]?.components), currentPageId] ); - const [boxes, setBoxes] = useState([]); - const [isDragging, setIsDragging] = useState(false); - const [isResizing, setIsResizing] = useState(false); + const [boxes, setBoxes] = useState(() => components); + // const [isDragging, setIsDragging] = useState(false); + // const [isResizing, setIsResizing] = useState(false); const [commentsPreviewList, setCommentsPreviewList] = useState([]); const [newThread, addNewThread] = useState({}); const [isContainerFocused, setContainerFocus] = useState(false); @@ -131,9 +128,7 @@ export const Container = ({ : updatedBoxes[id].layouts.desktop; }); setBoxes({ ...updatedBoxes }); - // console.log('currentLayout', data); } - // setNoOfGrids(currentLayout === 'mobile' ? 12 : 43); }, [currentLayout]); const paramUpdatesOptsRef = useRef({}); @@ -164,7 +159,7 @@ export const Container = ({ console.log('Clipboard API is not available in this browser.'); } } - enableReleasedVersionPopupState(); + useAppVersionStore.getState().actions.enableReleasedVersionPopupState(); }, [isContainerFocused, appDefinition, focusedParentIdRef.current], { scopes: 'editor' } @@ -191,10 +186,13 @@ export const Container = ({ } : updatedBoxes[id].layouts.desktop; }); - console.log(updatedBoxes); setBoxes({ ...updatedBoxes }); } else { - setBoxes(components); + const diffState = diff(components, boxes); + + if (!_.isEmpty(diffState) && !isOnlyLayoutUpdate(diffState)) { + setBoxes(components); + } } // eslint-disable-next-line react-hooks/exhaustive-deps @@ -253,7 +251,10 @@ export const Container = ({ ...appDefinition.pages, [currentPageId]: { ...appDefinition.pages[currentPageId], - components: boxes, + components: { + ...appDefinition.pages[currentPageId].components, + ...boxes, + }, }, }, }; @@ -293,6 +294,8 @@ export const Container = ({ } }); + const isDragging = isGridDragging || draggingState; + const updateCanvasHeight = useCallback( (components) => { const maxHeight = Object.values(components).reduce((max, component) => { @@ -311,18 +314,14 @@ export const Container = ({ [setCanvasHeight, currentLayout, mode] ); - useEffect(() => { - setIsDragging(draggingState); - }, [draggingState]); - - const [, drop] = useDrop( + const [{ isOver, isOverCurrent }, drop] = useDrop( () => ({ - accept: [ItemTypes.BOX, ItemTypes.COMMENT], - async drop(item, monitor) { - // if (item.currentLayout === 'mobile' && item.autoComputeLayout) { - // turnOffAutoLayout(); - // return false; - // } + accept: ItemTypes.BOX, + drop(item, monitor) { + const didDrop = monitor.didDrop(); + if (didDrop) { + return; + } if (item.parent) { return; @@ -356,6 +355,7 @@ export const Container = ({ const componentMeta = _.cloneDeep( componentTypes.find((component) => component.component === item.component.component) ); + const newComponent = addNewWidgetToTheEditor( componentMeta, monitor, @@ -366,6 +366,86 @@ export const Container = ({ zoomLevel ); + // Logic to add default child components + const childrenBoxes = {}; + if (componentMeta.defaultChildren) { + const parentMeta = componentMeta; + const widgetResolvables = Object.freeze({ + Listview: 'listItem', + }); + const customResolverVariable = widgetResolvables[parentMeta?.component]; + const defaultChildren = _.cloneDeep(parentMeta)['defaultChildren']; + const parentId = newComponent.id; + + defaultChildren.forEach((child) => { + const { componentName, layout, incrementWidth, properties, accessorKey, tab, defaultValue, styles } = child; + + const componentMeta = _.cloneDeep( + componentTypes.find((component) => component.component === componentName) + ); + const componentData = JSON.parse(JSON.stringify(componentMeta)); + + const width = layout.width ? layout.width : (componentMeta.defaultSize.width * 100) / noOfGrids; + const height = layout.height ? layout.height : componentMeta.defaultSize.height; + const newComponentDefinition = { + ...componentData.definition.properties, + }; + + if (_.isArray(properties) && properties.length > 0) { + properties.forEach((prop) => { + const accessor = customResolverVariable + ? `{{${customResolverVariable}.${accessorKey}}}` + : defaultValue[prop] || ''; + + _.set(newComponentDefinition, prop, { + value: accessor, + }); + }); + _.set(componentData, 'definition.properties', newComponentDefinition); + } + + if (_.isArray(styles) && styles.length > 0) { + styles.forEach((prop) => { + const accessor = customResolverVariable + ? `{{${customResolverVariable}.${accessorKey}}}` + : defaultValue[prop] || ''; + + _.set(newComponentDefinition, prop, { + value: accessor, + }); + }); + _.set(componentData, 'definition.styles', newComponentDefinition); + } + + const newChildComponent = addNewWidgetToTheEditor( + componentData, + {}, + boxes, + {}, + item.currentLayout, + snapToGrid, + zoomLevel, + true, + true + ); + + _.set(childrenBoxes, newChildComponent.id, { + component: { + ...newChildComponent.component, + parent: parentMeta.component === 'Tabs' ? parentId + '-' + tab : parentId, + }, + + layouts: { + [currentLayout]: { + ...layout, + width: incrementWidth ? width * incrementWidth : width, + height: height, + }, + }, + }); + }); + } + const newBoxes = { ...boxes, [newComponent.id]: { @@ -373,8 +453,8 @@ export const Container = ({ layouts: { ...newComponent.layout, }, - withDefaultChildren: newComponent.withDefaultChildren, }, + ...childrenBoxes, }; setBoxes(newBoxes); @@ -383,6 +463,10 @@ export const Container = ({ return undefined; }, + collect: (monitor) => ({ + isOver: monitor.isOver(), + isOverCurrent: monitor.isOver({ shallow: true }), + }), }), [moveBox] ); @@ -437,14 +521,17 @@ export const Container = ({ }; function onDragStop(boxPositions) { + const copyOfBoxes = JSON.parse(JSON.stringify(boxes)); + const updatedBoxes = boxPositions.reduce((boxesObj, { id, x, y, parent }) => { - let _width = boxes[id]['layouts'][currentLayout].width; - let _height = boxes[id]['layouts'][currentLayout].height; - const containerWidth = parent ? subContainerWidths[parent] : gridWidth; - if (parent !== boxes[id]['component']?.parent) { - if (boxes[id]['component']?.parent) { + let _width = copyOfBoxes[id]['layouts'][currentLayout].width; + let _height = copyOfBoxes[id]['layouts'][currentLayout].height; + const containerWidth = parent ? useGridStore.getState().subContainerWidths[parent] : gridWidth; + if (parent !== copyOfBoxes[id]['component']?.parent) { + if (copyOfBoxes[id]['component']?.parent) { _width = Math.round( - (boxes[id]['layouts'][currentLayout].width * subContainerWidths[boxes[id]['component']?.parent]) / + (copyOfBoxes[id]['layouts'][currentLayout].width * + useGridStore.getState().subContainerWidths[boxes[id]['component']?.parent]) / containerWidth ); } else { @@ -454,7 +541,7 @@ export const Container = ({ if (_width === 0) { _width = 1; } - let _left = Math.round(x / (parent ? subContainerWidths[parent] : gridWidth)); + let _left = Math.round(x / (parent ? useGridStore.getState().subContainerWidths[parent] : gridWidth)); if (_width + _left > noOfGrids) { _left = _left - (_width + _left - noOfGrids); if (_left < 0) { @@ -473,8 +560,8 @@ export const Container = ({ if (parent) { const parentElem = document.getElementById(`canvas-${parent}`); - const parentId = boxes[parent] ? parent : parent?.split('-').slice(0, -1).join('-'); - const compoenentType = boxes[parentId]?.component.component; + const parentId = copyOfBoxes[parent] ? parent : parent?.split('-').slice(0, -1).join('-'); + const compoenentType = copyOfBoxes[parentId]?.component.component; var parentHeight = parentElem?.clientHeight || _height; if (_height > parentHeight && ['Tabs', 'Listview'].includes(compoenentType)) { _height = parentHeight; @@ -482,18 +569,18 @@ export const Container = ({ } } - const componentData = { ...boxes[id]['component'] }; + const componentData = JSON.parse(JSON.stringify(copyOfBoxes[id]['component'])); componentData.parent = parent ? parent : null; return { - ...boxesObj, + ...copyOfBoxes, [id]: { - ...boxes[id], + ...copyOfBoxes[id], component: componentData, layouts: { - ...boxes[id]['layouts'], + ...copyOfBoxes[id]['layouts'], [currentLayout]: { - ...boxes[id]['layouts'][currentLayout], + ...copyOfBoxes[id]['layouts'][currentLayout], width: _width, height: _height, top: y, @@ -504,10 +591,29 @@ export const Container = ({ }; }, {}); let newBoxes = { - ...boxes, + ...copyOfBoxes, ...updatedBoxes, }; - setBoxes(newBoxes); + + handleLowPriorityWork(() => { + const diffState = diff(boxes, newBoxes); + + setBoxes((prev) => { + const updatedComponentsAsperDiff = Object.keys(diffState).reduce((acc, key) => { + const component = newBoxes[key]; + if (component) { + acc[key] = component; + } + return acc; + }, {}); + + return { + ...prev, + ...updatedComponentsAsperDiff, + }; + }); + }); + updateCanvasHeight(newBoxes); } @@ -648,6 +754,29 @@ export const Container = ({ return componentWithChildren; }, [components]); + const getContainerProps = React.useCallback((componentId) => { + return { + mode, + snapToGrid, + onComponentClick, + onEvent, + appDefinition, + appDefinitionChanged, + currentState, + appLoading, + zoomLevel, + setSelectedComponent, + removeComponent, + currentLayout, + selectedComponents, + darkMode, + currentPageId, + childComponents: childComponents[componentId], + parentGridWidth: gridWidth, + draggedSubContainer, + }; + }, []); + return ( {config.COMMENT_FEATURE_ENABLE && showComments && ( @@ -690,14 +818,13 @@ export const Container = ({
{Object.entries({ ...boxes, - ...(resizingComponentId && - boxes[resizingComponentId] && { resizingComponentId: boxes[resizingComponentId] }), }) .filter(([, box]) => isEmpty(box?.component?.parent)) .map(([id, box]) => { const canShowInCurrentLayout = box.component.definition.others[currentLayout === 'mobile' ? 'showOnMobile' : 'showOnDesktop'].value; - if (box.parent || !resolveReferences(canShowInCurrentLayout, currentState)) { + + if (box.parent || !resolveWidgetFieldValue(canShowInCurrentLayout)) { return ''; } return ( @@ -717,78 +844,41 @@ export const Container = ({ config.COMMENT_FEATURE_ENABLE && showComments ? handleAddThreadOnComponent : onComponentClick } onEvent={onEvent} - // height={height} - onComponentOptionChanged={onComponentOptionChanged} - onComponentOptionsChanged={onComponentOptionsChanged} key={id} paramUpdated={paramUpdated} id={id} {...box} mode={mode} - resizingStatusChanged={(status) => setIsResizing(status)} - draggingStatusChanged={(status) => setIsDragging(status)} inCanvas={true} zoomLevel={zoomLevel} - setSelectedComponent={setSelectedComponent} removeComponent={removeComponent} - deviceWindowWidth={deviceWindowWidth} isSelectedComponent={ mode === 'edit' ? selectedComponents.find((component) => component.id === id) : false } darkMode={darkMode} - // onComponentHover={onComponentHover} - // hoveredComponent={hoveredComponent} - sideBarDebugger={sideBarDebugger} isMultipleComponentsSelected={selectedComponents?.length > 1 ? true : false} - childComponents={childComponents[id]} - containerProps={{ - // turnOffAutoLayout, - mode, - snapToGrid, - onComponentClick, - onEvent, - appDefinition, - appDefinitionChanged, - currentState, - onComponentOptionChanged, - onComponentOptionsChanged, - appLoading, - zoomLevel, - setSelectedComponent, - removeComponent, - currentLayout, - deviceWindowWidth, - selectedComponents, - darkMode, - // onComponentHover, - // hoveredComponent, - sideBarDebugger, - addDefaultChildren: box.withDefaultChildren, - currentPageId, - childComponents, - // setIsChildDragged, - setSubContainerWidths: (id, width) => - setSubContainerWidths((widths) => ({ ...widths, [id]: width })), - parentGridWidth: gridWidth, - subContainerWidths, - draggedSubContainer, - }} + getContainerProps={getContainerProps} isVersionReleased={isVersionReleased} + currentPageId={currentPageId} /> ); })} - + + { const isSelected = !!(state.selectedComponents || []).find((selected) => selected?.id === id); - console.log('state.selectedComponents--', state.selectedComponents, id, isSelected); const isHovered = state?.hoveredComponent == id; return { isSelected, isHovered }; }, shallow); @@ -845,13 +934,10 @@ const WidgetWrapper = ({ children, widget, id, gridWidth, currentLayout, isResiz width: width + 'px', height: layoutData.height + 'px', transform: `translate(${layoutData.left * gridWidth}px, ${layoutData.top}px)`, - // ...(isGhostComponent ? { opacity: 0.5 } : isResizing ? { opacity: 0 } : {}), ...(isGhostComponent ? { opacity: 0.5 } : {}), ...(isWidgetActive ? { zIndex: 3 } : {}), }; - console.log('state.selectedComponents--', isWidgetActive, id, isSelected, isDragging); - return ( <>
state?.draggingComponentId); if (!draggingComponentId) return ''; return ( @@ -892,20 +978,10 @@ function GhostWidget() { ); } -function ContainerWrapper({ - children, - canvasHeight, - isDragging, - isResizing, - showComments, - handleAddThread, - containerRef, - styles, -}) { - // const [dragTarget] = useDragTarget(); - const { resizingComponentId, draggingComponentId } = useGridStore((state) => { - const { resizingComponentId, draggingComponentId } = state; - return { resizingComponentId, draggingComponentId }; +function ContainerWrapper({ children, canvasHeight, isDropping, showComments, handleAddThread, containerRef, styles }) { + const { resizingComponentId, draggingComponentId, dragTarget } = useGridStore((state) => { + const { resizingComponentId, draggingComponentId, dragTarget } = state; + return { resizingComponentId, draggingComponentId, dragTarget }; }, shallow); return ( @@ -914,8 +990,7 @@ function ContainerWrapper({ ref={containerRef} style={{ ...styles, height: canvasHeight }} className={cx('real-canvas', { - // 'show-grid': isDragging || isResizing || dragTarget === 'canvas', - 'show-grid': isDragging || isResizing || !!resizingComponentId || !!draggingComponentId, + 'show-grid': (!!resizingComponentId && !dragTarget) || (!!draggingComponentId && !dragTarget) || isDropping, })} id="real-canvas" data-cy="real-canvas" @@ -925,3 +1000,19 @@ function ContainerWrapper({
); } + +const ResizeGhostWidget = ({ resizingComponentId, widgets, currentLayout, canvasWidth, gridWidth }) => { + const dragTarget = useGridStore((state) => state.dragTarget); + if (!resizingComponentId || dragTarget) { + return ''; + } + + return ( + + ); +}; diff --git a/frontend/src/Editor/ControlledComponentToRender.jsx b/frontend/src/Editor/ControlledComponentToRender.jsx new file mode 100644 index 0000000000..a13b27252d --- /dev/null +++ b/frontend/src/Editor/ControlledComponentToRender.jsx @@ -0,0 +1,41 @@ +import React from 'react'; +import { getComponentToRender } from '@/_helpers/editorHelpers'; +import _ from 'lodash'; + +import { getComponentsToRenders } from '@/_stores/editorStore'; + +function deepEqualityCheckusingLoDash(obj1, obj2) { + return _.isEqual(obj1, obj2); +} + +export const shouldUpdate = (prevProps, nextProps) => { + const listToRender = getComponentsToRenders(); + + let needToRender = false; + + const componentId = prevProps?.id === nextProps?.id ? prevProps?.id : null; + + if (componentId) { + const componentToRender = listToRender.find((item) => item === componentId); + + if (componentToRender) { + needToRender = true; + } + } + + return ( + deepEqualityCheckusingLoDash(prevProps?.id, nextProps?.id) && + deepEqualityCheckusingLoDash(prevProps?.component?.definition, nextProps?.component?.definition) && + prevProps?.width === nextProps?.width && + prevProps?.height === nextProps?.height && + !needToRender + ); +}; + +const ComponentWrapper = React.memo(({ componentName, ...props }) => { + const ComponentToRender = getComponentToRender(componentName); + + return ; +}, shouldUpdate); + +export default ComponentWrapper; diff --git a/frontend/src/Editor/CustomDragLayer.jsx b/frontend/src/Editor/CustomDragLayer.jsx index b38d1cbcc5..554a7741db 100644 --- a/frontend/src/Editor/CustomDragLayer.jsx +++ b/frontend/src/Editor/CustomDragLayer.jsx @@ -1,6 +1,6 @@ import React, { useEffect } from 'react'; import { useDragLayer } from 'react-dnd'; -import { ItemTypes } from './ItemTypes'; +import { ItemTypes } from './editorConstants'; import { BoxDragPreview } from './BoxDragPreview'; import { snapToGrid } from '@/_helpers/appUtils'; import { useEditorStore } from '@/_stores/editorStore'; diff --git a/frontend/src/Editor/DragContainer.jsx b/frontend/src/Editor/DragContainer.jsx index dbb37fd12f..8355f0a678 100644 --- a/frontend/src/Editor/DragContainer.jsx +++ b/frontend/src/Editor/DragContainer.jsx @@ -1,4 +1,5 @@ -import React, { useEffect, useState, useRef } from 'react'; +// import '@/Editor/wdyr'; +import React, { useEffect, useState, useRef, useCallback } from 'react'; import Moveable from 'react-moveable'; import { useEditorStore } from '@/_stores/editorStore'; import { shallow } from 'zustand/shallow'; @@ -8,6 +9,13 @@ import { flushSync } from 'react-dom'; import { restrictedWidgetsObj } from './WidgetManager/restrictedWidgetsConfig'; import { useGridStore, useIsGroupHandleHoverd, useOpenModalWidgetId } from '@/_stores/gridStore'; import toast from 'react-hot-toast'; +import { individualGroupableProps } from './gridUtils'; + +const CANVAS_BOUNDS = { left: 0, top: 0, right: 0, bottom: 0, position: 'css' }; +const RESIZABLE_CONFIG = { + edge: ['nw', 'n', 'ne', 'w', 'e', 'sw', 's', 'se'], + renderDirections: ['nw', 'n', 'ne', 'w', 'e', 'sw', 's', 'se'], +}; export default function DragContainer({ widgets, @@ -16,10 +24,7 @@ export default function DragContainer({ onDrag, gridWidth, selectedComponents = [], - setIsDragging, - setIsResizing, currentLayout, - subContainerWidths, draggedSubContainer, }) { const lastDraggedEventsRef = useRef(null); @@ -34,6 +39,8 @@ export default function DragContainer({ if (lastDraggedEventsRef.current) { const preant = boxes.find((box) => box.id == lastDraggedEventsRef.current.events[0].target.id)?.component ?.parent; + // Adding the new updates to the macro task queue to unblock UI + onDrag( lastDraggedEventsRef.current.events.map((ev) => ({ id: ev.target.id, @@ -101,8 +108,6 @@ export default function DragContainer({ }, }; - // const [dragTarget, useGridStore.getState().actions.setDragTarget] = useDragTarget(); - const [draggedTarget, setDraggedTarget] = useState(); const moveableRef = useRef(); const draggedOverElemRef = useRef(null); const childMoveableRefs = useRef({}); @@ -159,8 +164,7 @@ export default function DragContainer({ } catch (error) { console.error('Error---->', error); } - }, [JSON.stringify(selectedComponents), JSON.stringify(boxes), hoveredComponent]); - // }, [JSON.stringify(selectedComponents), JSON.stringify(boxes), hoveredComponent]); + }, [hoveredComponent, reloadGrid]); useEffect(() => { setList(boxList); @@ -184,7 +188,7 @@ export default function DragContainer({ } }, [openModalWidgetId, selectedComponents]); - const reloadGrid = async () => { + const reloadGrid = useCallback(async () => { if (moveableRef.current) { moveableRef.current.updateRect(); moveableRef.current.updateTarget(); @@ -220,23 +224,19 @@ export default function DragContainer({ } } } - }; - - window.reloadGrid = reloadGrid; + }, [selectedComponents]); useEffect(() => { setList(boxList); }, [JSON.stringify(boxes)]); const groupedTargets = [ - ...findHighestLevelofSelection(selectedComponents) - // .filter((component) => !component?.component?.parent) - .map((component) => '.ele-' + component.id), + ...findHighestLevelofSelection(selectedComponents).map((component) => '.ele-' + component.id), ]; useEffect(() => { reloadGrid(); - }, [selectedComponents, openModalWidgetId]); + }, [selectedComponents, openModalWidgetId, widgets]); const updateNewPosition = (events, parent = null) => { const posWithParent = { @@ -262,24 +262,16 @@ export default function DragContainer({ origin={false} individualGroupable={groupedTargets.length <= 1} draggable={true} - resizable={{ - edge: ['nw', 'n', 'ne', 'w', 'e', 'sw', 's', 'se'], - renderDirections: ['nw', 'n', 'ne', 'w', 'e', 'sw', 's', 'se'], - }} + resizable={RESIZABLE_CONFIG} keepRatio={false} // key={list.length} - individualGroupableProps={(element) => { - if (element?.classList.contains('target2')) { - return { - resizable: false, - }; - } - }} + individualGroupableProps={individualGroupableProps} onResize={(e) => { const currentLayout = list.find(({ id }) => id === e.target.id); const currentWidget = boxes.find(({ id }) => id === e.target.id); - let _gridWidth = subContainerWidths[currentWidget.component?.parent] || gridWidth; + let _gridWidth = useGridStore.getState().subContainerWidths[currentWidget.component?.parent] || gridWidth; document.getElementById('canvas-' + currentWidget.component?.parent)?.classList.add('show-grid'); + useGridStore.getState().actions.setDragTarget(currentWidget.component?.parent); const currentWidth = currentLayout.width * _gridWidth; const diffWidth = e.width - currentWidth; const diffHeight = e.height - currentLayout.height; @@ -316,12 +308,12 @@ export default function DragContainer({ onResizeEnd={(e) => { try { useGridStore.getState().actions.setResizingComponentId(null); - setIsResizing(false); + // setIsResizing(false); const currentWidget = boxes.find(({ id }) => { return id === e.target.id; }); document.getElementById('canvas-' + currentWidget.component?.parent)?.classList.remove('show-grid'); - let _gridWidth = subContainerWidths[currentWidget.component?.parent] || gridWidth; + let _gridWidth = useGridStore.getState().subContainerWidths[currentWidget.component?.parent] || gridWidth; let width = Math.round(e.lastEvent.width / _gridWidth) * _gridWidth; const height = Math.round(e.lastEvent.height / 10) * 10; @@ -369,19 +361,19 @@ export default function DragContainer({ if (currentWidget.component?.parent) { resizeData.gw = _gridWidth; } + // Adding the new updates to the macro task queue to unblock UI + // setTimeout(() => { + // }); onResizeStop([resizeData]); } catch (error) { console.error('ResizeEnd error ->', error); } + useGridStore.getState().actions.setDragTarget(); }} onResizeStart={(e) => { + performance.mark('onResizeStart'); useGridStore.getState().actions.setResizingComponentId(e.target.id); - setIsResizing(true); e.setMin([gridWidth, 10]); - // if (currentLayout === 'mobile' && autoComputeLayout) { - // turnOffAutoLayout(); - // return false; - // } }} onResizeGroupStart={({ events }) => { const parentElm = events[0].target.closest('.real-canvas'); @@ -416,7 +408,7 @@ export default function DragContainer({ const currentWidget = boxes.find(({ id }) => { return id === ev.target.id; }); - let _gridWidth = subContainerWidths[currentWidget.component?.parent] || gridWidth; + let _gridWidth = useGridStore.getState().subContainerWidths[currentWidget.component?.parent] || gridWidth; let width = Math.round(ev.width / _gridWidth) * _gridWidth; width = width < _gridWidth ? _gridWidth : width; let posX = Math.round(ev.drag.translate[0] / _gridWidth) * _gridWidth; @@ -438,13 +430,17 @@ export default function DragContainer({ }); if (groupResizeDataRef.current.length) { - onResizeStop(newBoxs); + // Adding the new updates to the macro task queue to unblock UI + // setTimeout(() => { + // }); + onResizeStop([newBoxs]); } else { events.forEach((ev) => { const currentWidget = boxes.find(({ id }) => { return id === ev.target.id; }); - let _gridWidth = subContainerWidths[currentWidget.component?.parent] || gridWidth; + let _gridWidth = + useGridStore.getState().subContainerWidths[currentWidget.component?.parent] || gridWidth; let width = currentWidget?.layouts[currentLayout].width * _gridWidth; let posX = currentWidget?.layouts[currentLayout].left * _gridWidth; let posY = currentWidget?.layouts[currentLayout].top; @@ -486,17 +482,14 @@ export default function DragContainer({ if (hoveredComponent !== e.target.id) { return false; } - setDraggedTarget(e.target.id); }} onDragEnd={(e) => { try { if (isDraggingRef.current) { useGridStore.getState().actions.setDraggingComponentId(null); isDraggingRef.current = false; - setIsDragging(false); } - setDraggedTarget(); if (draggedSubContainer) { return; } @@ -534,7 +527,7 @@ export default function DragContainer({ draggedOverElemIdType = draggedOverElem?.getAttribute('data-parent-type'); } - const _gridWidth = subContainerWidths[draggedOverElemId] || gridWidth; + const _gridWidth = useGridStore.getState().subContainerWidths[draggedOverElemId] || gridWidth; const currentParentId = boxes.find(({ id: widgetId }) => e.target.id === widgetId)?.component?.parent; let left = e.lastEvent.translate[0]; let top = e.lastEvent.translate[1]; @@ -568,13 +561,16 @@ export default function DragContainer({ toast.error(`${currentWidget} is not compatible as a child component of ${parentWidget}`); e.target.style.transform = `translate(${left}px, ${top}px)`; } - } else { - e.target.style.transform = `translate(${Math.round(left / _gridWidth) * _gridWidth}px, ${ - Math.round(top / 10) * 10 - }px)`; } + e.target.style.transform = `translate(${Math.round(left / _gridWidth) * _gridWidth}px, ${ + Math.round(top / 10) * 10 + }px)`; + if (draggedOverElemId === currentParentId || isParentChangeAllowed) { + // Adding the new updates to the macro task queue to unblock UI + // setTimeout(() => + // ); onDrag([ { id: e.target.id, @@ -585,7 +581,7 @@ export default function DragContainer({ ]); } const box = boxes.find((box) => box.id === e.target.id); - useEditorStore.getState().actions.setSelectedComponents([{ ...box }]); + setTimeout(() => useEditorStore.getState().actions.setSelectedComponents([{ ...box }])); } catch (error) { console.log('draggedOverElemId->error', error); } @@ -600,14 +596,11 @@ export default function DragContainer({ if (!isDraggingRef.current) { useGridStore.getState().actions.setDraggingComponentId(e.target.id); isDraggingRef.current = true; - setIsDragging(true); } if (draggedSubContainer) { return; } - if (e.target.id !== draggedTarget) { - setDraggedTarget(e.target.id); - } + if (!draggedSubContainer) { const parentComponent = widgets[widgets[e.target.id]?.component?.parent]; let top = e.translate[1]; @@ -650,6 +643,8 @@ export default function DragContainer({ const parentWidgetId = draggedOverContainer.getAttribute('data-parent') || draggedOverElem?.id; document.getElementById('canvas-' + parentWidgetId)?.classList.add('show-grid'); + useGridStore.getState().actions.setDragTarget(parentWidgetId); + if ( draggedOverElemRef.current?.id !== draggedOverContainer?.id && !draggedOverContainer.classList.contains('hide-grid') @@ -659,7 +654,7 @@ export default function DragContainer({ draggedOverElemRef.current = draggedOverContainer; } } - console.log('getOffset--', getOffset(e.target, document.querySelector('#real-canvas'))); + const offset = getOffset(e.target, document.querySelector('#real-canvas')); if (document.getElementById('moveable-drag-ghost')) { document.getElementById('moveable-drag-ghost').style.transform = `translate(${offset.x}px, ${offset.y}px)`; @@ -699,6 +694,7 @@ export default function DragContainer({ const { posRight, posLeft, posTop, posBottom } = getPositionForGroupDrag(events, parentWidth, parentHeight); + // Adding the new updates to the macro task queue to unblock UI onDrag( events.map((ev) => { let posX = ev.lastEvent.translate[0]; @@ -731,7 +727,7 @@ export default function DragContainer({ snappable={true} snapThreshold={10} isDisplaySnapDigit={false} - bounds={{ left: 0, top: 0, right: 0, bottom: 0, position: 'css' }} + bounds={CANVAS_BOUNDS} displayAroundControls={true} controlPadding={20} /> diff --git a/frontend/src/Editor/DraggableBox.jsx b/frontend/src/Editor/DraggableBox.jsx index 7121ae4713..d51387fa8c 100644 --- a/frontend/src/Editor/DraggableBox.jsx +++ b/frontend/src/Editor/DraggableBox.jsx @@ -2,11 +2,11 @@ import React, { useCallback, useEffect, useState } from 'react'; import cx from 'classnames'; import { useDrag } from 'react-dnd'; -import { ItemTypes } from './ItemTypes'; +import { ItemTypes } from './editorConstants'; import { getEmptyImage } from 'react-dnd-html5-backend'; import { Box } from './Box'; import { ConfigHandle } from './ConfigHandle'; -import { resolveWidgetFieldValue, resolveReferences } from '@/_helpers/utils'; +import { resolveWidgetFieldValue } from '@/_helpers/utils'; import ErrorBoundary from './ErrorBoundary'; import { useCurrentState } from '@/_stores/currentStateStore'; import { useEditorStore } from '@/_stores/editorStore'; @@ -16,8 +16,6 @@ import WidgetBox from './WidgetBox'; import * as Sentry from '@sentry/react'; import { findHighestLevelofSelection } from './DragContainer'; -// const noOfGrid = 43; - function computeWidth(currentLayoutOptions) { return `${currentLayoutOptions?.width}%`; } @@ -32,45 +30,36 @@ function getStyles(isDragging, isSelectedComponent) { }; } -export const DraggableBox = React.memo( +const DraggableBox = React.memo( ({ id, className, mode, title, parent, - allComponents, component, index, inCanvas, onEvent, onComponentClick, - onComponentOptionChanged, - onComponentOptionsChanged, paramUpdated, - resizingStatusChanged, zoomLevel, - containerProps, - setSelectedComponent, removeComponent, layouts, - draggingStatusChanged, darkMode, canvasWidth, readOnly, customResolvables, parentId, - sideBarDebugger, - childComponents = null, + getContainerProps, + currentPageId, }) => { - const [isResizing, setResizing] = useState(false); - const [isDragging2, setDragging] = useState(false); + const isResizing = useGridStore((state) => state.resizingComponentId === id); const [canDrag, setCanDrag] = useState(true); const noOfGrid = useNoOfGrid(); const { currentLayout, setHoveredComponent, - mouseOver, selectionInProgress, isSelectedComponent, isMultipleComponentsSelected, @@ -79,7 +68,6 @@ export const DraggableBox = React.memo( (state) => ({ currentLayout: state?.currentLayout, setHoveredComponent: state?.actions?.setHoveredComponent, - mouseOver: state?.hoveredComponent === id, selectionInProgress: state?.selectionInProgress, isSelectedComponent: mode === 'edit' ? state?.selectedComponents?.some((component) => component?.id === id) : false, @@ -89,7 +77,6 @@ export const DraggableBox = React.memo( shallow ); const currentState = useCurrentState(); - const [boxHeight, setboxHeight] = useState(layoutData?.height); // height for layouting with top and side values const [{ isDragging }, drag, preview] = useDrag( () => ({ @@ -116,22 +103,6 @@ export const DraggableBox = React.memo( preview(getEmptyImage(), { captureDraggingState: true }); }, [isDragging]); - useEffect(() => { - if (resizingStatusChanged) { - resizingStatusChanged(isResizing); - } - }, [isResizing]); - - useEffect(() => { - if (draggingStatusChanged) { - draggingStatusChanged(isDragging2); - } - - if (isDragging2 && !isSelectedComponent) { - setSelectedComponent(id, component); - } - }, [isDragging2]); - let _refProps = {}; if (mode === 'edit' && canDrag) { @@ -153,51 +124,23 @@ export const DraggableBox = React.memo( width: 43, height: 500, }; - // const layoutData = inCanvas ? layouts[currentLayout] || defaultData : defaultData; + const layoutData = inCanvas ? layouts[currentLayout] || layouts['desktop'] : defaultData; - console.log('layoutData--', layoutData, currentLayout, layouts); const width = (canvasWidth * layoutData.width) / noOfGrid; const configWidgetHandlerForModalComponent = !isSelectedComponent && component.component === 'Modal' && - resolveWidgetFieldValue(component.definition.properties.useDefaultButton, currentState)?.value === false; + resolveWidgetFieldValue(component.definition.properties.useDefaultButton?.value) === false; - const onComponentHover = (id) => { - if (selectionInProgress) return; - setHoveredComponent(id); - }; + const onComponentHover = useCallback( + (id) => { + if (selectionInProgress) return; + setHoveredComponent(id); + }, + [id] + ); - const { label = { value: null } } = component?.definition?.properties ?? {}; - - useEffect(() => { - if ( - component.component == 'TextInput' || - component.component == 'PasswordInput' || - component.component == 'NumberInput' - ) { - const { alignment = { value: null } } = component?.definition?.styles ?? {}; - let newHeight = layoutData?.height; - if (alignment?.value && resolveReferences(alignment?.value, currentState, null, customResolvables) === 'top') { - const { width = { value: null } } = component?.definition?.styles ?? {}; - const { auto = { value: null } } = component?.definition?.styles ?? {}; - const resolvedWidth = resolveReferences(width?.value, currentState, null, customResolvables); - const resolvedAuto = resolveReferences(auto?.value, currentState, null, customResolvables); - if ( - (label?.value?.length > 0 && resolvedWidth > 0) || - (resolvedAuto && resolvedWidth == 0 && label?.value && label?.value?.length != 0) - ) { - newHeight = layoutData?.height + 20; - } - } - setboxHeight(newHeight); - } - }, [layoutData?.height, label?.value?.length, currentLayout]); - - const adjustHeightBasedOnAlignment = (increase) => { - if (increase) return setboxHeight(layoutData?.height + 20); - else return setboxHeight(layoutData?.height); - }; return (
- {mode === 'edit' && !readOnly && !isResizing && ( + {mode === 'edit' && !readOnly && ( )}
- {/* */}
) : (
@@ -303,3 +237,5 @@ export const DraggableBox = React.memo( ); } ); + +export { DraggableBox }; diff --git a/frontend/src/Editor/Editor.jsx b/frontend/src/Editor/Editor.jsx index 80102bf106..25d087a3aa 100644 --- a/frontend/src/Editor/Editor.jsx +++ b/frontend/src/Editor/Editor.jsx @@ -1,4 +1,4 @@ -import React, { useEffect, useLayoutEffect, useRef, useState } from 'react'; +import React, { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react'; import { appService, authenticationService, @@ -25,7 +25,6 @@ import { onQueryConfirmOrCancel, runQuery, computeComponentState, - debuggerActions, cloneComponents, removeSelectedComponent, buildAppDefinition, @@ -60,7 +59,7 @@ import { withRouter } from '@/_hoc/withRouter'; import { ReleasedVersionError } from './AppVersionsManager/ReleasedVersionError'; import { useDataSourcesStore } from '@/_stores/dataSourcesStore'; import { useDataQueriesStore } from '@/_stores/dataQueriesStore'; -import { useAppVersionStore, useAppVersionActions, useAppVersionState } from '@/_stores/appVersionStore'; +import { useAppVersionStore, useAppVersionActions } from '@/_stores/appVersionStore'; import { useQueryPanelStore } from '@/_stores/queryPanelStore'; import { useCurrentStateStore, useCurrentState, getCurrentState } from '@/_stores/currentStateStore'; import { @@ -71,8 +70,8 @@ import { resetAllStores, } from '@/_stores/utils'; import { setCookie } from '@/_helpers/cookie'; -import { EMPTY_ARRAY, useEditorActions, useEditorStore } from '@/_stores/editorStore'; -import { useAppDataActions, useAppInfo, useAppDataStore } from '@/_stores/appDataStore'; +import { EMPTY_ARRAY, flushComponentsToRender, useEditorActions, useEditorStore } from '@/_stores/editorStore'; +import { useAppDataActions, useAppDataStore } from '@/_stores/appDataStore'; import { useNoOfGrid } from '@/_stores/gridStore'; import { useMounted } from '@/_hooks/use-mount'; import EditorSelecto from './EditorSelecto'; @@ -89,12 +88,12 @@ import AutoLayoutAlert from './AutoLayoutAlert'; import { HotkeysProvider } from 'react-hotkeys-hook'; import { useResolveStore } from '@/_stores/resolverStore'; import { dfs } from '@/_stores/handleReferenceTransactions'; +import { decimalToHex } from './editorConstants'; +import { findComponentsWithReferences, handleLowPriorityWork } from '@/_helpers/editorHelpers'; setAutoFreeze(false); enablePatches(); -const decimalToHex = (alpha) => (alpha === 0 ? '00' : Math.round(255 * alpha).toString(16)); - const EditorComponent = (props) => { const { socket } = createWebsocketConnection(props?.params?.id); const isSocketOpen = useSocketOpen(socket); @@ -109,8 +108,13 @@ const EditorComponent = (props) => { autoUpdateEventStore, } = useAppDataActions(); - const { updateEditorState, updateQueryConfirmationList, setSelectedComponents, setCurrentPageId } = - useEditorActions(); + const { + updateEditorState, + updateQueryConfirmationList, + setSelectedComponents, + setCurrentPageId, + updateComponentsNeedsUpdateOnNextRender, + } = useEditorActions(); const { setAppVersions } = useAppVersionActions(); const { isVersionReleased, editingVersionId, releasedVersionId } = useAppVersionStore( @@ -208,7 +212,8 @@ const EditorComponent = (props) => { const selectionRef = useRef(null); const prevAppDefinition = useRef(appDefinition); - const prevEventsStoreRef = useRef(events); + + const onAppLoadAndPageLoadEventsAreTriggered = useRef(false); useLayoutEffect(() => { resetAllStores(); @@ -277,19 +282,62 @@ const EditorComponent = (props) => { } if (mounted && didAppDefinitionChanged && currentPageId) { - const components = appDefinition?.pages[currentPageId]?.components || {}; + // const components = appDefinition?.pages[currentPageId]?.components || {}; - computeComponentState(components); + // computeComponentState(components); if (appDiffOptions?.skipAutoSave === true || appDiffOptions?.entityReferenceUpdated === true) return; - if (useEditorStore.getState().isUpdatingEditorStateInProcess) { - autoSave(); - } + handleLowPriorityWork(() => autoSave()); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [JSON.stringify({ appDefinition, currentPageId, dataQueries })]); + /** + ** Async updates components in batches to optimize and processing efficiency. + * This function iterates over an array of component IDs, updating them in fixed-size batches, + * and introduces a delay after each batch to allow the UI thread to manage other tasks, such as rendering updates. + * After all batches are processed, it flushes the updates to clear any flags or temporary states indicating pending updates, + * ensuring the system is ready for the next cycle of updates. + * + * @param {Array} componentIds An array of component IDs that need updates. + * @returns {Promise} A promise that resolves once all batches have been processed and flushed. + */ + + async function batchUpdateComponents(componentIds) { + if (componentIds.length === 0) return; + + let updatedComponentIds = []; + + for (let i = 0; i < componentIds.length; i += 10) { + const batch = componentIds.slice(i, i + 10); + batch.forEach((id) => { + updatedComponentIds.push(id); + }); + + updateComponentsNeedsUpdateOnNextRender(batch); + // Delay to allow UI to process + await new Promise((resolve) => setTimeout(resolve, 0)); + } + + // Flush only updated components + flushComponentsToRender(updatedComponentIds); + } + + const lastUpdatedRef = useResolveStore((state) => state.lastUpdatedRefs, shallow); + + useEffect(() => { + if (lastUpdatedRef.length > 0) { + const currentComponents = useEditorStore.getState().appDefinition?.pages?.[currentPageId]?.components || {}; + const componentIdsWithReferences = findComponentsWithReferences(currentComponents, lastUpdatedRef); + + if (componentIdsWithReferences.length > 0) { + batchUpdateComponents(componentIdsWithReferences); + } + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [lastUpdatedRef]); + useEffect( () => { const components = appDefinition?.pages?.[currentPageId]?.components || {}; @@ -326,14 +374,6 @@ const EditorComponent = (props) => { } }, [currentLayout, mounted]); - const handleYmapEventUpdates = () => { - props.ymap?.set('eventHandlersUpdated', { - currentVersionId: currentVersionId, - currentSessionId: currentSessionId, - update: true, - }); - }; - const handleMessage = (event) => { const { data } = event; @@ -463,11 +503,6 @@ const EditorComponent = (props) => { socket?.addEventListener('message', (event) => { const data = event.data.replace(/^"(.+(?="$))"$/, '$1'); if (data === 'versionReleased') fetchApp(); - // else if (data === 'dataQueriesChanged') { - // fetchDataQueries(editingVersion?.id); - // } else if (data === 'dataSourcesChanged') { - // fetchDataSources(editingVersion?.id); - // } }); }; @@ -587,30 +622,6 @@ const EditorComponent = (props) => { const handleQueryPaneDragging = (bool) => setIsQueryPaneDragging(bool); const handleQueryPaneExpanding = (bool) => setIsQueryPaneExpanded(bool); - const handleOnComponentOptionChanged = (component, optionName, value) => { - return onComponentOptionChanged(component, optionName, value); - }; - - const handleOnComponentOptionsChanged = (component, options) => { - return onComponentOptionsChanged(component, options); - }; - - const handleComponentClick = (id, component) => { - updateEditorState({ - selectedComponent: { id, component }, - }); - }; - - const sideBarDebugger = { - error: (data) => { - debuggerActions.error(data); - }, - flush: () => { - debuggerActions.flush(); - }, - generateErrorLogs: (errors) => debuggerActions.generateErrorLogs(errors), - }; - const changeDarkMode = (newMode) => { useCurrentStateStore.getState().actions.setCurrentState({ globals: { @@ -621,9 +632,10 @@ const EditorComponent = (props) => { props.switchDarkMode(newMode); }; - const handleEvent = (eventName, event, options) => { + const handleEvent = React.useCallback((eventName, event, options) => { return onEvent(getEditorRef(), eventName, event, options, 'edit'); - }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); const handleRunQuery = (queryId, queryName) => runQuery(getEditorRef(), queryId, queryName); @@ -631,13 +643,13 @@ const EditorComponent = (props) => { dataSourceModalRef.current.dataSourceModalToggleStateHandler(); }; - const setSelectedComponent = (id, component, multiSelect = false) => { + const setSelectedComponent = React.useCallback((id, component, multiSelect = false) => { const isAlreadySelected = useEditorStore.getState()?.selectedComponents.find((component) => component.id === id); if (!isAlreadySelected) { setSelectedComponents([{ id, component }], multiSelect); } - }; + }, []); const onVersionRelease = (versionId) => { useAppVersionStore.getState().actions.updateReleasedVersionId(versionId); @@ -661,7 +673,6 @@ const EditorComponent = (props) => { }; const getPagesWithIds = () => { - //! Needs attention return Object.entries(appDefinition?.pages).map(([id, page]) => ({ ...page, id })); }; @@ -732,13 +743,12 @@ const EditorComponent = (props) => { useCurrentStateStore.getState().actions.setCurrentState({ page: currentpageData, - }); - - updateEditorState({ - isLoading: false, - appDefinition: appJson, - isUpdatingEditorStateInProcess: false, - }); + }), + updateEditorState({ + isLoading: false, + appDefinition: appJson, + isUpdatingEditorStateInProcess: false, + }); updateState({ components: appJson.pages[homePageId]?.components }); @@ -782,17 +792,29 @@ const EditorComponent = (props) => { let dataQueries = JSON.parse(JSON.stringify(useDataQueriesStore.getState().dataQueries)); let allEvents = JSON.parse(JSON.stringify(useAppDataStore.getState().events)); - const entityReferencesInComponentDefinitions = findAllEntityReferences(currentComponents, [])?.filter( - (entity) => entity && isValidUUID(entity) - ); + const entityReferencesInComponentDefinitions = findAllEntityReferences(currentComponents, []) + ?.map((entity) => { + if (entity && isValidUUID(entity)) { + return entity; + } + }) + ?.filter((e) => e !== undefined); - const entityReferencesInQueryoOptions = findAllEntityReferences(dataQueries, [])?.filter( - (entity) => entity && isValidUUID(entity) - ); + const entityReferencesInQueryoOptions = findAllEntityReferences(dataQueries, []) + ?.map((entity) => { + if (entity && isValidUUID(entity)) { + return entity; + } + }) + ?.filter((e) => e !== undefined); - const entityReferencesInEvents = findAllEntityReferences(allEvents, [])?.filter( - (entity) => entity && isValidUUID(entity) - ); + const entityReferencesInEvents = findAllEntityReferences(allEvents, []) + ?.map((entity) => { + if (entity && isValidUUID(entity)) { + return entity; + } + }) + ?.filter((e) => e !== undefined); const manager = useResolveStore.getState().referenceMapper; @@ -872,13 +894,13 @@ const EditorComponent = (props) => { (event) => event.target === 'page' && event.sourceId === homePageId ); - const queuedQueriesForRunOnAppLoad = useDataQueriesStore.getState().queuedQueriesForRunOnAppLoad; const editorRef = getEditorRef(); - await runQueries(queuedQueriesForRunOnAppLoad, editorRef); - await handleEvent('onPageLoad', currentPageEvents, {}, true); - - useDataQueriesStore.getState().actions.clearQueuedQueriesForRunOnAppLoad(); + handleLowPriorityWork(async () => { + await runQueries(useDataQueriesStore.getState().dataQueries, editorRef, true); + await handleEvent('onPageLoad', currentPageEvents, {}, true); + await handleLowPriorityWork(() => (onAppLoadAndPageLoadEventsAreTriggered.current = true)); + }); }); }; @@ -918,7 +940,7 @@ const EditorComponent = (props) => { }, []); }; - const appDefinitionChanged = async (newDefinition, opts = {}) => { + const appDefinitionChanged = useCallback(async (newDefinition, opts = {}) => { if (opts?.versionChanged) { setCurrentPageId(newDefinition.homePageId); return new Promise((resolve) => { @@ -930,7 +952,9 @@ const EditorComponent = (props) => { }); } let updatedAppDefinition; - const copyOfAppDefinition = JSON.parse(JSON.stringify(useEditorStore.getState().appDefinition)); + const appDefinition = useEditorStore.getState().appDefinition; + const copyOfAppDefinition = JSON.parse(JSON.stringify(appDefinition)); + const currentPageId = useEditorStore.getState().currentPageId; if (opts?.skipYmapUpdate && opts?.currentSessionId !== currentSessionId) { updatedAppDefinition = produce(copyOfAppDefinition, (draft) => { @@ -959,7 +983,6 @@ const EditorComponent = (props) => { if (opts?.containerChanges || opts?.componentDefinitionChanged) { const currentPageComponents = newDefinition.pages[currentPageId]?.components; - draft.pages[currentPageId].components = currentPageComponents; } @@ -1024,7 +1047,7 @@ const EditorComponent = (props) => { appDefinition: updatedAppDefinition, }); } - }; + }, []); const cloneEventsForClonedComponents = (componentUpdateDiff, operation, componentMap) => { function getKeyFromComponentMap(componentMap, newItem) { @@ -1108,24 +1131,26 @@ const EditorComponent = (props) => { } //Todo: Move this to a separate function or as a middleware of the api to createing a component - if (updateDiff?.type === 'components' && updateDiff?.operation === 'create') { - const componentsFromCurrentState = getCurrentState().components; - const newComponentIds = Object.keys(updateDiff?.updateDiff); - const newComponentsExposedData = {}; - const componentEntityArray = []; - Object.values(componentsFromCurrentState).filter((component) => { - if (newComponentIds.includes(component.id)) { - const componentName = updateDiff?.updateDiff[component.id]?.name; - newComponentsExposedData[componentName] = component; - componentEntityArray.push({ id: component.id, name: componentName }); - } - }); + handleLowPriorityWork(() => { + if (updateDiff?.type === 'components' && updateDiff?.operation === 'create') { + const componentsFromCurrentState = getCurrentState().components; + const newComponentIds = Object.keys(updateDiff?.updateDiff); + const newComponentsExposedData = {}; + const componentEntityArray = []; + Object.values(componentsFromCurrentState).filter((component) => { + if (newComponentIds.includes(component.id)) { + const componentName = updateDiff?.updateDiff[component.id]?.name; + newComponentsExposedData[componentName] = component; + componentEntityArray.push({ id: component.id, name: componentName }); + } + }); - useResolveStore.getState().actions.addEntitiesToMap(componentEntityArray); - useResolveStore.getState().actions.addAppSuggestions({ - components: newComponentsExposedData, - }); - } + useResolveStore.getState().actions.addEntitiesToMap(componentEntityArray); + useResolveStore.getState().actions.addAppSuggestions({ + components: newComponentsExposedData, + }); + } + }); if ( updateDiff?.type === 'components' && @@ -1176,7 +1201,7 @@ const EditorComponent = (props) => { }; const realtimeSave = debounce(appDefinitionChanged, 100); - const autoSave = debounce(saveEditingVersion, 150); + const autoSave = saveEditingVersion; function handlePaths(prevPatch, path = [], appJSON) { const paths = [...path]; @@ -1309,9 +1334,11 @@ const EditorComponent = (props) => { } } }; - const removeComponent = (componentId) => { + const removeComponent = React.useCallback((componentId) => { if (!isVersionReleased) { + const appDefinition = useEditorStore.getState().appDefinition; let newDefinition = JSON.parse(JSON.stringify(appDefinition)); + const currentPageId = useEditorStore.getState().currentPageId; let childComponents = []; @@ -1330,7 +1357,6 @@ const EditorComponent = (props) => { }); delete newDefinition.pages[currentPageId].components[componentId]; - const platform = navigator?.userAgentData?.platform || navigator?.platform || 'unknown'; if (platform.toLowerCase().indexOf('mac') > -1) { toast('Component deleted! (⌘ + Z to undo)', { @@ -1365,37 +1391,12 @@ const EditorComponent = (props) => { }); } - const newAppDefinition = JSON.parse(JSON.stringify(useEditorStore.getState().appDefinition)); - const currentComponents = newAppDefinition.pages[currentPageId].components; - const currentDataQueries = useDataQueriesStore.getState().dataQueries; - - const newComponentDefinition = useResolveStore - .getState() - .actions.findAndReplaceReferences(currentComponents, deletedComponentNames); - - const entityReferencesInQuerries = findAllEntityReferences(currentDataQueries, []); - - if (entityReferencesInQuerries.length > 0) { - const newDataQueries = useResolveStore - .getState() - .actions.findAndReplaceReferences(currentDataQueries, deletedComponentNames); - - useDataQueriesStore.getState().actions.setDataQueries(newDataQueries, 'mappingUpdate'); - } - - newAppDefinition.pages[currentPageId].components = newComponentDefinition; - - useEditorStore.getState().actions.updateEditorState({ - appDefinition: newAppDefinition, - isUpdatingEditorStateInProcess: true, - }); - useResolveStore.getState().actions.removeEntitiesFromMap(deleteFromMap); useResolveStore.getState().actions.removeAppSuggestions(allHintsAssociatedWithQuery); } else { useAppVersionStore.getState().actions.enableReleasedVersionPopupState(); } - }; + }, []); const moveComponents = (direction) => { const _appDefinition = JSON.parse(JSON.stringify(appDefinition)); @@ -1880,9 +1881,9 @@ const EditorComponent = (props) => { } }; - const deviceWindowWidth = 450; + const isEditorReady = useCurrentStateStore((state) => state.isEditorReady); - if (isLoading) { + if (isLoading && !isEditorReady) { return (
@@ -1937,7 +1938,6 @@ const EditorComponent = (props) => { canRedo={canRedo} handleUndo={handleUndo} handleRedo={handleRedo} - // saveError={saveError} onNameChanged={onNameChanged} setAppDefinitionFromVersion={setAppDefinitionFromVersion} onVersionRelease={onVersionRelease} @@ -1960,7 +1960,6 @@ const EditorComponent = (props) => { globalDataSourcesChanged={globalDataSourcesChanged} onZoomChanged={onZoomChanged} switchDarkMode={changeDarkMode} - debuggerActions={sideBarDebugger} appDefinition={{ components: appDefinition?.pages[currentPageId]?.components ?? {}, pages: appDefinition?.pages ?? {}, @@ -2016,7 +2015,7 @@ const EditorComponent = (props) => { onMouseUp={handleCanvasContainerMouseUp} ref={canvasContainerRef} onScroll={() => { - selectionRef.current.checkScroll(); + selectionRef.current?.checkScroll(); }} >
@@ -2061,10 +2060,8 @@ const EditorComponent = (props) => { {defaultComponentStateComputed && ( <> { : 'edit' } zoomLevel={zoomLevel} - deviceWindowWidth={deviceWindowWidth} appLoading={isLoading} onEvent={handleEvent} - onComponentOptionChanged={handleOnComponentOptionChanged} - onComponentOptionsChanged={handleOnComponentOptionsChanged} setSelectedComponent={setSelectedComponent} handleUndo={handleUndo} handleRedo={handleRedo} removeComponent={removeComponent} onComponentClick={noop} // Prop is used in Viewer hence using a dummy function to prevent error in editor - sideBarDebugger={sideBarDebugger} currentPageId={currentPageId} /> { - const { setSelectionInProgress, setSelectedComponents, scrollOptions } = useEditorStore( + const { setSelectionInProgress, setSelectedComponents } = useEditorStore( (state) => ({ setSelectionInProgress: state?.actions?.setSelectionInProgress, setSelectedComponents: state?.actions?.setSelectedComponents, - scrollOptions: state.scrollOptions, }), shallow ); - const [dragTarget, setDragTarget] = useState([]); + const scrollOptions = { + container: canvasContainerRef.current, + throttleTime: 30, + threshold: 0, + }; const onAreaSelectionStart = useCallback( (e) => { - console.log('onAreaSelectionStart', e); const isMultiSelect = e.inputEvent.shiftKey || useEditorStore.getState().selectedComponents.length > 0; setSelectionInProgress(true); setSelectedComponents([...(isMultiSelect ? useEditorStore.getState().selectedComponents : EMPTY_ARRAY)]); @@ -34,8 +35,6 @@ const EditorSelecto = ({ ); const onAreaSelection = useCallback((e) => { - console.log('onAreaSelection', e); - setDragTarget(e.selected); e.added.forEach((el) => { el.classList.add('resizer-select'); }); @@ -51,7 +50,6 @@ const EditorSelecto = ({ const onAreaSelectionEnd = useCallback( (e) => { - console.log('onAreaSelectionEnd', e); setSelectionInProgress(false); e.selected.forEach((el, index) => { const id = el.getAttribute('widgetid'); @@ -109,34 +107,6 @@ const EditorSelecto = ({ canvasContainerRef.current.scrollBy(e.direction[0] * 10, e.direction[1] * 10); }} /> - {/* { - e.target.style.transform = e.transform; - }} - onDragGroup={(e) => { - console.log('Dragging--------new'); - e.events.forEach((ev) => { - ev.target.style.transform = ev.transform; - }); - }} - onClickGroup={(e) => window.objSelecto.clickTarget(e.inputEvent, e.inputTarget)} - onRender={(ev) => (ev.target.style.cssText += ev.cssText)} - // onDragStart={(_a) => { - // var target = _a.target, - // clientX = _a.clientX, - // clientY = _a.clientY; - // }} - // onDragEnd={(_a) => { - // var target = _a.target, - // isDrag = _a.isDrag, - // clientX = _a.clientX, - // clientY = _a.clientY; - // }} - /> */} ); }; diff --git a/frontend/src/Editor/GhostWidget.jsx b/frontend/src/Editor/GhostWidget.jsx new file mode 100644 index 0000000000..5ca519bea5 --- /dev/null +++ b/frontend/src/Editor/GhostWidget.jsx @@ -0,0 +1,27 @@ +import React from 'react'; +import { isEmpty } from 'lodash'; + +export default function GhostWidget({ layouts, currentLayout, canvasWidth, gridWidth }) { + let layoutStyle = {}; + if (!isEmpty(layouts?.[currentLayout] || layouts?.['desktop'])) { + const layoutData = layouts?.[currentLayout] || layouts?.['desktop']; + let width = (canvasWidth * layoutData.width) / 43; + layoutStyle = { + width: width + 'px', + height: layoutData.height + 'px', + transform: `translate(${layoutData.left * gridWidth}px, ${layoutData.top}px)`, + }; + } + return ( +
+ ); +} diff --git a/frontend/src/Editor/Header/GlobalSettings.jsx b/frontend/src/Editor/Header/GlobalSettings.jsx index b546b2c719..32a2ae7b61 100644 --- a/frontend/src/Editor/Header/GlobalSettings.jsx +++ b/frontend/src/Editor/Header/GlobalSettings.jsx @@ -15,6 +15,7 @@ import { shallow } from 'zustand/shallow'; import { ButtonSolid } from '@/_ui/AppButton/AppButton'; import { useAppDataActions, useAppInfo } from '@/_stores/appDataStore'; import CodeHinter from '../CodeEditor'; +import { useCurrentState } from '@/_stores/currentStateStore'; export const GlobalSettings = ({ globalSettings, @@ -23,8 +24,8 @@ export const GlobalSettings = ({ toggleAppMaintenance, isMaintenanceOn, backgroundFxQuery, - realState, }) => { + const realState = useCurrentState(); const { t } = useTranslation(); const { hideHeader, canvasMaxWidth, canvasMaxWidthType, canvasBackgroundColor } = globalSettings; const [showPicker, setShowPicker] = useState(false); diff --git a/frontend/src/Editor/Inspector/Components/Chart.jsx b/frontend/src/Editor/Inspector/Components/Chart.jsx index 9f34c632e8..fd657bf17c 100644 --- a/frontend/src/Editor/Inspector/Components/Chart.jsx +++ b/frontend/src/Editor/Inspector/Components/Chart.jsx @@ -2,7 +2,7 @@ import React from 'react'; import { renderElement } from '../Utils'; import { EventManager } from '@/Editor/Inspector/EventManager'; import Accordion from '@/_ui/Accordion'; -import { resolveReferences } from '@/_helpers/utils'; +import { resolveWidgetFieldValue } from '@/_helpers/utils'; import CodeHinter from '@/Editor/CodeEditor'; class Chart extends React.Component { @@ -74,9 +74,8 @@ class Chart extends React.Component { const jsonDescription = this.props.component.component.definition.properties.jsonDescription; - const plotFromJson = resolveReferences( - this.props.component.component.definition.properties.plotFromJson?.value, - currentState + const plotFromJson = resolveWidgetFieldValue( + this.props.component.component.definition.properties.plotFromJson?.value ); const chartType = this.props.component.component.definition.properties.type.value; diff --git a/frontend/src/Editor/Inspector/Components/DefaultComponent.jsx b/frontend/src/Editor/Inspector/Components/DefaultComponent.jsx index eff2444311..62f4d04fcc 100644 --- a/frontend/src/Editor/Inspector/Components/DefaultComponent.jsx +++ b/frontend/src/Editor/Inspector/Components/DefaultComponent.jsx @@ -5,7 +5,8 @@ import { renderElement } from '../Utils'; // eslint-disable-next-line import/no-unresolved import i18next from 'i18next'; import { resolveReferences } from '@/_helpers/utils'; -import { AllComponents } from '@/Editor/Box'; +// import { AllComponents } from '@/Editor/Box'; +import { AllComponents } from '@/_helpers/editorHelpers'; const SHOW_ADDITIONAL_ACTIONS = ['Text', 'TextInput', 'NumberInput', 'PasswordInput']; const PROPERTIES_VS_ACCORDION_TITLE = { diff --git a/frontend/src/Editor/Inspector/EventManager.jsx b/frontend/src/Editor/Inspector/EventManager.jsx index 8b22c75b60..ae2bd7ac77 100644 --- a/frontend/src/Editor/Inspector/EventManager.jsx +++ b/frontend/src/Editor/Inspector/EventManager.jsx @@ -21,6 +21,10 @@ import NoListItem from './Components/Table/NoListItem'; import ManageEventButton from './ManageEventButton'; import { EditorContext } from '../Context/EditorContextWrapper'; import CodeHinter from '../CodeEditor'; +// eslint-disable-next-line import/no-unresolved +import { diff } from 'deep-object-diff'; +import { useEditorStore } from '@/_stores/editorStore'; +import { handleLowPriorityWork } from '@/_helpers/editorHelpers'; export const EventManager = ({ sourceId, @@ -297,18 +301,28 @@ export const EventManager = ({ updatedEvent.event['componentSpecificActionParams'] = getDefault; } + const shouldUpdateEvent = !_.isEmpty(diff(events[index], updatedEvent)); + + if (!shouldUpdateEvent) return; + + const eventSourceid = updatedEvent?.sourceId; + + useEditorStore.getState().actions.updateComponentsNeedsUpdateOnNextRender([eventSourceid]); + newEvents[index] = updatedEvent; - updateAppVersionEventHandlers( - [ - { - event_id: updatedEvent.id, - diff: updatedEvent, - }, - ], - 'update', - param - ); + handleLowPriorityWork(() => { + updateAppVersionEventHandlers( + [ + { + event_id: updatedEvent.id, + diff: updatedEvent, + }, + ], + 'update', + param + ); + }); } function removeHandler(index) { diff --git a/frontend/src/Editor/ItemTypes.js b/frontend/src/Editor/ItemTypes.js deleted file mode 100644 index 162fe48486..0000000000 --- a/frontend/src/Editor/ItemTypes.js +++ /dev/null @@ -1,5 +0,0 @@ -export const ItemTypes = { - BOX: 'box', - COMMENT: 'comment', - NEW_COMMENT: 'new_comment', -}; diff --git a/frontend/src/Editor/LeftSidebar/SidebarDebugger/useDebugger.js b/frontend/src/Editor/LeftSidebar/SidebarDebugger/useDebugger.js index 75f9124fa4..a92a655430 100644 --- a/frontend/src/Editor/LeftSidebar/SidebarDebugger/useDebugger.js +++ b/frontend/src/Editor/LeftSidebar/SidebarDebugger/useDebugger.js @@ -14,7 +14,6 @@ const useDebugger = ({ currentPageId, isDebuggerOpen }) => { const { errors, succededQuery } = useCurrentStateStore( (state) => ({ errors: state.errors, - queries: state.queries, succededQuery: state.succededQuery, }), shallow diff --git a/frontend/src/Editor/LeftSidebar/SidebarPageSelector/index.jsx b/frontend/src/Editor/LeftSidebar/SidebarPageSelector/index.jsx index f39a5e2ad9..6d4f776cf7 100644 --- a/frontend/src/Editor/LeftSidebar/SidebarPageSelector/index.jsx +++ b/frontend/src/Editor/LeftSidebar/SidebarPageSelector/index.jsx @@ -26,7 +26,6 @@ const LeftSidebarPageSelector = ({ disableEnablePage, updateHomePage, updatePageHandle, - pages, homePageId, showHideViewerNavigationControls, updateOnSortingPages, @@ -35,6 +34,13 @@ const LeftSidebarPageSelector = ({ pinned, setPinned, }) => { + const pages = useMemo( + () => + Object.entries(_.cloneDeep(appDefinition.pages)) + .map(([id, page]) => ({ id, ...page })) + .sort((a, b) => a.index - b.index) || [], + [JSON.stringify(appDefinition.pages)] + ); const [allpages, setPages] = useState(pages); const [haveUserPinned, setHaveUserPinned] = useState(false); const currentState = useCurrentState(); diff --git a/frontend/src/Editor/LeftSidebar/index.jsx b/frontend/src/Editor/LeftSidebar/index.jsx index e6adb30771..a1ecd3604a 100644 --- a/frontend/src/Editor/LeftSidebar/index.jsx +++ b/frontend/src/Editor/LeftSidebar/index.jsx @@ -19,8 +19,6 @@ import { useDataSources } from '@/_stores/dataSourcesStore'; import { shallow } from 'zustand/shallow'; import useDebugger from './SidebarDebugger/useDebugger'; import { GlobalSettings } from '../Header/GlobalSettings'; -import { resolveReferences } from '@/_helpers/utils'; -import { useCurrentState } from '@/_stores/currentStateStore'; export const LeftSidebar = forwardRef((props, ref) => { const router = useRouter(); @@ -77,7 +75,6 @@ export const LeftSidebar = forwardRef((props, ref) => { }), shallow ); - const currentState = useCurrentState(); const [pinned, setPinned] = useState(!!localStorage.getItem('selectedSidebarItem')); const { errorLogs, clearErrorLogs, unReadErrorCount, allLog } = useDebugger({ @@ -140,97 +137,98 @@ export const LeftSidebar = forwardRef((props, ref) => { const backgroundFxQuery = appDefinition?.globalSettings?.backgroundFxQuery; - const SELECTED_ITEMS = { - page: ( - ({ id, ...page })) - .sort((a, b) => a.index - b.index) || [] - } - homePageId={appDefinition.homePageId} - showHideViewerNavigationControls={showHideViewerNavigationControls} - updateOnSortingPages={updateOnSortingPages} - apps={apps} - setPinned={handlePin} - pinned={pinned} - /> - ), - inspect: ( - - ), - datasource: ( - { - handleSelectedSidebarItem(null); - handlePin(false); - delete sideBarBtnRefs.current['datasource']; - }} - setPinned={handlePin} - pinned={pinned} - /> - ), - debugger: ( - - ), - settings: ( - - ), + const renderPopoverContent = () => { + if (selectedSidebarItem === null) return null; + switch (selectedSidebarItem) { + case 'page': + return ( + ({ id, ...page })) + .sort((a, b) => a.index - b.index) || [] + } + homePageId={appDefinition.homePageId} + showHideViewerNavigationControls={showHideViewerNavigationControls} + updateOnSortingPages={updateOnSortingPages} + apps={apps} + setPinned={handlePin} + pinned={pinned} + /> + ); + case 'inspect': + return ( + + ); + case 'datasource': + return ( + { + handleSelectedSidebarItem(null); + handlePin(false); + delete sideBarBtnRefs.current['datasource']; + }} + setPinned={handlePin} + pinned={pinned} + /> + ); + case 'debugger': + return ( + + ); + case 'settings': + return ( + + ); + } }; - useEffect(() => { - backgroundFxQuery && - globalSettingsChanged({ canvasBackgroundColor: resolveReferences(backgroundFxQuery, currentState) }); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [JSON.stringify(resolveReferences(backgroundFxQuery, currentState))]); - return (
{ open={pinned || !!selectedSidebarItem} popoverContentClassName={`p-0 sidebar-h-100-popover ${selectedSidebarItem}`} side="right" - popoverContent={SELECTED_ITEMS[selectedSidebarItem]} + popoverContent={renderPopoverContent()} popoverContentHeight={popoverContentHeight} /> diff --git a/frontend/src/Editor/Middlewares/HydrateWithResolveReferences.jsx b/frontend/src/Editor/Middlewares/HydrateWithResolveReferences.jsx new file mode 100644 index 0000000000..90baad87f4 --- /dev/null +++ b/frontend/src/Editor/Middlewares/HydrateWithResolveReferences.jsx @@ -0,0 +1,106 @@ +import React, { useEffect, useMemo } from 'react'; +import { + resolveGeneralProperties, + resolveGeneralStyles, + resolveProperties, + resolveStyles, +} from '../component-properties-resolution'; +import { validateProperties } from '../component-properties-validation'; +import { getComponentName, debuggerActions } from '@/_helpers/appUtils'; +import { memoizeFunction } from '../../_helpers/editorHelpers'; +import { componentTypes } from '../WidgetManager/components'; +import { useCurrentState } from '@/_stores/currentStateStore'; + +const shouldAddBoxShadowAndVisibility = ['TextInput', 'PasswordInput', 'NumberInput', 'Text']; + +const getComponentMetaData = memoizeFunction((componentType) => { + return componentTypes.find((comp) => componentType === comp.component); +}); + +const HydrateWithResolveReferences = ({ id, mode, component, customResolvables, children }) => { + const componentMeta = useMemo(() => getComponentMetaData(component?.component), []); + + const currentState = useCurrentState(); + + const resolvedProperties = useMemo(() => { + return resolveProperties(component, currentState, null, customResolvables, id); + }, [component, currentState, customResolvables, id]); + + const resolvedStyles = useMemo(() => { + return resolveStyles(component, currentState, null, customResolvables); + }, [component, currentState, customResolvables]); + + const resolvedGeneralProperties = useMemo(() => { + return resolveGeneralProperties(component, currentState, null, customResolvables); + }, [component, currentState, customResolvables]); + + const resolvedGeneralStyles = useMemo(() => { + return resolveGeneralStyles(component, currentState, null, customResolvables); + }, [component, currentState, customResolvables]); + + const [validatedProperties, propertyErrors] = + mode === 'edit' && component.validate + ? validateProperties(resolvedProperties, componentMeta.properties) + : [resolvedProperties, []]; + + if (shouldAddBoxShadowAndVisibility.includes(component.component)) { + validatedProperties.visibility = validatedProperties.visibility !== false ? true : false; + } + + const [validatedStyles, styleErrors] = + mode === 'edit' && component.validate + ? validateProperties(resolvedStyles, componentMeta.styles) + : [resolvedStyles, []]; + + if (!shouldAddBoxShadowAndVisibility.includes(component.component)) { + validatedStyles.visibility = validatedStyles.visibility !== false ? true : false; + } + + const [validatedGeneralProperties, generalPropertiesErrors] = component.validate + ? validateProperties(resolvedGeneralProperties, componentMeta.general) + : [resolvedGeneralProperties, []]; + + const [validatedGeneralStyles, generalStylesErrors] = + mode === 'edit' && component.validate + ? validateProperties(resolvedGeneralStyles, componentMeta.generalStyles) + : [resolvedGeneralStyles, []]; + + useEffect(() => { + const currentPage = currentState?.page; + const componentName = getComponentName(currentState, id); + const errorLog = Object.fromEntries( + [...propertyErrors, ...styleErrors, ...generalPropertiesErrors, ...generalStylesErrors].map((error) => [ + `${componentName} - ${error.property}`, + { + page: currentPage, + type: 'component', + kind: 'component', + strace: 'page_level', + data: { message: `${error.message}`, status: true }, + resolvedProperties: resolvedProperties, + effectiveProperties: validatedProperties, + }, + ]) + ); + debuggerActions?.error(errorLog); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [JSON.stringify({ propertyErrors, styleErrors, generalPropertiesErrors })]); + + const resolvedReferences = useMemo(() => { + return { + properties: validatedProperties, + styles: validatedStyles, + generalProperties: validatedGeneralProperties, + generalStyles: validatedGeneralStyles, + }; + }, [validatedProperties, validatedStyles, validatedGeneralProperties, validatedGeneralStyles]); + + // Clone the child component with resolved props + const childWithProps = React.Children.map(children, (child) => { + return React.cloneElement(child, { ...resolvedReferences }); + }); + + return <>{childWithProps}; +}; + +export default HydrateWithResolveReferences; diff --git a/frontend/src/Editor/SubContainer.jsx b/frontend/src/Editor/SubContainer.jsx index b4f09e2503..c664c87fea 100644 --- a/frontend/src/Editor/SubContainer.jsx +++ b/frontend/src/Editor/SubContainer.jsx @@ -1,42 +1,37 @@ /* eslint-disable import/no-named-as-default */ -import React, { useCallback, useState, useEffect, useRef, useMemo } from 'react'; -import { useDrop, useDragLayer } from 'react-dnd'; -import { ItemTypes } from './ItemTypes'; +import React, { useState, useEffect, useRef, useMemo } from 'react'; +import { useDrop } from 'react-dnd'; +import { ItemTypes } from './editorConstants'; import { DraggableBox } from './DraggableBox'; import update from 'immutability-helper'; import _, { isEmpty } from 'lodash'; import { componentTypes } from './WidgetManager/components'; -import { addNewWidgetToTheEditor } from '@/_helpers/appUtils'; -import { resolveReferences } from '@/_helpers/utils'; +import { addNewWidgetToTheEditor, onComponentOptionChanged, onComponentOptionsChanged } from '@/_helpers/appUtils'; +import { resolveWidgetFieldValue } from '@/_helpers/utils'; import { toast } from 'react-hot-toast'; import { restrictedWidgetsObj } from '@/Editor/WidgetManager/restrictedWidgetsConfig'; import { useCurrentState } from '@/_stores/currentStateStore'; import { shallow } from 'zustand/shallow'; -import { useMounted } from '@/_hooks/use-mount'; + import { useEditorStore } from '@/_stores/editorStore'; // eslint-disable-next-line import/no-unresolved import { diff } from 'deep-object-diff'; +// eslint-disable-next-line import/namespace import { useGridStore, useResizingComponentId } from '@/_stores/gridStore'; -import { SUBCONTAINER_WITH_SCROLL } from './constants'; import { isPDFSupported } from '@/_stores/utils'; - -// const NO_OF_GRIDS = 43; +import GhostWidget from './GhostWidget'; export const SubContainer = ({ mode, snapToGrid, onComponentClick, onEvent, - appDefinition, appDefinitionChanged, - onComponentOptionChanged, - onComponentOptionsChanged, appLoading, zoomLevel, parent, parentRef, setSelectedComponent, - deviceWindowWidth, selectedComponent, currentLayout, removeComponent, @@ -50,23 +45,21 @@ export const SubContainer = ({ sideBarDebugger, onOptionChange, exposedVariables, - addDefaultChildren = false, height = '100%', currentPageId, childComponents = null, listmode = null, columns = 1, - setSubContainerWidths, parentWidgetId, - // turnOffAutoLayout, }) => { //Todo add custom resolve vars for other widgets too - const mounted = useMounted(); + const widgetResolvables = Object.freeze({ Listview: 'listItem', }); - const customResolverVariable = widgetResolvables[parentComponent?.component]; + const appDefinition = useEditorStore((state) => state.appDefinition, shallow); + const currentState = useCurrentState(); const { selectedComponents } = useEditorStore( (state) => ({ @@ -74,9 +67,9 @@ export const SubContainer = ({ }), shallow ); + const resizingComponentId = useResizingComponentId(); - // const [noOfGrids] = useNoOfGrid(); const noOfGrids = 43; const { isGridActive } = useGridStore((state) => ({ isGridActive: state.activeGrid === parent }), shallow); @@ -94,51 +87,44 @@ export const SubContainer = ({ zoomLevel = zoomLevel || 1; // eslint-disable-next-line react-hooks/exhaustive-deps - const allComponents = appDefinition ? appDefinition.pages[currentPageId].components : {}; + const allComponents = appDefinition.pages[currentPageId]?.components ?? {}; - const getChildWidgets = (components) => { - let childWidgets = {}; - Object.keys(components).forEach((key) => { - const componentParent = components[key].component.parent; - if (componentParent === parent) { - childWidgets[key] = { ...components[key], component: { ...components[key]['component'], parent } }; + const allChildComponents = useMemo(() => { + const _childWidgets = {}; + Object.entries(allComponents).forEach(([componentId, componentData]) => { + if (componentData?.component?.parent === parent) { + _childWidgets[componentId] = componentData; } }); - return childWidgets; - }; + return _childWidgets; + }, [allComponents, parent]); - const [boxes, setBoxes] = useState(allComponents); - const [childWidgets, setChildWidgets] = useState([]); + const [childWidgets, setChildWidgets] = useState(() => allChildComponents); const [isDragging, setIsDragging] = useState(false); const [isResizing, setIsResizing] = useState(false); - // const [subContainerHeight, setSubContainerHeight] = useState('100%'); //used to determine the height of the sub container for modal + const subContainerHeightRef = useRef(height ?? '100%'); useEffect(() => { - setBoxes(allComponents); - setChildWidgets(() => getChildWidgets(allComponents)); + if (parent) { + const _childWidgets = {}; + + Object.entries(allComponents).forEach(([componentId, componentData]) => { + if (componentData?.component?.parent === parent) { + _childWidgets[componentId] = componentData; + } + }); + + const shouldUpdate = !_.isEqual(childWidgets, _childWidgets); + + if (shouldUpdate) { + setChildWidgets(_childWidgets); + } + } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [allComponents, parent]); - - useEffect(() => { - try { - const isParentScrollable = SUBCONTAINER_WITH_SCROLL.has(allComponents[parent]?.component?.component); - const canvasBounds = parentRef.current.getBoundingClientRect(); - const subContainerHeight = canvasBounds.height - 30; - const componentBottom = Object.values(childWidgets).reduce(function (max, currentElement) { - let currentSum = currentElement.layouts[currentLayout].top + currentElement.layouts[currentLayout].height; - return Math.max(max, currentSum); - }, 0); - - if (isParentScrollable && subContainerHeight <= componentBottom) { - subContainerHeightRef.current = componentBottom + 100; - } - } catch (error) { - console.error('console.error', error); - } - }, [childWidgets]); + }, [JSON.stringify(allChildComponents), parent]); const containerWidth = getContainerCanvasWidth(); @@ -201,120 +187,10 @@ export const SubContainer = ({ }; useEffect(() => { - setSubContainerWidths(parent, containerWidth / noOfGrids); + useGridStore.getState().actions.setSubContainerWidths(parent, containerWidth / noOfGrids); // eslint-disable-next-line react-hooks/exhaustive-deps }, [containerWidth]); - useEffect(() => { - if (mounted) { - //find children with parent prop - const children = Object.keys(allComponents).filter((key) => { - if (key === parent) return false; - return allComponents[key].parent === parent; - }); - - if (children.length === 0 && addDefaultChildren === true) { - const defaultChildren = _.cloneDeep(parentComponent)['defaultChildren']; - const childrenBoxes = {}; - const parentId = - parentComponent.component !== 'Tabs' - ? parentRef.current.id - : parentRef.current.id?.substring(0, parentRef.current.id.lastIndexOf('-')); - - const _allComponents = JSON.parse(JSON.stringify(allComponents)); - - defaultChildren.forEach((child) => { - const { componentName, layout, incrementWidth, properties, accessorKey, tab, defaultValue, styles } = child; - - const componentMeta = _.cloneDeep(componentTypes.find((component) => component.component === componentName)); - const componentData = JSON.parse(JSON.stringify(componentMeta)); - - const width = layout.width ? layout.width : (componentMeta.defaultSize.width * 100) / noOfGrids; - const height = layout.height ? layout.height : componentMeta.defaultSize.height; - const newComponentDefinition = { - ...componentData.definition.properties, - }; - - if (_.isArray(properties) && properties.length > 0) { - properties.forEach((prop) => { - const accessor = customResolverVariable - ? `{{${customResolverVariable}.${accessorKey}}}` - : defaultValue[prop] || ''; - - _.set(newComponentDefinition, prop, { - value: accessor, - }); - }); - _.set(componentData, 'definition.properties', newComponentDefinition); - } - - if (_.isArray(styles) && styles.length > 0) { - styles.forEach((prop) => { - const accessor = customResolverVariable - ? `{{${customResolverVariable}.${accessorKey}}}` - : defaultValue[prop] || ''; - - _.set(newComponentDefinition, prop, { - value: accessor, - }); - }); - _.set(componentData, 'definition.styles', newComponentDefinition); - } - - const newComponent = addNewWidgetToTheEditor( - componentData, - {}, - { ..._allComponents, ...childrenBoxes }, - {}, - currentLayout, - snapToGrid, - zoomLevel, - true, - true - ); - - _.set(childrenBoxes, newComponent.id, { - component: { - ...newComponent.component, - parent: parentComponent.component === 'Tabs' ? parentId + '-' + tab : parentId, - }, - - layouts: { - [currentLayout]: { - ...layout, - width: incrementWidth ? width * incrementWidth : width, - height: height, - }, - }, - }); - }); - - _allComponents[parentId] = { - ...allComponents[parentId], - withDefaultChildren: false, - }; - setBoxes({ - ..._allComponents, - ...childrenBoxes, - }); - } - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [mounted]); - - const moveBox = useCallback( - (id, left, top) => { - setBoxes( - update(boxes, { - [id]: { - $merge: { left, top }, - }, - }) - ); - }, - [boxes] - ); - useEffect(() => { if (appDefinitionChanged) { const newDefinition = { @@ -323,13 +199,16 @@ export const SubContainer = ({ ...appDefinition.pages, [currentPageId]: { ...appDefinition.pages[currentPageId], - components: boxes, + components: { + ...appDefinition.pages[currentPageId].components, + ...childWidgets, + }, }, }, }; const oldComponents = appDefinition.pages[currentPageId]?.components ?? {}; - const newComponents = boxes; + const newComponents = newDefinition.pages[currentPageId]?.components ?? {}; const componendAdded = Object.keys(newComponents).length > Object.keys(oldComponents).length; @@ -346,50 +225,17 @@ export const SubContainer = ({ } } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [boxes]); + }, [childWidgets]); - const { draggingState } = useDragLayer((monitor) => { - // TODO: Need to move to a performant version of the block below - if (monitor.getItem()) { - if (monitor.getItem().id === undefined) { - if (parentRef.current) { - const currentOffset = monitor.getSourceClientOffset(); - if (currentOffset) { - const canvasBoundingRect = parentRef?.current - ?.getElementsByClassName('real-canvas')[0] - ?.getBoundingClientRect(); - if (!canvasBoundingRect) return { draggingState: false }; - if ( - currentOffset.x > canvasBoundingRect.x && - currentOffset.x < canvasBoundingRect.x + canvasBoundingRect.width - ) { - return { draggingState: true }; - } - } - } - } - } - - if (monitor.isDragging() && monitor.getItem().parent) { - if (monitor.getItem().parent === parent) { - return { draggingState: true }; - } else { - return { draggingState: false }; - } - } else { - return { draggingState: false }; - } - }); - - //!Todo: need to check: this never gets called as draggingState is always false - useEffect(() => { - setIsDragging(draggingState); - }, [draggingState]); - - const [, drop] = useDrop( + const [{ isOver, isOverCurrent }, drop] = useDrop( () => ({ accept: ItemTypes.BOX, drop(item, monitor) { + const didDrop = monitor.didDrop(); + if (didDrop && !parent) { + return; + } + if (item.component.component === 'PDF' && !isPDFSupported()) { toast.error( 'PDF is not supported in this version of browser. We recommend upgrading to the latest version for full support.' @@ -411,7 +257,7 @@ export const SubContainer = ({ let newComponent = addNewWidgetToTheEditor( componentMeta, monitor, - boxes, + { ...allComponents, ...childWidgets }, canvasBoundingRect, item.currentLayout, snapToGrid, @@ -426,18 +272,20 @@ export const SubContainer = ({ newComponent = placeComponentInsideParent(newComponent, canvasBoundingRect); } - setBoxes({ - ...boxes, - [newComponent.id]: { - component: { - ...newComponent.component, - parent: parentRef.current.id, + setChildWidgets((prev) => { + return { + ...prev, + [newComponent.id]: { + component: { + ...newComponent.component, + parent: parentRef.current.id, + }, + layouts: { + ...newComponent.layout, + }, + withDefaultChildren: newComponent.withDefaultChildren, }, - layouts: { - ...newComponent.layout, - }, - withDefaultChildren: newComponent.withDefaultChildren, - }, + }; }); setSelectedComponent(newComponent.id, newComponent.component); @@ -456,8 +304,12 @@ export const SubContainer = ({ ); } }, + collect: (monitor) => ({ + isOver: monitor.isOver(), + isOverCurrent: monitor.isOver({ shallow: true }), + }), }), - [moveBox] + [parent] ); function getContainerCanvasWidth() { @@ -482,7 +334,7 @@ export const SubContainer = ({ return; } if (Object.keys(value).length > 0) { - setBoxes((boxes) => { + setChildWidgets((boxes) => { return update(boxes, { [id]: { $merge: { @@ -549,6 +401,31 @@ export const SubContainer = ({ return false; } + const getContainerProps = (componentId) => { + return { + mode, + snapToGrid, + onComponentClick, + onEvent, + appDefinition, + appDefinitionChanged, + currentState, + appLoading, + zoomLevel, + setSelectedComponent, + removeComponent, + currentLayout, + selectedComponents, + darkMode, + readOnly, + onComponentHover, + hoveredComponent, + sideBarDebugger, + currentPageId, + childComponents, + }; + }; + return ( { - const addDefaultChildren = box['withDefaultChildren'] || false; - // const box = childWidgets[key]; - const canShowInCurrentLayout = box.component.definition.others[currentLayout === 'mobile' ? 'showOnMobile' : 'showOnDesktop'].value; - if (box.component.parent && resolveReferences(canShowInCurrentLayout, currentState)) { + if (box.component.parent && resolveWidgetFieldValue(canShowInCurrentLayout)) { return ( setIsResizing(status)} - draggingStatusChanged={(status) => setIsDragging(status)} inCanvas={true} zoomLevel={zoomLevel} - setSelectedComponent={setSelectedComponent} selectedComponent={selectedComponent} - deviceWindowWidth={deviceWindowWidth} isSelectedComponent={ mode === 'edit' ? selectedComponents.find((component) => component.id === key) : false } @@ -644,42 +512,21 @@ export const SubContainer = ({ onComponentHover={onComponentHover} hoveredComponent={hoveredComponent} parentId={parent} - sideBarDebugger={sideBarDebugger} isMultipleComponentsSelected={selectedComponents?.length > 1 ? true : false} exposedVariables={exposedVariables ?? {}} - childComponents={childComponents[key]} - containerProps={{ - mode, - snapToGrid, - onComponentClick, - onEvent, - appDefinition, - appDefinitionChanged, - currentState, - onComponentOptionChanged, - onComponentOptionsChanged, - appLoading, - zoomLevel, - setSelectedComponent, - removeComponent, - currentLayout, - deviceWindowWidth, - selectedComponents, - darkMode, - readOnly, - onComponentHover, - hoveredComponent, - sideBarDebugger, - addDefaultChildren, - currentPageId, - childComponents, - setSubContainerWidths, - }} + getContainerProps={getContainerProps} /> ); } })} +
{appLoading && ( @@ -695,6 +542,21 @@ export const SubContainer = ({ ); }; +const ResizeGhostWidget = ({ resizingComponentId, widgets, currentLayout, canvasWidth, gridWidth }) => { + if (!resizingComponentId) { + return ''; + } + + return ( + + ); +}; + const SubWidgetWrapper = ({ parent, readOnly, @@ -733,7 +595,7 @@ const SubWidgetWrapper = ({ useEffect(() => { const controlBox = document.querySelector(`[target-id="${id}"]`); - console.log('controlBox', { hide: !isOnScreen && isSelected && !isDragging && !isResizing, isOnScreen }); + // console.log('controlBox', { hide: !isOnScreen && isSelected && !isDragging && !isResizing, isOnScreen }); //adding attribute instead of class since react-moveable seems to replace classes internally on scroll stop if (!isOnScreen && isSelected && !isDragging && !isResizing) { controlBox?.classList.add('hide-control'); @@ -772,7 +634,6 @@ const SubWidgetWrapper = ({ }; const SubContianerWrapper = ({ children, isDragging, isResizing, isGridActive, readOnly, drop, styles, parent }) => { - // const [dragTarget] = useDragTarget(); return (
diff --git a/frontend/src/Editor/SubCustomDragLayer.jsx b/frontend/src/Editor/SubCustomDragLayer.jsx index 4ff1c1644c..306abc2424 100644 --- a/frontend/src/Editor/SubCustomDragLayer.jsx +++ b/frontend/src/Editor/SubCustomDragLayer.jsx @@ -1,6 +1,6 @@ import React from 'react'; import { useDragLayer } from 'react-dnd'; -import { ItemTypes } from './ItemTypes'; +import { ItemTypes } from './editorConstants'; import { BoxDragPreview } from './BoxDragPreview'; import { snapToGrid } from '@/_helpers/appUtils'; const layerStyles = { @@ -47,8 +47,6 @@ function getItemStyles(delta, item, initialOffset, currentOffset, parentRef, par [x, y] = snapToGrid(canvasWidth, x, y); - console.log(`translate(${x}px, ${y}px)`); - const transform = `translate(${x}px, ${y}px)`; return { transform, diff --git a/frontend/src/Editor/Viewer.jsx b/frontend/src/Editor/Viewer.jsx index e210a0846e..3b4276a7cc 100644 --- a/frontend/src/Editor/Viewer.jsx +++ b/frontend/src/Editor/Viewer.jsx @@ -4,6 +4,7 @@ import { authenticationService, orgEnvironmentVariableService, orgEnvironmentConstantService, + dataqueryService, appService, appEnvironmentService, } from '@/_services'; @@ -20,11 +21,11 @@ import { runQuery, computeComponentState, buildAppDefinition, - runQueries, } from '@/_helpers/appUtils'; import queryString from 'query-string'; import ViewerLogoIcon from './Icons/viewer-logo.svg'; -import { resolveReferences, isQueryRunnable, setWindowTitle, pageTitles, isValidUUID } from '@/_helpers/utils'; +import { DataSourceTypes } from './DataSourceManager/SourceComponents'; +import { resolveReferences, isQueryRunnable, setWindowTitle, pageTitles } from '@/_helpers/utils'; import { withTranslation } from 'react-i18next'; import _ from 'lodash'; import { Navigate } from 'react-router-dom'; @@ -45,10 +46,6 @@ import ViewerSidebarNavigation from './Viewer/ViewerSidebarNavigation'; import MobileHeader from './Viewer/MobileHeader'; import DesktopHeader from './Viewer/DesktopHeader'; import './Viewer/viewer.scss'; -import { findAllEntityReferences } from '@/_stores/utils'; -import { useResolveStore } from '@/_stores/resolverStore'; -import { dfs } from '@/_stores/handleReferenceTransactions'; -const { produce } = require('immer'); class ViewerComponent extends React.Component { constructor(props) { @@ -99,6 +96,10 @@ class ViewerComponent extends React.Component { appDefinition: { ...appDefData }, pages: appDefData.pages, }); + + useEditorStore.getState().actions.updateEditorState({ + appDefinition: { ...appDefData }, + }); }; setStateForContainer = async (data, appVersionId) => { @@ -120,26 +121,41 @@ class ViewerComponent extends React.Component { let dataQueries = []; if (appVersionId) { - await useDataQueriesStore.getState().actions.fetchDataQueries(appVersionId, false, true); + const { data_queries } = await dataqueryService.getAll(appVersionId); + dataQueries = data_queries; } else { dataQueries = data.data_queries; + } + const queryConfirmationList = []; - const referencesManager = useResolveStore.getState().referenceMapper; - const newQueries = dataQueries - .map((dq) => { - if (!referencesManager.get(dq.id)) { - return { - id: dq.id, - name: dq.name, - }; - } - }) - .filter((c) => c !== undefined); + if (dataQueries.length > 0) { + dataQueries.forEach((query) => { + if (query?.options && query?.options?.requestConfirmation && query?.options?.runOnPageLoad) { + queryConfirmationList.push({ queryId: query.id, queryName: query.name }); + } - useResolveStore.getState().actions.addEntitiesToMap(newQueries); - useDataQueriesStore.getState().actions.setDataQueries(dataQueries); + if (query.pluginId || query?.plugin?.id) { + const exposedVariables = + query.plugin?.manifestFile?.data?.source?.exposedVariables || + query.plugin?.manifest_file?.data?.source?.exposed_variables; + + queryState[query.name] = { + ...exposedVariables, + ...this.props.currentState.queries[query.name], + }; + } else { + const dataSourceTypeDetail = DataSourceTypes.find((source) => source.kind === query.kind); + queryState[query.name] = { + ...dataSourceTypeDetail.exposedVariables, + ...this.props.currentState.queries[query.name], + }; + } + }); } + if (queryConfirmationList.length !== 0) { + this.updateQueryConfirmationList(queryConfirmationList); + } const variables = await this.fetchOrgEnvironmentVariables(data.slug, data.is_public); const constants = await this.fetchOrgEnvironmentConstants(data.slug, data.is_public); @@ -149,25 +165,7 @@ class ViewerComponent extends React.Component { const currentPageId = pages.filter((page) => page.handle === startingPageHandle)[0]?.id ?? homePageId; const currentPage = pages.find((page) => page.id === currentPageId); - const currentComponents = appDefData?.pages?.[currentPageId]?.components; - - if (currentComponents && Object.keys(currentComponents).length > 0) { - const referenceManager = useResolveStore.getState().referenceMapper; - - const newComponents = Object.keys(currentComponents).map((componentId) => { - const component = currentComponents[componentId]; - - if (!referenceManager.get(componentId)) { - return { - id: componentId, - name: component.component.name, - }; - } - }); - - useResolveStore.getState().actions.addEntitiesToMap(newComponents); - } - + useDataQueriesStore.getState().actions.setDataQueries(dataQueries); this.props.setCurrentState({ queries: queryState, components: {}, @@ -191,7 +189,6 @@ class ViewerComponent extends React.Component { }); useEditorStore.getState().actions.toggleCurrentLayout(this.props?.currentLayout == 'mobile' ? 'mobile' : 'desktop'); this.props.updateState({ events: data.events ?? [] }); - this.setState( { currentUser, @@ -209,108 +206,18 @@ class ViewerComponent extends React.Component { events: data.events ?? [], }, () => { - let components = this.state.appDefinition?.pages[currentPageId]?.components || {}; - let allEvents = data.events || []; - let allDataQueries = this.state.dataQueries || []; - let newComponentDefinition = JSON.parse(JSON.stringify(components)); + const components = appDefData?.pages[currentPageId]?.components || {}; - const entityReferencesInComponentDefinitions = findAllEntityReferences(newComponentDefinition, [])?.filter( - (entity) => entity && isValidUUID(entity) - ); + computeComponentState(components).then(async () => { + this.setState({ initialComputationOfStateDone: true, defaultComponentStateComputed: true }); + this.runQueries(dataQueries); - const entityReferencesInQueryoOptions = findAllEntityReferences(allDataQueries, [])?.filter( - (entity) => entity && isValidUUID(entity) - ); + const currentPageEvents = this.state.events.filter( + (event) => event.target === 'page' && event.sourceId === this.state.currentPageId + ); - const entityReferencesInEvents = findAllEntityReferences(allEvents, [])?.filter( - (entity) => entity && isValidUUID(entity) - ); - - const manager = useResolveStore.getState().referenceMapper; - - if ( - Array.isArray(entityReferencesInComponentDefinitions) && - entityReferencesInComponentDefinitions?.length > 0 - ) { - entityReferencesInComponentDefinitions.forEach((entity) => { - const entityrefExists = manager.has(entity); - - if (entityrefExists) { - const value = manager.get(entity); - newComponentDefinition = dfs(newComponentDefinition, entity, value); - } - const newAppDefinition = produce(this.state.appDefinition, (draft) => { - draft.pages[homePageId].components = newComponentDefinition; - }); - - components = newComponentDefinition; - - this.setState({ - appDefinition: newAppDefinition, - }); - }); - - if (Array.isArray(entityReferencesInQueryoOptions) && entityReferencesInQueryoOptions?.length > 0) { - let newQueryOptions = {}; - allDataQueries?.forEach((query) => { - newQueryOptions[query.id] = query.options; - }); - - entityReferencesInQueryoOptions.forEach((entity) => { - const entityrefExists = manager.has(entity); - - if (entityrefExists) { - const value = manager.get(entity); - newQueryOptions = dfs(newQueryOptions, entity, value); - } - }); - - allDataQueries = allDataQueries.map((query) => { - const queryId = query.id; - const dqOptions = newQueryOptions[queryId]; - - return { - ...query, - options: dqOptions, - }; - }); - - useDataQueriesStore.getState().actions.setDataQueries(allDataQueries); - } - - if (Array.isArray(entityReferencesInEvents) && entityReferencesInEvents?.length > 0) { - let newEvents = JSON.parse(JSON.stringify(allEvents)); - - entityReferencesInEvents.forEach((entity) => { - const entityrefExists = manager.has(entity); - - if (entityrefExists) { - const value = manager.get(entity); - newEvents = dfs(newEvents, entity, value); - } - }); - - this.props.updateState({ events: newEvents }); - } - } - - computeComponentState(newComponentDefinition) - .then(async () => { - this.setState({ initialComputationOfStateDone: true, defaultComponentStateComputed: true }); - - const queuedQueriesForRunOnAppLoad = useDataQueriesStore.getState().queuedQueriesForRunOnAppLoad; - const viewerRef = this.getViewerRef(); - await runQueries(queuedQueriesForRunOnAppLoad, viewerRef, 'view'); - - useDataQueriesStore.getState().actions.clearQueuedQueriesForRunOnAppLoad(); - }) - .finally(async () => { - const currentPageEvents = data.events.filter( - (event) => event.target === 'page' && event.sourceId === homePageId - ); - - await this.handleEvent('onPageLoad', currentPageEvents); - }); + await this.handleEvent('onPageLoad', currentPageEvents); + }); } ); }; diff --git a/frontend/src/Editor/editorConstants.js b/frontend/src/Editor/editorConstants.js new file mode 100644 index 0000000000..bf2cc746d2 --- /dev/null +++ b/frontend/src/Editor/editorConstants.js @@ -0,0 +1,11 @@ +export const ItemTypes = { + BOX: 'box', + COMMENT: 'comment', + NEW_COMMENT: 'new_comment', +}; + +export const EditorConstants = Object.freeze({ + deviceWindowWidth: 450, +}); + +export const decimalToHex = (alpha) => (alpha === 0 ? '00' : Math.round(255 * alpha).toString(16)); diff --git a/frontend/src/Editor/gridUtils.js b/frontend/src/Editor/gridUtils.js index 89666de401..04df15ce49 100644 --- a/frontend/src/Editor/gridUtils.js +++ b/frontend/src/Editor/gridUtils.js @@ -1,3 +1,5 @@ +import { useGridStore } from '@/_stores/gridStore'; + export function correctBounds(layout, bounds) { layout = scaleLayouts(layout); const collidesWith = []; @@ -233,3 +235,50 @@ function scaleLayouts(layouts, cols = 6) { width: layout.width * 3 > 43 ? 43 : layout.width * 3, })); } + +export const individualGroupableProps = (element) => { + if (element?.classList.contains('target2')) { + return { + resizable: false, + }; + } +}; + +export const handleWidgetResize = (e, list, boxes, gridWidth) => { + const currentLayout = list.find(({ id }) => id === e.target.id); + const currentWidget = boxes.find(({ id }) => id === e.target.id); + let _gridWidth = useGridStore.getState().subContainerWidths[currentWidget.component?.parent] || gridWidth; + document.getElementById('canvas-' + currentWidget.component?.parent)?.classList.add('show-grid'); + const currentWidth = currentLayout.width * _gridWidth; + const diffWidth = e.width - currentWidth; + const diffHeight = e.height - currentLayout.height; + const isLeftChanged = e.direction[0] === -1; + const isTopChanged = e.direction[1] === -1; + + let transformX = currentLayout.left * _gridWidth; + let transformY = currentLayout.top; + if (isLeftChanged) { + transformX = currentLayout.left * _gridWidth - diffWidth; + } + if (isTopChanged) { + transformY = currentLayout.top - diffHeight; + } + + const elemContainer = e.target.closest('.real-canvas'); + const containerHeight = elemContainer.clientHeight; + const containerWidth = elemContainer.clientWidth; + const maxY = containerHeight - e.target.clientHeight; + const maxLeft = containerWidth - e.target.clientWidth; + const maxWidthHit = transformX < 0 || transformX >= maxLeft; + const maxHeightHit = transformY < 0 || transformY >= maxY; + transformY = transformY < 0 ? 0 : transformY > maxY ? maxY : transformY; + transformX = transformX < 0 ? 0 : transformX > maxLeft ? maxLeft : transformX; + + if (!maxWidthHit || e.width < e.target.clientWidth) { + e.target.style.width = `${e.width}px`; + } + if (!maxHeightHit || e.height < e.target.clientHeight) { + e.target.style.height = `${e.height}px`; + } + e.target.style.transform = `translate(${transformX}px, ${transformY}px)`; +}; diff --git a/frontend/src/_helpers/appUtils.js b/frontend/src/_helpers/appUtils.js index a38f89b94d..f6c185c1e3 100644 --- a/frontend/src/_helpers/appUtils.js +++ b/frontend/src/_helpers/appUtils.js @@ -37,6 +37,7 @@ import { useAppDataStore } from '@/_stores/appDataStore'; import { useEditorStore } from '@/_stores/editorStore'; import { useGridStore } from '@/_stores/gridStore'; import { useResolveStore } from '@/_stores/resolverStore'; +import { handleLowPriorityWork } from './editorHelpers'; const ERROR_TYPES = Object.freeze({ ReferenceError: 'ReferenceError', @@ -47,6 +48,8 @@ const ERROR_TYPES = Object.freeze({ EvalError: 'EvalError', }); +export let duplicateCurrentState = null; + export function setStateAsync(_ref, state) { return new Promise((resolve) => { _ref.setState(state, resolve); @@ -64,37 +67,84 @@ export function setCurrentStateAsync(_ref, changes) { }); } +const debouncedChange = _.debounce(() => { + useCurrentStateStore.getState().actions.setCurrentState({ + components: duplicateCurrentState, + }); +}, 100); + export function onComponentOptionsChanged(component, options) { const componentName = component.name; - const components = getCurrentState().components; - let componentData = components[componentName]; - componentData = componentData || {}; + const { isEditorReady } = getCurrentState(); - for (const option of options) { - componentData[option[0]] = option[1]; + if (isEditorReady) { + if (duplicateCurrentState !== null) { + duplicateCurrentState = null; + } + + const components = getCurrentState().components; + let componentData = components[componentName]; + componentData = componentData || {}; + + for (const option of options) { + componentData[option[0]] = option[1]; + } + + useCurrentStateStore.getState().actions.setCurrentState({ + components: { ...components, [componentName]: componentData }, + }); + } else { + const components = duplicateCurrentState === null ? getCurrentState().components : duplicateCurrentState; + let componentData = components[componentName]; + componentData = componentData || {}; + + for (const option of options) { + componentData[option[0]] = option[1]; + } + + duplicateCurrentState = { ...components, [componentName]: componentData }; + + debouncedChange(); } - - useCurrentStateStore.getState().actions.setCurrentState({ - components: { ...components, [componentName]: componentData }, - }); return Promise.resolve(); } export function onComponentOptionChanged(component, option_name, value) { const componentName = component.name; - const components = getCurrentState().components; - let componentData = components[componentName]; - componentData = componentData || {}; + const { isEditorReady, components: currentComponents } = getCurrentState(); + const components = duplicateCurrentState === null ? currentComponents : duplicateCurrentState; + let componentData = components[componentName] || {}; componentData[option_name] = value; - if (option_name !== 'id') { - useCurrentStateStore.getState().actions.setCurrentState({ - components: { ...components, [componentName]: componentData }, - }); - } else if (!componentData?.id) { + const path = option_name ? `components.${componentName}.${option_name}` : null; + + if (isEditorReady) { + // Always update the current state if editor is ready useCurrentStateStore.getState().actions.setCurrentState({ components: { ...components, [componentName]: componentData }, }); + + if (!_.isEmpty(useResolveStore.getState().lookupTable?.resolvedRefs) && path) { + const lookUpTable = useResolveStore.getState().lookupTable; + + const existingRef = lookUpTable.resolvedRefs?.get(lookUpTable.hints?.get(path)); + + if (typeof existingRef === 'function') return; + + const shouldUpdateRef = existingRef !== componentData[option_name]; + + if (shouldUpdateRef) { + handleLowPriorityWork(() => { + useResolveStore + .getState() + .actions.updateResolvedRefsOfHints([{ hint: path, newRef: componentData[option_name] }]); + }); + } + } + } else { + // Update the duplicate state if editor is not ready + duplicateCurrentState = { ...components, [componentName]: componentData }; + debouncedChange(); } return Promise.resolve(); @@ -376,7 +426,7 @@ function showModal(_ref, modal, show) { console.log('No modal is associated with this event.'); return Promise.resolve(); } - + useEditorStore.getState().actions.updateComponentsNeedsUpdateOnNextRender([modalId]); const modalMeta = _ref.appDefinition.pages[_ref.currentPageId].components[modalId]; //! NeedToFix const _components = { @@ -812,6 +862,7 @@ export async function onEvent(_ref, eventName, events, options = {}, mode = 'edi } if (['onDataQuerySuccess', 'onDataQueryFailure'].includes(eventName)) { + if (!events || !isArray(events) || events.length === 0) return; await executeActionsForEventId(_self, eventName, events, mode, customVariables); } } @@ -983,7 +1034,8 @@ export function runQuery( confirmed = undefined, mode = 'edit', userSuppliedParameters = {}, - shouldSetPreviewData = false + shouldSetPreviewData = false, + isOnLoad = false ) { let parameters = userSuppliedParameters; const query = useDataQueriesStore.getState().dataQueries.find((query) => query.id === queryId); @@ -1046,218 +1098,231 @@ export function runQuery( // eslint-disable-next-line no-unused-vars return new Promise(function (resolve, reject) { - useCurrentStateStore.getState().actions.setCurrentState({ - queries: { - ...getCurrentState().queries, - [queryName]: { - ...getCurrentState().queries[queryName], - isLoading: true, - data: [], - rawData: [], - }, - }, - errors: {}, - }); - let queryExecutionPromise = null; - if (query.kind === 'runjs') { - queryExecutionPromise = executeMultilineJS(_self, query.options.code, query?.id, false, mode, parameters); - } else if (query.kind === 'runpy') { - queryExecutionPromise = executeRunPycode(_self, query.options.code, query, false, mode, queryState); - } else if (query.kind === 'tooljetdb') { - queryExecutionPromise = tooljetDbOperations.perform(query, queryState); - } else { - queryExecutionPromise = dataqueryService.run(queryId, options, query?.options); - } - - queryExecutionPromise - .then(async (data) => { - if (data.status === 'needs_oauth') { - const url = data.data.auth_url; // Backend generates and return sthe auth url - fetchOAuthToken(url, dataQuery['data_source_id'] || dataQuery['dataSourceId']); - } - - let queryStatusCode = data?.status ?? null; - const promiseStatus = - query.kind === 'tooljetdb' - ? data.statusText - : query.kind === 'runpy' - ? data?.data?.status ?? 'ok' - : data.status; - // Note: Need to move away from statusText -> statusCode - if ( - promiseStatus === 'failed' || - promiseStatus === 'Bad Request' || - promiseStatus === 'Not Found' || - promiseStatus === 'Unprocessable Entity' || - queryStatusCode === 400 || - queryStatusCode === 404 || - queryStatusCode === 422 - ) { - let errorData = {}; - switch (query.kind) { - case 'runpy': - errorData = data.data; - break; - case 'tooljetdb': - if (data?.error) { - errorData = { - message: data?.error?.message || 'Something went wrong', - description: data?.error?.message || 'Something went wrong', - status: data?.statusText || 'Failed', - data: data?.error || {}, - }; - } else { - errorData = data; - } - break; - default: - errorData = data; - break; - } - if (shouldSetPreviewData) { - setPreviewLoading(false); - setPreviewData(errorData); - } - // errorData = query.kind === 'runpy' ? data.data : data; - useCurrentStateStore.getState().actions.setErrors({ - [queryName]: { - type: 'query', - kind: query.kind, - data: errorData, - options: options, - }, - }); - - useCurrentStateStore.getState().actions.setCurrentState({ - queries: { - ...getCurrentState().queries, - [queryName]: _.assign( - { - ...getCurrentState().queries[queryName], - isLoading: false, - }, - query.kind === 'restapi' - ? { - request: data.data.requestObject, - response: data.data.responseObject, - responseHeaders: data.data.responseHeaders, - } - : {} - ), - }, - }); - resolve(data); - onEvent(_self, 'onDataQueryFailure', queryEvents); - if (mode !== 'view') { - const err = query.kind == 'tooljetdb' ? data?.error || data : data; - toast.error(err?.message ? err?.message : 'Something went wrong'); - } - return; - } else { - let rawData = data.data; - let finalData = data.data; - - if (dataQuery.options.enableTransformation) { - finalData = await runTransformation( - _ref, - finalData, - query.options.transformation, - query.options.transformationLanguage, - query, - 'edit' - ); - if (finalData.status === 'failed') { - useCurrentStateStore.getState().actions.setCurrentState({ - queries: { - ...getCurrentState().queries, - [queryName]: { - ...getCurrentState().queries[queryName], - isLoading: false, - }, - }, - }); - - useCurrentStateStore.getState().actions.setErrors({ - [queryName]: { - type: 'transformations', - data: finalData, - options: options, - }, - }); - resolve(finalData); - onEvent(_self, 'onDataQueryFailure', queryEvents); - return; - } - } - - if (shouldSetPreviewData) { - setPreviewLoading(false); - setPreviewData(finalData); - } - - if (dataQuery.options.showSuccessNotification) { - const notificationDuration = dataQuery.options.notificationDuration * 1000 || 5000; - toast.success(dataQuery.options.successMessage, { - duration: notificationDuration, - }); - } - - const currentQueries = getCurrentState().queries; - - useCurrentStateStore.getState().actions.setCurrentState({ - queries: { - ...currentQueries, - [queryName]: _.assign( - { - ...currentQueries[queryName], - isLoading: false, - data: finalData, - rawData, - }, - query.kind === 'restapi' - ? { - request: data.request, - response: data.response, - responseHeaders: data.responseHeaders, - } - : {} - ), - }, - // Used to generate logs - succededQuery: { - [queryName]: { - type: 'query', - kind: query.kind, - }, - }, - }); - - if (mode === 'edit') { - useResolveStore.getState().actions.addAppSuggestions({ - queries: { - [queryName]: { - data: finalData, - }, - }, - }); - } - resolve({ status: 'ok', data: finalData }); - onEvent(_self, 'onDataQuerySuccess', queryEvents, mode); - } - }) - .catch(({ error }) => { - if (mode !== 'view') toast.error(error ?? 'Unknown error'); + setTimeout(() => { + if (!isOnLoad) { useCurrentStateStore.getState().actions.setCurrentState({ queries: { ...getCurrentState().queries, [queryName]: { - isLoading: false, + ...getCurrentState().queries[queryName], + isLoading: true, + data: [], + rawData: [], + }, + }, + errors: {}, + }); + useResolveStore.getState().actions.addAppSuggestions({ + queries: { + [queryName]: { + data: [], + isLoading: true, }, }, }); + } + let queryExecutionPromise = null; + if (query.kind === 'runjs') { + queryExecutionPromise = executeMultilineJS(_self, query.options.code, query?.id, false, mode, parameters); + } else if (query.kind === 'runpy') { + queryExecutionPromise = executeRunPycode(_self, query.options.code, query, false, mode, queryState); + } else if (query.kind === 'tooljetdb') { + queryExecutionPromise = tooljetDbOperations.perform(query, queryState); + } else { + queryExecutionPromise = dataqueryService.run(queryId, options, query?.options); + } - resolve({ status: 'failed', message: error }); - }); + queryExecutionPromise + .then(async (data) => { + if (data.status === 'needs_oauth') { + const url = data.data.auth_url; // Backend generates and return sthe auth url + fetchOAuthToken(url, dataQuery['data_source_id'] || dataQuery['dataSourceId']); + } + + let queryStatusCode = data?.status ?? null; + const promiseStatus = + query.kind === 'tooljetdb' + ? data.statusText + : query.kind === 'runpy' + ? data?.data?.status ?? 'ok' + : data.status; + // Note: Need to move away from statusText -> statusCode + if ( + promiseStatus === 'failed' || + promiseStatus === 'Bad Request' || + promiseStatus === 'Not Found' || + promiseStatus === 'Unprocessable Entity' || + queryStatusCode === 400 || + queryStatusCode === 404 || + queryStatusCode === 422 + ) { + let errorData = {}; + switch (query.kind) { + case 'runpy': + errorData = data.data; + break; + case 'tooljetdb': + if (data?.error) { + errorData = { + message: data?.error?.message || 'Something went wrong', + description: data?.error?.message || 'Something went wrong', + status: data?.statusText || 'Failed', + data: data?.error || {}, + }; + } else { + errorData = data; + } + break; + default: + errorData = data; + break; + } + if (shouldSetPreviewData) { + setPreviewLoading(false); + setPreviewData(errorData); + } + // errorData = query.kind === 'runpy' ? data.data : data; + useCurrentStateStore.getState().actions.setErrors({ + [queryName]: { + type: 'query', + kind: query.kind, + data: errorData, + options: options, + }, + }); + + useCurrentStateStore.getState().actions.setCurrentState({ + queries: { + ...getCurrentState().queries, + [queryName]: _.assign( + { + ...getCurrentState().queries[queryName], + isLoading: false, + }, + query.kind === 'restapi' + ? { + request: data.data.requestObject, + response: data.data.responseObject, + responseHeaders: data.data.responseHeaders, + } + : {} + ), + }, + }); + resolve(data); + onEvent(_self, 'onDataQueryFailure', queryEvents); + if (mode !== 'view') { + const err = query.kind == 'tooljetdb' ? data?.error || data : data; + toast.error(err?.message ? err?.message : 'Something went wrong'); + } + return; + } else { + let rawData = data.data; + let finalData = data.data; + + if (dataQuery.options.enableTransformation) { + finalData = await runTransformation( + _ref, + finalData, + query.options.transformation, + query.options.transformationLanguage, + query, + 'edit' + ); + if (finalData.status === 'failed') { + useCurrentStateStore.getState().actions.setCurrentState({ + queries: { + ...getCurrentState().queries, + [queryName]: { + ...getCurrentState().queries[queryName], + isLoading: false, + }, + }, + }); + + useCurrentStateStore.getState().actions.setErrors({ + [queryName]: { + type: 'transformations', + data: finalData, + options: options, + }, + }); + resolve(finalData); + onEvent(_self, 'onDataQueryFailure', queryEvents); + return; + } + } + + if (shouldSetPreviewData) { + setPreviewLoading(false); + setPreviewData(finalData); + } + + if (dataQuery.options.showSuccessNotification) { + const notificationDuration = dataQuery.options.notificationDuration * 1000 || 5000; + toast.success(dataQuery.options.successMessage, { + duration: notificationDuration, + }); + } + useCurrentStateStore.getState().actions.setCurrentState({ + queries: { + ...getCurrentState().queries, + [queryName]: _.assign( + { + ...getCurrentState().queries[queryName], + isLoading: false, + data: finalData, + rawData, + }, + query.kind === 'restapi' + ? { + request: data.request, + response: data.response, + responseHeaders: data.responseHeaders, + } + : {} + ), + }, + // Used to generate logs + succededQuery: { + [queryName]: { + type: 'query', + kind: query.kind, + }, + }, + }); + + useResolveStore.getState().actions.addAppSuggestions({ + queries: { + [queryName]: { + data: finalData, + isLoading: false, + }, + }, + }); + + const basePath = `queries.${queryName}`; + + useResolveStore.getState().actions.updateLastUpdatedRefs([`${basePath}.data`, `${basePath}.isLoading`]); + + resolve({ status: 'ok', data: finalData }); + onEvent(_self, 'onDataQuerySuccess', queryEvents, mode); + } + }) + .catch(({ error }) => { + if (mode !== 'view') toast.error(error ?? 'Unknown error'); + useCurrentStateStore.getState().actions.setCurrentState({ + queries: { + ...getCurrentState().queries, + [queryName]: { + isLoading: false, + }, + }, + }); + + resolve({ status: 'failed', message: error }); + }); + }, 100); }); } @@ -1338,7 +1403,7 @@ export function computeComponentState(components = {}) { useEditorStore.getState().actions.updateEditorState({ defaultComponentStateComputed: true, }); - resolve(); + resolve('CURRENT_STATE_UPDATED'); }); } catch (error) { console.log(error); @@ -1884,10 +1949,10 @@ function convertMapSet(obj) { export const checkExistingQueryName = (newName) => useDataQueriesStore.getState().dataQueries.some((query) => query.name === newName); -export const runQueries = (queries, _ref, mode = 'edit') => { +export const runQueries = (queries, _ref, isOnLoad = false) => { queries.forEach((query) => { if (query.options.runOnPageLoad && isQueryRunnable(query)) { - runQuery(_ref, query.id, query.name, undefined, mode); + runQuery(_ref, query.id, query.name, isOnLoad); } }); }; @@ -1974,7 +2039,7 @@ export const buildAppDefinition = (data) => { _.unset(editingVersion, 'id'); const pages = data.pages.reduce((acc, page) => { - const currentComponents = buildComponentMetaDefinition(_.cloneDeep(page?.components)); + const currentComponents = buildComponentMetaDefinition(page?.components); page.components = currentComponents; diff --git a/frontend/src/_helpers/editorHelpers.js b/frontend/src/_helpers/editorHelpers.js new file mode 100644 index 0000000000..2971cd3bc0 --- /dev/null +++ b/frontend/src/_helpers/editorHelpers.js @@ -0,0 +1,198 @@ +import { Button } from '@/Editor/Components/Button'; +import { Image } from '@/Editor/Components/Image'; +import { Text } from '@/Editor/Components/Text'; +import { Table } from '@/Editor/Components/Table/Table'; +import { TextInput } from '@/Editor/Components/TextInput'; +import { NumberInput } from '@/Editor/Components/NumberInput'; +import { TextArea } from '@/Editor/Components/TextArea'; +import { Container } from '@/Editor/Components/Container'; +import { Tabs } from '@/Editor/Components/Tabs'; +import { RichTextEditor } from '@/Editor/Components/RichTextEditor'; +import { DropDown } from '@/Editor/Components/DropDown'; +import { Checkbox } from '@/Editor/Components/Checkbox'; +import { Datepicker } from '@/Editor/Components/Datepicker'; +import { DaterangePicker } from '@/Editor/Components/DaterangePicker'; +import { Multiselect } from '@/Editor/Components/Multiselect'; +import { Modal } from '@/Editor/Components/Modal'; +import { Chart } from '@/Editor/Components/Chart'; +// import { Map } from '@/Editor/Components/Map/Map'; +import { QrScanner } from '@/Editor/Components/QrScanner/QrScanner'; +import { ToggleSwitch } from '@/Editor/Components/Toggle'; +import { RadioButton } from '@/Editor/Components/RadioButton'; +import { StarRating } from '@/Editor/Components/StarRating'; +import { Divider } from '@/Editor/Components/Divider'; +import { FilePicker } from '@/Editor/Components/FilePicker'; +import { PasswordInput } from '@/Editor/Components/PasswordInput'; +import { Calendar } from '@/Editor/Components/Calendar'; +import { Listview } from '@/Editor/Components/Listview'; +import { IFrame } from '@/Editor/Components/IFrame'; +import { CodeEditor } from '@/Editor/Components/CodeEditor'; +import { Timer } from '@/Editor/Components/Timer'; +import { Statistics } from '@/Editor/Components/Statistics'; +import { Pagination } from '@/Editor/Components/Pagination'; +import { Tags } from '@/Editor/Components/Tags'; +import { Spinner } from '@/Editor/Components/Spinner'; +import { CircularProgressBar } from '@/Editor/Components/CirularProgressbar'; +import { RangeSlider } from '@/Editor/Components/RangeSlider'; +import { Timeline } from '@/Editor/Components/Timeline'; +import { SvgImage } from '@/Editor/Components/SvgImage'; +import { Html } from '@/Editor/Components/Html'; +import { ButtonGroup } from '@/Editor/Components/ButtonGroup'; +import { CustomComponent } from '@/Editor/Components/CustomComponent/CustomComponent'; +import { VerticalDivider } from '@/Editor/Components/verticalDivider'; +import { ColorPicker } from '@/Editor/Components/ColorPicker'; +import { KanbanBoard } from '@/Editor/Components/KanbanBoard/KanbanBoard'; +import { Kanban } from '@/Editor/Components/Kanban/Kanban'; +import { Steps } from '@/Editor/Components/Steps'; +import { TreeSelect } from '@/Editor/Components/TreeSelect'; +import { Icon } from '@/Editor/Components/Icon'; +import { Link } from '@/Editor/Components/Link'; +import { Form } from '@/Editor/Components/Form/Form'; +import { BoundedBox } from '@/Editor/Components/BoundedBox/BoundedBox'; + +export function memoizeFunction(func) { + const cache = new Map(); + + return function (...args) { + const key = JSON.stringify(args); + + if (cache.has(key)) { + return cache.get(key); + } + + const result = func.apply(this, args); + cache.set(key, result); + return result; + }; +} + +export const AllComponents = { + Button, + Image, + Text, + TextInput, + NumberInput, + Table, + TextArea, + Container, + Tabs, + RichTextEditor, + DropDown, + Checkbox, + Datepicker, + DaterangePicker, + Multiselect, + Modal, + Chart, + // Map, + QrScanner, + ToggleSwitch, + RadioButton, + StarRating, + Divider, + FilePicker, + PasswordInput, + Calendar, + IFrame, + CodeEditor, + Listview, + Timer, + Statistics, + Pagination, + Tags, + Spinner, + CircularProgressBar, + RangeSlider, + Timeline, + SvgImage, + Html, + ButtonGroup, + CustomComponent, + VerticalDivider, + ColorPicker, + KanbanBoard, + Kanban, + Steps, + TreeSelect, + Link, + Icon, + Form, + BoundedBox, +}; + +export const getComponentToRender = (componentName) => { + return AllComponents[componentName]; +}; + +export function isOnlyLayoutUpdate(diffState) { + const componentDiff = Object.keys(diffState).filter((key) => diffState[key]?.layouts && !diffState[key]?.component); + + return componentDiff.length > 0; +} + +function findReferenceInComponent(node, changedCurrentState) { + if (!node) return false; + + try { + if (typeof node === 'object') { + for (let key in node) { + const value = node[key]; + if (typeof value === 'string' && value.includes('{{') && value.includes('}}')) { + // Check if the referenced entity is in the state + if (changedCurrentState.some((state) => value.includes(state))) { + return true; + } + } else if (typeof value === 'object') { + const found = findReferenceInComponent(value, changedCurrentState); + + if (found) return true; + } + } + } + + return false; + } catch (error) { + console.log('error', { error }); + + return false; + } +} + +// Function to find which component ids contain the references +export function findComponentsWithReferences(components, changedCurrentState) { + const componentIdsWithReferences = []; + + if (!components) return componentIdsWithReferences; + + Object.entries(components).forEach(([componentId, componentData]) => { + const hasReference = findReferenceInComponent(componentData, changedCurrentState); + if (hasReference) { + componentIdsWithReferences.push(componentId); + } + }); + + return componentIdsWithReferences; +} + +export function handleLowPriorityWork(callback, timeout = null) { + const options = timeout ? { timeout } : {}; + window.requestIdleCallback(callback, options); +} + +export function generatePath(obj, targetKey, currentPath = '') { + for (const key in obj) { + const newPath = currentPath ? currentPath + '.' + key : key; + + if (key === targetKey) { + return newPath; + } + + if (typeof obj[key] === 'object' && obj[key] !== null) { + const result = generatePath(obj[key], targetKey, newPath); + if (result) { + return result; + } + } + } + return null; +} diff --git a/frontend/src/_helpers/utils.js b/frontend/src/_helpers/utils.js index 4d8fc06783..9ef51e0e57 100644 --- a/frontend/src/_helpers/utils.js +++ b/frontend/src/_helpers/utils.js @@ -8,10 +8,12 @@ import { toast } from 'react-hot-toast'; import { authenticationService } from '@/_services/authentication.service'; import { useDataQueriesStore } from '@/_stores/dataQueriesStore'; -import { getCurrentState } from '@/_stores/currentStateStore'; +import { getCurrentState, useCurrentState } from '@/_stores/currentStateStore'; import { getWorkspaceIdOrSlugFromURL, getSubpath, returnWorkspaceIdIfNeed } from './routes'; import { getCookie, eraseCookie } from '@/_helpers/cookie'; import { staticDataSources } from '@/Editor/QueryManager/constants'; +import { resolveReferences as newResolver } from '@/Editor/CodeEditor/utils'; +import { useResolveStore } from '@/_stores/resolverStore'; export function findProp(obj, prop, defval) { if (typeof defval === 'undefined') defval = null; @@ -324,10 +326,22 @@ export const serializeNestedObjectToQueryParams = function (obj, prefix) { return str.join('&'); }; -export function resolveWidgetFieldValue(prop, state, _default = [], customResolveObjects = {}) { +export function resolveWidgetFieldValue(prop, _default = [], customResolveObjects = {}) { const widgetFieldValue = prop; + const isStoreAndEditorReady = useResolveStore.getState().updateStoreState && useCurrentState.getState().isEditorReady; + try { + if (isStoreAndEditorReady) { + const [_, _error, resolveValue] = newResolver(widgetFieldValue?.value); + + if (_error) { + return _default; + } + + return resolveValue; + } + const state = getCurrentState(); return resolveReferences(widgetFieldValue, state, _default, customResolveObjects); } catch (err) { console.log(err); @@ -347,7 +361,7 @@ export function validateWidget({ validationObject, widgetValue, currentState, cu const maxValue = validationObject?.maxValue?.value; const customRule = validationObject?.customRule?.value; const mandatory = validationObject?.mandatory?.value; - const validationRegex = resolveWidgetFieldValue(regex, currentState, '', customResolveObjects); + const validationRegex = resolveWidgetFieldValue(regex, '', customResolveObjects); const re = new RegExp(validationRegex, 'g'); if (!re.test(widgetValue)) { @@ -357,7 +371,7 @@ export function validateWidget({ validationObject, widgetValue, currentState, cu }; } - const resolvedMinLength = resolveWidgetFieldValue(minLength, currentState, 0, customResolveObjects); + const resolvedMinLength = resolveWidgetFieldValue(minLength, 0, customResolveObjects); if ((widgetValue || '').length < parseInt(resolvedMinLength)) { return { isValid: false, @@ -365,7 +379,7 @@ export function validateWidget({ validationObject, widgetValue, currentState, cu }; } - const resolvedMaxLength = resolveWidgetFieldValue(maxLength, currentState, undefined, customResolveObjects); + const resolvedMaxLength = resolveWidgetFieldValue(maxLength, undefined, customResolveObjects); if (resolvedMaxLength !== undefined) { if ((widgetValue || '').length > parseInt(resolvedMaxLength)) { return { @@ -375,7 +389,7 @@ export function validateWidget({ validationObject, widgetValue, currentState, cu } } - const resolvedMinValue = resolveWidgetFieldValue(minValue, currentState, undefined, customResolveObjects); + const resolvedMinValue = resolveWidgetFieldValue(minValue, undefined, customResolveObjects); if (resolvedMinValue !== undefined) { if (widgetValue === undefined || widgetValue < parseFloat(resolvedMinValue)) { return { @@ -395,12 +409,12 @@ export function validateWidget({ validationObject, widgetValue, currentState, cu } } - const resolvedCustomRule = resolveWidgetFieldValue(customRule, currentState, false, customResolveObjects); + const resolvedCustomRule = resolveWidgetFieldValue(customRule, false, customResolveObjects); if (typeof resolvedCustomRule === 'string' && resolvedCustomRule !== '') { return { isValid: false, validationError: resolvedCustomRule }; } - const resolvedMandatory = resolveWidgetFieldValue(mandatory, currentState, false, customResolveObjects); + const resolvedMandatory = resolveWidgetFieldValue(mandatory, false, customResolveObjects); if (resolvedMandatory == true) { if (!widgetValue) { diff --git a/frontend/src/_hoc/withProfiler.jsx b/frontend/src/_hoc/withProfiler.jsx new file mode 100644 index 0000000000..9c6bc58623 --- /dev/null +++ b/frontend/src/_hoc/withProfiler.jsx @@ -0,0 +1,18 @@ +import React, { Profiler } from 'react'; + +export const withProfiler = (WrappedComponent) => (props) => { + function onRenderCallback(id, phase, actualDuration, baseDuration, startTime, commitTime) { + const markName = `⚛ ${id} (${phase})`; + performance.measure(markName, { + end: commitTime, + start: startTime, + }); + performance.clearMeasures(markName); + } + + return ( + + + + ); +}; diff --git a/frontend/src/_hooks/useRenderCount.js b/frontend/src/_hooks/useRenderCount.js new file mode 100644 index 0000000000..3ad64f6b9e --- /dev/null +++ b/frontend/src/_hooks/useRenderCount.js @@ -0,0 +1,18 @@ +import { useRef, useEffect } from 'react'; + +function useRenderCount(componentName) { + const renderCountRef = useRef(0); + + useEffect(() => { + return () => { + console.log(`--Component ${componentName} rendered unmounting ${renderCountRef.current} times.`); + }; + }, []); + + renderCountRef.current++; + + console.log(`CountingRender- Component ${componentName} rendered ${renderCountRef.current} times.`); + return renderCountRef.current; +} + +export default useRenderCount; diff --git a/frontend/src/_stores/appDataStore.js b/frontend/src/_stores/appDataStore.js index 2159f6cf84..f8227587c1 100644 --- a/frontend/src/_stores/appDataStore.js +++ b/frontend/src/_stores/appDataStore.js @@ -56,7 +56,7 @@ export const useAppDataStore = create( let updateDiff = appDefinitionDiff.updateDiff; if (appDefinitionDiff.operation === 'update') { - updateDiff = useResolveStore.getState().actions.findAndReplaceReferences(updateDiff); + updateDiff = useResolveStore.getState().actions.findReferences(updateDiff); } appVersionService @@ -91,7 +91,7 @@ export const useAppDataStore = create( const appId = get().appId; const versionId = get().currentVersionId; - const entityIdMappingData = useResolveStore.getState().actions.findAndReplaceReferences(events); + const entityIdMappingData = useResolveStore.getState().actions.findReferences(events); const response = await appVersionService.saveAppVersionEventHandlers( appId, @@ -111,9 +111,13 @@ export const useAppDataStore = create( } }); - const entityReferencesInEvents = findAllEntityReferences(updatedEvents, [])?.filter( - (entity) => entity && isValidUUID(entity) - ); + const entityReferencesInEvents = findAllEntityReferences(updatedEvents, []) + ?.map((entity) => { + if (entity && isValidUUID(entity)) { + return entity; + } + }) + ?.filter((e) => e !== undefined); const manager = useResolveStore.getState().referenceMapper; let newEvents = JSON.parse(JSON.stringify(updatedEvents)); diff --git a/frontend/src/_stores/currentStateStore.js b/frontend/src/_stores/currentStateStore.js index cfaeb1d72a..bb7af52df0 100644 --- a/frontend/src/_stores/currentStateStore.js +++ b/frontend/src/_stores/currentStateStore.js @@ -1,9 +1,11 @@ import { shallow } from 'zustand/shallow'; import { create, zustandDevTools } from './utils'; -import _, { omit } from 'lodash'; +import _, { debounce, merge, omit } from 'lodash'; import { useResolveStore } from './resolverStore'; // eslint-disable-next-line import/no-unresolved import { diff } from 'deep-object-diff'; +import { useEditorStore } from './editorStore'; +import { handleLowPriorityWork } from '@/_helpers/editorHelpers'; const initialState = { queries: {}, @@ -24,6 +26,24 @@ const initialState = { isEditorReady: false, }; +function generatePath(obj, targetKey, currentPath = '') { + for (const key in obj) { + const newPath = currentPath ? currentPath + '.' + key : key; + + if (key === targetKey) { + return newPath; + } + + if (typeof obj[key] === 'object' && obj[key] !== null) { + const result = generatePath(obj[key], targetKey, newPath); + if (result) { + return result; + } + } + } + return null; +} + export const useCurrentStateStore = create( zustandDevTools( (set, get) => ({ @@ -68,19 +88,20 @@ useCurrentStateStore.subscribe((state) => { const isStoreIntialized = useResolveStore.getState().storeReady; if (!isStoreIntialized) { - useResolveStore.getState().actions.updateAppSuggestions({ - queries: state.queries, - components: state.components, - globals: state.globals, - page: state.page, - variables: state.variables, - client: state.client, - server: state.server, - constants: state.constants, + handleLowPriorityWork(() => { + useResolveStore.getState().actions.updateAppSuggestions({ + queries: state.queries, + components: state.components, + globals: state.globals, + page: state.page, + variables: state.variables, + client: state.client, + server: state.server, + constants: state.constants, + }); }); - useResolveStore.getState().actions.updateStoreState({ storeReady: true }); console.log('Resolver store initialized with current state.'); - return; + return useResolveStore.getState().actions.updateStoreState({ storeReady: true }); } }, shallow); diff --git a/frontend/src/_stores/dataQueriesStore.js b/frontend/src/_stores/dataQueriesStore.js index 944647000d..670ac6346a 100644 --- a/frontend/src/_stores/dataQueriesStore.js +++ b/frontend/src/_stores/dataQueriesStore.js @@ -1,9 +1,11 @@ -import { create, findAllEntityReferences, zustandDevTools } from './utils'; +import { create, zustandDevTools } from './utils'; import { getDefaultOptions } from './storeHelper'; import { dataqueryService } from '@/_services'; +// import debounce from 'lodash/debounce'; import { useAppDataStore } from '@/_stores/appDataStore'; import { useQueryPanelStore } from '@/_stores/queryPanelStore'; import { useAppVersionStore } from '@/_stores/appVersionStore'; +import { runQueries } from '@/_helpers/appUtils'; import { v4 as uuidv4 } from 'uuid'; import { toast } from 'react-hot-toast'; import _, { isEmpty, throttle } from 'lodash'; @@ -25,7 +27,7 @@ const initialState = { isUpdatingQueryInProcess: false, /** When a 'Create Data Query' operation is in progress, rename/update API calls are cached in the variable. */ queuedActions: {}, - queuedQueriesForRunOnAppLoad: [], + // queuedQueriesForRunOnAppLoad: [], }; export const useDataQueriesStore = create( @@ -35,7 +37,7 @@ export const useDataQueriesStore = create( actions: { // TODO: Remove editor state while changing currentState fetchDataQueries: async (appVersionId, selectFirstQuery = false, runQueriesOnAppLoad = false, ref) => { - set({ loadingDataQueries: true }); + get().loadingDataQueries && set({ loadingDataQueries: true }); const data = await dataqueryService.getAll(appVersionId); const diff = _.differenceWith(data.data_queries, get().dataQueries, _.isEqual); @@ -73,7 +75,20 @@ export const useDataQueriesStore = create( const currentQueries = useCurrentStateStore.getState().queries; data.data_queries.forEach(({ id, name, options }) => { - updatedQueries[name] = _.merge(currentQueries[name], { id: id, isLoading: false, data: [], rawData: [] }); + if (runQueriesOnAppLoad && options.runOnPageLoad) { + updatedQueries[name] = _.merge(currentQueries[name], { + id: id, + isLoading: true, + data: [], + rawData: [], + }); + } else + updatedQueries[name] = _.merge(currentQueries[name], { + id: id, + isLoading: false, + data: [], + rawData: [], + }); if (options && options?.requestConfirmation && options?.runOnPageLoad) { queryConfirmationList.push({ queryId: id, queryName: name }); @@ -91,23 +106,18 @@ export const useDataQueriesStore = create( } // Runs query on loading application - if (runQueriesOnAppLoad) { - set({ queuedQueriesForRunOnAppLoad: data.data_queries }); - } + // if (runQueriesOnAppLoad) { + // set({ queuedQueriesForRunOnAppLoad: data.data_queries }); + // } }, setDataQueries: (dataQueries, type = 'initial') => { set({ dataQueries }); if (type === 'mappingUpdate') { const { actions } = useQueryPanelStore.getState(); + actions.setSelectedQuery(null); + const queryId = dataQueries[0]?.id; - return new Promise((resolve) => { - actions.setSelectedQuery(null); - resolve(); - }).then(() => { - const queryId = dataQueries[0]?.id; - - actions.setSelectedQuery(queryId); - }); + actions.setSelectedQuery(queryId); } }, deleteDataQueries: (queryId) => { @@ -141,30 +151,6 @@ export const useDataQueriesStore = create( }, {}), }); - const newAppDefinition = JSON.parse(JSON.stringify(useEditorStore.getState().appDefinition)); - const currentPageId = useEditorStore.getState().currentPageId; - const currentComponents = newAppDefinition.pages[currentPageId].components; - - const newComponentDefinition = useResolveStore - .getState() - .actions.findAndReplaceReferences(currentComponents, [deletedQueryName]); - const entityReferencesInQuerries = findAllEntityReferences(get().dataQueries, []); - - if (entityReferencesInQuerries.length > 0) { - const newDataQueries = useResolveStore - .getState() - .actions.findAndReplaceReferences(get().dataQueries, [deletedQueryName]); - - get().actions.setDataQueries(newDataQueries, 'mappingUpdate'); - } - - newAppDefinition.pages[currentPageId].components = newComponentDefinition; - - useEditorStore.getState().actions.updateEditorState({ - appDefinition: newAppDefinition, - isUpdatingEditorStateInProcess: true, - }); - const referenceManager = useResolveStore.getState().referenceMapper; referenceManager.delete(queryId); @@ -491,7 +477,7 @@ export const useDataQueriesStore = create( set({ queuedActions: { ...get().queuedActions, saveData: newValues } }); return; } - const entityIdMappedOptions = useResolveStore.getState().actions.findAndReplaceReferences(newValues?.options); + const entityIdMappedOptions = useResolveStore.getState().actions.findReferences(newValues?.options); useAppDataStore.getState().actions.setIsSaving(true); set({ isUpdatingQueryInProcess: true }); @@ -565,9 +551,9 @@ export const useDataQueriesStore = create( useAppDataStore.getState().actions.setIsSaving(false); }); }, - clearQueuedQueriesForRunOnAppLoad: () => { - set({ queuedQueriesForRunOnAppLoad: [] }); - }, + // clearQueuedQueriesForRunOnAppLoad: () => { + // set({ queuedQueriesForRunOnAppLoad: [] }); + // }, updateQueryOptionsState: (queryOptionsList) => { set({ isUpdatingQueryInProcess: true }); const { actions, selectedQuery } = useQueryPanelStore.getState(); diff --git a/frontend/src/_stores/editorStore.js b/frontend/src/_stores/editorStore.js index d71f99d9da..cc540c6992 100644 --- a/frontend/src/_stores/editorStore.js +++ b/frontend/src/_stores/editorStore.js @@ -1,10 +1,7 @@ import _ from 'lodash'; import { create } from './utils'; import { v4 as uuid } from 'uuid'; -import { useAppDataStore } from './appDataStore'; import { useResolveStore } from './resolverStore'; -import { shallow } from 'zustand/shallow'; -import { useCurrentState, useCurrentStateStore } from './currentStateStore'; const STORE_NAME = 'Editor'; export const EMPTY_ARRAY = []; @@ -27,11 +24,6 @@ const initialState = { selectedComponents: EMPTY_ARRAY, isEditorActive: false, selectedComponent: null, - scrollOptions: { - container: null, - throttleTime: 0, - threshold: 0, - }, canUndo: false, canRedo: false, currentVersion: {}, @@ -45,6 +37,7 @@ const initialState = { queryConfirmationList: [], currentPageId: null, currentSessionId: uuid(), + componentsNeedsUpdateOnNextRender: [], }; export const useEditorStore = create( @@ -70,6 +63,20 @@ export const useEditorStore = create( }, setIsEditorActive: (isEditorActive) => set(() => ({ isEditorActive })), updateEditorState: (state) => set((prev) => ({ ...prev, ...state })), + updateCurrentStateDiff: (currentStateDiff) => set(() => ({ currentStateDiff })), + updateComponentsNeedsUpdateOnNextRender: (componentsNeedsUpdateOnNextRender) => { + set(() => ({ componentsNeedsUpdateOnNextRender })); + }, + flushComponentsNeedsUpdateOnNextRender: (toRemoveIds = []) => { + const currentComponents = get().componentsNeedsUpdateOnNextRender; + + if (currentComponents.length === 0 || toRemoveIds.length === 0) return; + + const updatedComponents = currentComponents.filter((item) => !toRemoveIds.includes(item)); + + set(() => ({ componentsNeedsUpdateOnNextRender: updatedComponents })); + }, + updateQueryConfirmationList: (queryConfirmationList) => set({ queryConfirmationList }), setHoveredComponent: (hoveredComponent) => set({ hoveredComponent }, false, { @@ -102,3 +109,14 @@ export const useEditorStore = create( export const useEditorActions = () => useEditorStore((state) => state.actions); export const useEditorState = () => useEditorStore((state) => state); + +export const getComponentsToRenders = () => { + return useEditorStore.getState().componentsNeedsUpdateOnNextRender; +}; + +export const flushComponentsToRender = (componentIds = []) => { + if (!componentIds.length) return; + + useEditorStore.getState().actions.flushComponentsNeedsUpdateOnNextRender(componentIds); + useResolveStore.getState().actions.getLastUpdatedRefs(); +}; diff --git a/frontend/src/_stores/gridStore.js b/frontend/src/_stores/gridStore.js index 0e716577f9..919d09a784 100644 --- a/frontend/src/_stores/gridStore.js +++ b/frontend/src/_stores/gridStore.js @@ -12,6 +12,7 @@ const initialState = { isGroupHandleHoverd: false, idGroupDragged: false, openModalWidgetId: null, + subContainerWidths: {}, }; export const useGridStore = create( @@ -28,6 +29,8 @@ export const useGridStore = create( setIsGroupHandleHoverd: (isGroupHandleHoverd) => set({ isGroupHandleHoverd }), setIdGroupDragged: (idGroupDragged) => set({ idGroupDragged }), setOpenModalWidgetId: (openModalWidgetId) => set({ openModalWidgetId }), + setSubContainerWidths: (id, width) => + set((state) => ({ subContainerWidths: { ...state.subContainerWidths, [id]: width } })), }, }), { name: 'Grid Store' } diff --git a/frontend/src/_stores/queryPanelStore.js b/frontend/src/_stores/queryPanelStore.js index 83187b80e5..7b28ae84b8 100644 --- a/frontend/src/_stores/queryPanelStore.js +++ b/frontend/src/_stores/queryPanelStore.js @@ -1,5 +1,5 @@ import { create, zustandDevTools } from './utils'; - +import { shallow } from 'zustand/shallow'; import { useDataQueriesStore } from '@/_stores/dataQueriesStore'; const queryManagerPreferences = JSON.parse(localStorage.getItem('queryManagerPreferences')) ?? {}; diff --git a/frontend/src/_stores/resolverStore.js b/frontend/src/_stores/resolverStore.js index 865ac28c54..48c6b94fcd 100644 --- a/frontend/src/_stores/resolverStore.js +++ b/frontend/src/_stores/resolverStore.js @@ -58,7 +58,7 @@ const initialState = { hints: {}, resolvedRefs: {}, }, - + lastUpdatedRefs: [], referenceMapper: new ReferencesBiMap(), }; @@ -78,6 +78,16 @@ export const useResolveStore = create( })); }, + flushLastUpdatedRefs: () => { + set(() => ({ lastUpdatedRefs: [] })); + }, + getLastUpdatedRefs: () => { + return get().lastUpdatedRefs; + }, + //for queries references used in component definitons + updateLastUpdatedRefs: (updatedRefs) => { + set(() => ({ lastUpdatedRefs: updatedRefs })); + }, addAppSuggestions: (partialRefState) => { if (Object.keys(partialRefState).length === 0) return; @@ -86,6 +96,8 @@ export const useResolveStore = create( const lookupHintsMap = new Map([...get().lookupTable.hints]); const lookupResolvedRefs = new Map([...get().lookupTable.resolvedRefs]); + const newUpdatedrefs = []; + hintsMap.forEach((value, key) => { const alreadyExists = lookupHintsMap.has(key); @@ -97,6 +109,7 @@ export const useResolveStore = create( resolvedRefs.delete(value); resolvedRefs.set(existingLookupId, newResolvedRef); + newUpdatedrefs.push(key); } }); @@ -118,6 +131,7 @@ export const useResolveStore = create( hints: lookupHintsMap, resolvedRefs: lookupResolvedRefs, }, + lastUpdatedRefs: newUpdatedrefs, })); }, @@ -158,6 +172,35 @@ export const useResolveStore = create( }); }, + updateResolvedRefsOfHints: (resolvedRefs = []) => { + const lookupResolvedRefs = new Map([...get().lookupTable.resolvedRefs]); + const hintsMap = new Map([...get().lookupTable.hints]); + + const updatedList = []; + + resolvedRefs.forEach((ref) => { + if (!ref.hint || !ref.newRef || !hintsMap.has(ref.hint)) return; + + const refId = hintsMap.get(ref.hint); + const currentRef = lookupResolvedRefs.get(refId); + + if (currentRef !== ref.newRef) { + lookupResolvedRefs.set(refId, ref.newRef); + updatedList.push(ref.hint); + } + }); + + if (updatedList.length > 0) { + set(() => ({ + lookupTable: { + ...get().lookupTable, + resolvedRefs: lookupResolvedRefs, + }, + lastUpdatedRefs: updatedList, + })); + } + }, + updateJSHints: () => { const hints = createJavaScriptSuggestions(); set(() => ({ suggestions: { ...get().suggestions, jsHints: hints } })); @@ -185,19 +228,8 @@ export const useResolveStore = create( return get().referenceMapper; }, - getEntityId: (entityName) => { - const { referenceMapper } = get(); - - for (const [key, value] of referenceMapper._map) { - if (value === entityName) { - return referenceMapper.reverseGet(key); - } - } - }, - - findAndReplaceReferences: (obj, targetEntityNames = []) => { - const entityNameReferences = - targetEntityNames.length === 0 ? findAllEntityReferences(obj, []) : targetEntityNames; + findReferences: (obj) => { + const entityNameReferences = findAllEntityReferences(obj, []); if (entityNameReferences.length === 0) return obj; diff --git a/frontend/src/_stores/utils.js b/frontend/src/_stores/utils.js index 9f89e41497..e7d2df97e4 100644 --- a/frontend/src/_stores/utils.js +++ b/frontend/src/_stores/utils.js @@ -465,7 +465,12 @@ export function createReferencesLookup(refState, forQueryParams = false, initalL } if (_type === 'Array') { map.set(newPath, { type: _type }); - buildMap(value, newPath); + + if (path.startsWith('queries') && key === 'data' && value.length > 2) { + // do nothing + } else { + buildMap(value, newPath); + } } else { map.set(newPath, { type: _type }); } diff --git a/frontend/src/index.jsx b/frontend/src/index.jsx index 92d995444f..6b87da50a1 100755 --- a/frontend/src/index.jsx +++ b/frontend/src/index.jsx @@ -1,6 +1,6 @@ import React from 'react'; import { render } from 'react-dom'; -// import { createRoot } from 'react-dom/client'; + import * as Sentry from '@sentry/react'; import { useLocation, useNavigationType, createRoutesFromChildren, matchRoutes } from 'react-router-dom'; import { appService } from '@/_services'; @@ -8,7 +8,7 @@ import { App } from './App'; // eslint-disable-next-line import/no-unresolved import i18n from 'i18next'; import { initReactI18next } from 'react-i18next'; -// import LanguageDetector from 'i18next-browser-languagedetector'; + import Backend from 'i18next-http-backend'; const AppWithProfiler = Sentry.withProfiler(App); diff --git a/frontend/src/wdyr.js b/frontend/src/wdyr.js new file mode 100644 index 0000000000..83c7d54a58 --- /dev/null +++ b/frontend/src/wdyr.js @@ -0,0 +1,8 @@ +import React from 'react'; + +if (process.env.NODE_ENV === 'development') { + const whyDidYouRender = require('@welldone-software/why-did-you-render'); + whyDidYouRender(React, { + trackAllPureComponents: true, + }); +} diff --git a/server/src/helpers/import_export.helpers.ts b/server/src/helpers/import_export.helpers.ts deleted file mode 100644 index db9b02ddec..0000000000 --- a/server/src/helpers/import_export.helpers.ts +++ /dev/null @@ -1,53 +0,0 @@ -export function updateEntityReferences(node, resourceMapping: Record = {}) { - if (typeof node === 'object') { - for (const key in node) { - let value = node[key]; - if (typeof value === 'string' && value.includes('{{') && value.includes('}}')) { - const referenceExists = value; - - if (referenceExists) { - const ref = value.replace('{{', '').replace('}}', ''); - - const entityName = ref.split('.')[1]; - - if (resourceMapping[entityName]) { - const newValue = value.replace(entityName, resourceMapping[entityName]); - - node[key] = newValue; - } - } - } else if (typeof value === 'object') { - value = updateEntityReferences(value, resourceMapping); - } - } - } - - return node; -} - -export function findAllEntityReferences(node, allRefs): [] { - if (typeof node === 'object') { - for (const key in node) { - const value = node[key]; - if (typeof value === 'string' && value.includes('{{') && value.includes('}}')) { - const referenceExists = value; - - if (referenceExists) { - const ref = value.replace('{{', '').replace('}}', ''); - - const entityName = ref.split('.')[1]; - - allRefs.push(entityName); - } - } else if (typeof value === 'object') { - findAllEntityReferences(value, allRefs); - } - } - } - return allRefs; -} - -export function isValidUUID(uuid) { - const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; - return uuidRegex.test(uuid); -} diff --git a/server/src/services/app_import_export.service.ts b/server/src/services/app_import_export.service.ts index c432c280d7..d30e0f936e 100644 --- a/server/src/services/app_import_export.service.ts +++ b/server/src/services/app_import_export.service.ts @@ -32,7 +32,6 @@ import { Component } from 'src/entities/component.entity'; import { Layout } from 'src/entities/layout.entity'; import { EventHandler, Target } from 'src/entities/event_handler.entity'; import { v4 as uuid } from 'uuid'; -import { findAllEntityReferences, isValidUUID, updateEntityReferences } from 'src/helpers/import_export.helpers'; interface AppResourceMappings { defaultDataSourceIdMapping: Record; @@ -238,20 +237,21 @@ export class AppImportExportService { ? true : isTooljetVersionWithNormalizedAppDefinitionSchem(importedAppTooljetVersion); + const shouldUpdateForGridCompatibility = !cloning; + const importedApp = await this.createImportedAppForUser(this.entityManager, schemaUnifiedAppParams, user); - const resourceMapping = await this.setupImportedAppAssociations( + await this.setupImportedAppAssociations( this.entityManager, importedApp, schemaUnifiedAppParams, user, externalResourceMappings, isNormalizedAppDefinitionSchema, + shouldUpdateForGridCompatibility, tooljetVersion ); - await this.createAdminGroupPermissions(this.entityManager, importedApp); - await this.updateEntityReferencesForImportedApp(this.entityManager, resourceMapping); // NOTE: App slug updation callback doesn't work while wrapped in transaction // hence updating slug explicitly @@ -262,64 +262,6 @@ export class AppImportExportService { return importedApp; } - async updateEntityReferencesForImportedApp(manager: EntityManager, resourceMapping: AppResourceMappings) { - const mappings = { ...resourceMapping.componentsMapping, ...resourceMapping.dataQueryMapping }; - const newComponentIds = Object.values(resourceMapping.componentsMapping); - const newQueriesIds = Object.values(resourceMapping.dataQueryMapping); - - if (newComponentIds.length > 0) { - const components = await manager - .createQueryBuilder(Component, 'components') - .where('components.id IN(:...componentIds)', { componentIds: newComponentIds }) - .select([ - 'components.id', - 'components.properties', - 'components.styles', - 'components.general', - 'components.validation', - 'components.generalStyles', - 'components.displayPreferences', - ]) - .getMany(); - - const toUpdateComponents = components.filter((component) => { - const entityReferencesInComponentDefinitions = findAllEntityReferences(component, []).filter( - (entity) => entity && isValidUUID(entity) - ); - - if (entityReferencesInComponentDefinitions.length > 0) { - return updateEntityReferences(component, mappings); - } - }); - - if (!isEmpty(toUpdateComponents)) { - await manager.save(toUpdateComponents); - } - } - - if (newQueriesIds.length > 0) { - const dataQueries = await manager - .createQueryBuilder(DataQuery, 'dataQueries') - .where('dataQueries.id IN(:...dataQueryIds)', { dataQueryIds: newQueriesIds }) - .select(['dataQueries.id', 'dataQueries.options']) - .getMany(); - - const toUpdateDataQueries = dataQueries.filter((dataQuery) => { - const entityReferencesInQueryOptions = findAllEntityReferences(dataQuery, []).filter( - (entity) => entity && isValidUUID(entity) - ); - - if (entityReferencesInQueryOptions.length > 0) { - return updateEntityReferences(dataQuery, mappings); - } - }); - - if (!isEmpty(toUpdateDataQueries)) { - await manager.save(toUpdateDataQueries); - } - } - } - async createImportedAppForUser(manager: EntityManager, appParams: any, user: User): Promise { return await catchDbException(async () => { const importedApp = manager.create(App, { @@ -386,6 +328,7 @@ export class AppImportExportService { user: User, externalResourceMappings: Record, isNormalizedAppDefinitionSchema: boolean, + shouldUpdateForGridCompatibility: boolean, tooljetVersion: string ) { // Old version without app version @@ -450,6 +393,7 @@ export class AppImportExportService { importingPages, importingComponents, importingEvents, + shouldUpdateForGridCompatibility, tooljetVersion ); @@ -655,6 +599,7 @@ export class AppImportExportService { importingPages: Page[], importingComponents: Component[], importingEvents: EventHandler[], + shouldUpdateForGridCompatibility: boolean, tooljetVersion: string ): Promise { appResourceMappings = { ...appResourceMappings }; @@ -949,6 +894,18 @@ export class AppImportExportService { ); } + // const appVersionIds = Object.values(appResourceMappings.appVersionMapping); + + // for (const appVersionId of appVersionIds) { + // await this.updateEventActionsForNewVersionWithNewMappingIds( + // manager, + // appVersionId, + // appResourceMappings.dataQueryMapping, + // appResourceMappings.componentsMapping, + // appResourceMappings.pagesMapping + // ); + // } + return appResourceMappings; } @@ -1699,10 +1656,9 @@ export class AppImportExportService { .createQueryBuilder(EventHandler, 'event') .where('event.appVersionId = :versionId', { versionId }) .getMany(); - const mappings = { ...oldDataQueryToNewMapping, ...oldComponentToNewComponentMapping } as Record; for (const event of allEvents) { - const eventDefinition = updateEntityReferences(event.event, mappings); + const eventDefinition = event.event; if (eventDefinition?.actionId === 'run-query' && oldDataQueryToNewMapping[eventDefinition.queryId]) { eventDefinition.queryId = oldDataQueryToNewMapping[eventDefinition.queryId]; From 757775edaa51368f34ef81017066d1ec8b0658e3 Mon Sep 17 00:00:00 2001 From: Kiran Ashok Date: Fri, 5 Apr 2024 16:08:32 +0530 Subject: [PATCH 02/11] Bugfixes :: Codehinter (#9306) * fix : If the query panel is small in size the suggestions are not clearly visible. * fix : Not able to popout the runjs query editor field if it has larger data. * fix : scrolling inside the codehinter should only scroll within codehinter (query panel) * fix : If runjs returns string it is parsing as an array on codehinter * fix : align placeholders of inputfields to vertically center of the field. * fix : Code hinter says something went wrong for ds and when focusing and clicking out without typing * fixes * update : resolve multi reference null check * remove braces from fx preview box * suggestion for fx in with curly brace * fix : We should be able to save empty fields, currently it is not possible. * resolve fx codehinter in preview * remove all fxActive checks and fix codehinter suggestion position * CLEANUP * fix :: to fix parameter suggestion UI/UX/ + on mouse click data is not saving , eventmanager, add paramater --- frontend/src/Editor/CodeEditor/CodeHinter.jsx | 3 +- frontend/src/Editor/CodeEditor/PreviewBox.jsx | 23 +++----------- .../CodeEditor/SingleLineCodeEditor.jsx | 31 +++++++------------ .../CodeEditor/autocompleteExtensionConfig.js | 9 +++--- frontend/src/Editor/CodeEditor/styles.scss | 27 ++++++++++++++-- frontend/src/Editor/CodeEditor/utils.js | 13 ++++---- .../src/Editor/Inspector/EventManager.jsx | 4 +-- .../Components/ParameterDetails.jsx | 8 ++--- frontend/src/_helpers/appUtils.js | 8 +++++ frontend/src/_stores/resolverStore.js | 4 +-- frontend/src/_stores/utils.js | 7 ++++- 11 files changed, 73 insertions(+), 64 deletions(-) diff --git a/frontend/src/Editor/CodeEditor/CodeHinter.jsx b/frontend/src/Editor/CodeEditor/CodeHinter.jsx index 70453a4895..39d615384c 100644 --- a/frontend/src/Editor/CodeEditor/CodeHinter.jsx +++ b/frontend/src/Editor/CodeEditor/CodeHinter.jsx @@ -88,8 +88,7 @@ const Portal = ({ children, ...restProps }) => { const PopupIcon = ({ callback, icon, tip, position, isMultiEditor = false }) => { const size = 16; const topRef = isNumber(position?.height) ? Math.floor(position?.height) - 30 : 32; - let top = isMultiEditor ? 370 : topRef > 32 ? topRef : 0; - + let top = isMultiEditor ? 270 : topRef > 32 ? topRef : 0; return (
{ +export const PreviewBox = ({ currentValue, validationSchema, setErrorStateActive, componentId, setErrorMessage }) => { const { variablesExposedForPreview } = useContext(EditorContext); const customVariables = variablesExposedForPreview?.[componentId] ?? {}; @@ -27,10 +20,8 @@ export const PreviewBox = ({ const [resolvedValue, setResolvedValue] = useState(''); const [error, setError] = useState(null); const [coersionData, setCoersionData] = useState(null); - const getPreviewContent = (content, type) => { if (!content) return currentValue; - try { switch (type) { case 'Object': @@ -75,12 +66,7 @@ export const PreviewBox = ({ }, [error]); useEffect(() => { - const [valid, _error, newValue, resolvedValue] = resolveReferences( - currentValue, - validationSchema, - customVariables, - fxActive - ); + const [valid, _error, newValue, resolvedValue] = resolveReferences(currentValue, validationSchema, customVariables); if (!validationSchema || isEmpty(validationSchema)) { return setResolvedValue(newValue); @@ -345,7 +331,6 @@ const PreviewCodeBlock = ({ code, isExpectValue = false }) => {
); } - return (
 {
           padding: '0',
         }}
       >
-        {prettyPrintedJson}
+        {prettyPrintedJson?.startsWith('{{')
+          ? prettyPrintedJson?.replace(/{{/g, '').replace(/}}/g, '')
+          : prettyPrintedJson}
       
); diff --git a/frontend/src/Editor/CodeEditor/SingleLineCodeEditor.jsx b/frontend/src/Editor/CodeEditor/SingleLineCodeEditor.jsx index 2c3c9403ef..0b620851be 100644 --- a/frontend/src/Editor/CodeEditor/SingleLineCodeEditor.jsx +++ b/frontend/src/Editor/CodeEditor/SingleLineCodeEditor.jsx @@ -34,15 +34,9 @@ const SingleLineCodeEditor = ({ suggestions, componentName, fieldMeta = {}, fxAc useEffect(() => { if (typeof initialValue !== 'string') return; - - if (fxActive && initialValue?.startsWith('{{')) { - const _value = initialValue?.replace(/{{/g, '').replace(/}}/g, ''); - return setCurrentValue(_value); - } - setCurrentValue(initialValue); // eslint-disable-next-line react-hooks/exhaustive-deps - }, [componentName, initialValue, fxActive]); + }, [componentName, initialValue]); useEffect(() => { const handleClickOutside = (event) => { @@ -80,7 +74,7 @@ const SingleLineCodeEditor = ({ suggestions, componentName, fieldMeta = {}, fxAc setErrorStateActive={setErrorStateActive} ignoreValidation={restProps?.ignoreValidation || isEmpty(validation)} componentId={restProps?.componentId ?? null} - fxActive={fxActive} + // fxActive={fxActive} isWorkspaceVariable={isWorkspaceVariable} errorStateActive={errorStateActive} previewPlacement={restProps?.cyLabel === 'canvas-bg-colour' ? 'top' : 'left-start'} @@ -99,7 +93,6 @@ const SingleLineCodeEditor = ({ suggestions, componentName, fieldMeta = {}, fxAc cyLabel={restProps.cyLabel} portalProps={portalProps} componentName={componentName} - fxActive={fxActive} {...restProps} />
@@ -124,7 +117,6 @@ const EditorInput = ({ renderPreview, portalProps, ignoreValidation, - fxActive, lang, isFocused, componentId, @@ -138,15 +130,15 @@ const EditorInput = ({ if (totalReferences > 1) { const currentWord = queryInput.split('{{').pop().split('}}')[0]; - queryInput = fxActive ? currentWord : `{{${currentWord}}}`; + queryInput = currentWord; } - let completions = getAutocompletion(queryInput, validationType, hints, fxActive, totalReferences); + let completions = getAutocompletion(queryInput, validationType, hints, totalReferences); return { from: word.from, options: completions, - validFor: !fxActive ? /^\{\{.*\}\}$/ : '', + validFor: /^\{\{.*\}\}$/, }; } @@ -168,18 +160,17 @@ const EditorInput = ({ setCurrentValue(val); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); - const handleOnBlur = React.useCallback(() => { setFirstTimeFocus(false); if (ignoreValidation) { return onBlurUpdate(currentValue); } - - if (!error) { - const _value = fxActive ? `{{${currentValue}}}` : currentValue; - - onBlurUpdate(_value); - } + setTimeout(() => { + if (!error || currentValue == '') { + const _value = currentValue; + onBlurUpdate(_value); + } + }, 0); // eslint-disable-next-line react-hooks/exhaustive-deps }, [currentValue, error]); diff --git a/frontend/src/Editor/CodeEditor/autocompleteExtensionConfig.js b/frontend/src/Editor/CodeEditor/autocompleteExtensionConfig.js index d06c49a1bf..8d034b9139 100644 --- a/frontend/src/Editor/CodeEditor/autocompleteExtensionConfig.js +++ b/frontend/src/Editor/CodeEditor/autocompleteExtensionConfig.js @@ -1,8 +1,7 @@ -export const getAutocompletion = (input, fieldType, hints, fxActive = false, totalReferences = 1) => { - if (!fxActive && (!input.startsWith('{{') || !input.endsWith('}}'))) return []; - - const actualInput = !fxActive ? input.replace(/{{|}}/g, '') : input; +export const getAutocompletion = (input, fieldType, hints, totalReferences = 1) => { + if (!input.startsWith('{{') || !input.endsWith('}}')) return []; + const actualInput = input.replace(/{{|}}/g, ''); let JSLangHints = []; if (fieldType) { @@ -50,7 +49,7 @@ export const getAutocompletion = (input, fieldType, hints, fxActive = false, tot if (autoSuggestionList.length === 0 && !cm.hint.includes(actualInput)) return true; }); - const suggestions = generateHints([...jsHints, ...autoSuggestionList], fxActive, totalReferences); + const suggestions = generateHints([...jsHints, ...autoSuggestionList], totalReferences); return orderSuggestions(suggestions, fieldType).map((cm, index) => ({ ...cm, boost: 100 - index })); }; diff --git a/frontend/src/Editor/CodeEditor/styles.scss b/frontend/src/Editor/CodeEditor/styles.scss index 3acc3a490c..f032ee08b0 100644 --- a/frontend/src/Editor/CodeEditor/styles.scss +++ b/frontend/src/Editor/CodeEditor/styles.scss @@ -24,6 +24,10 @@ } } +.cm-widgetBuffer { + display: none !important; +} + .cm-base-autocomplete { @@ -153,18 +157,35 @@ } } +.query-manager-sort-filter-popup { + .cm-base-autocomplete { + position: fixed !important; + top: 130px !important; + } +} +.canvas-codehinter-container { + .cm-base-autocomplete { + position: fixed !important; + top: 500px !important; + left: 38px !important; + } +} .widget-code-editor { height: 100%; .cm-content { - max-width: 220px !important; + max-width: 100% !important; white-space: pre-wrap; word-wrap: break-word; } } +.cm-placeholder { + width: 220px; +} + .code-hinter-wrapper { .cm-editor { min-height: 32px; @@ -224,6 +245,7 @@ .cm-tooltip-autocomplete { @extend .cm-base-autocomplete; + top: 0px !important; } } @@ -277,6 +299,7 @@ overflow-y: auto !important; overflow-x: hidden !important; padding: 2px !important; + overscroll-behavior: contain; } .cm-focused { @@ -315,8 +338,6 @@ .code-hinter-preview-card-body::-webkit-scrollbar { width: 4px !important; - /* for vertical scrollbars */ - ; } .code-hinter-preview-card-body::-webkit-scrollbar-track { diff --git a/frontend/src/Editor/CodeEditor/utils.js b/frontend/src/Editor/CodeEditor/utils.js index 7f06064f21..22dc1eef40 100644 --- a/frontend/src/Editor/CodeEditor/utils.js +++ b/frontend/src/Editor/CodeEditor/utils.js @@ -211,9 +211,8 @@ const resolveMultiDynamicReferences = (code, lookupTable) => { return resolvedValue; }; -export const resolveReferences = (query, validationSchema, customResolvers = {}, fxActive = false) => { +export const resolveReferences = (query, validationSchema, customResolvers = {}) => { if (!query || typeof query !== 'string') return [false, null, null]; - let resolvedValue = query; let error = null; @@ -226,20 +225,20 @@ export const resolveReferences = (query, validationSchema, customResolvers = {}, return [true, error, resolvedValue]; } - if (validationSchema && !fxActive && !query?.includes('{{') && !query?.includes('}}')) { + if (validationSchema && !query?.includes('{{') && !query?.includes('}}')) { const [valid, errors, newValue] = validateComponentProperty(query, validationSchema); return [valid, errors, newValue, resolvedValue]; } - const hasMultiDynamicVariables = getDynamicVariables(query); + const hasMultiDynamicVariables = getDynamicVariables(query)?.length > 1; const { lookupTable } = useResolveStore.getState(); - if (isEmpty(validationSchema) && hasMultiDynamicVariables) { + if (hasMultiDynamicVariables) { resolvedValue = resolveMultiDynamicReferences(query, lookupTable); } else { - let value = !fxActive ? query?.replace(/{{|}}/g, '').trim() : query; + let value = query?.replace(/{{|}}/g, '').trim(); - if (fxActive && (value.startsWith('#') || value.includes('table-'))) { + if (value.startsWith('#') || value.includes('table-')) { value = JSON.stringify(value); } const { toResolveReference, jsExpression, jsExpMatch } = inferJSExpAndReferences(value, lookupTable.hints); diff --git a/frontend/src/Editor/Inspector/EventManager.jsx b/frontend/src/Editor/Inspector/EventManager.jsx index ae2bd7ac77..b723f17180 100644 --- a/frontend/src/Editor/Inspector/EventManager.jsx +++ b/frontend/src/Editor/Inspector/EventManager.jsx @@ -108,10 +108,10 @@ export const EventManager = ({ return { name: action.name, value: action.id }; }); - let checkIfClicksAreInsideOf = document.querySelector('#cm-complete-0'); + let checkIfClicksAreInsideOf = document.querySelector('.cm-completionListIncompleteBottom'); // Listen for click events on body if (checkIfClicksAreInsideOf) { - document.body.addEventListener('click', function (event) { + document.body.addEventListener('mousedown', function (event) { if (checkIfClicksAreInsideOf.contains(event.target)) { event.stopPropagation(); } diff --git a/frontend/src/Editor/QueryManager/Components/ParameterDetails.jsx b/frontend/src/Editor/QueryManager/Components/ParameterDetails.jsx index 528cc480f9..a92919eaca 100644 --- a/frontend/src/Editor/QueryManager/Components/ParameterDetails.jsx +++ b/frontend/src/Editor/QueryManager/Components/ParameterDetails.jsx @@ -21,19 +21,19 @@ const ParameterDetails = ({ darkMode, onSubmit, isEdit, name, defaultValue, onRe if ( showModal && event.target.closest('#parameter-form-popover') === null && - event.target.closest('#cm-complete-0') === null + event.target.closest('.cm-completionListIncompleteBottom') === null ) { closeMenu(); } }; if (showModal) { - document.addEventListener('mouseup', handleClickOutside); + document.addEventListener('click', handleClickOutside); } else { - document.removeEventListener('mouseup', handleClickOutside); + document.removeEventListener('click', handleClickOutside); } return () => { - document.removeEventListener('mouseup', handleClickOutside); + document.removeEventListener('click', handleClickOutside); }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [showModal]); diff --git a/frontend/src/_helpers/appUtils.js b/frontend/src/_helpers/appUtils.js index f6c185c1e3..beebecae5a 100644 --- a/frontend/src/_helpers/appUtils.js +++ b/frontend/src/_helpers/appUtils.js @@ -604,6 +604,9 @@ function executeActionWithDebounce(_ref, event, mode, customVariables) { const value = resolveReferences(event.value, getCurrentState(), undefined, customVariables); const customAppVariables = { ...getCurrentState().variables }; customAppVariables[key] = value; + useResolveStore.getState().actions.addAppSuggestions({ + variables: customAppVariables, + }); return useCurrentStateStore.getState().actions.setCurrentState({ variables: customAppVariables, }); @@ -619,6 +622,7 @@ function executeActionWithDebounce(_ref, event, mode, customVariables) { const key = resolveReferences(event.key, getCurrentState(), undefined, customVariables); const customAppVariables = { ...getCurrentState().variables }; delete customAppVariables[key]; + // useResolveStore.getState().actions.removeAppSuggestions(key); return useCurrentStateStore.getState().actions.setCurrentState({ variables: customAppVariables, }); @@ -631,6 +635,9 @@ function executeActionWithDebounce(_ref, event, mode, customVariables) { ...getCurrentState().page.variables, [key]: value, }; + useResolveStore.getState().actions.addAppSuggestions({ + variables: customPageVariables, + }); return useCurrentStateStore.getState().actions.setCurrentState({ page: { ...getCurrentState().page, @@ -650,6 +657,7 @@ function executeActionWithDebounce(_ref, event, mode, customVariables) { case 'unset-page-variable': { const key = resolveReferences(event.key, getCurrentState(), undefined, customVariables); const customPageVariables = _.omit(getCurrentState().page.variables, key); + // useResolveStore.getState().actions.removeAppSuggestions(key); return useCurrentStateStore.getState().actions.setCurrentState({ page: { ...getCurrentState().page, diff --git a/frontend/src/_stores/resolverStore.js b/frontend/src/_stores/resolverStore.js index 48c6b94fcd..4fc70a1d5b 100644 --- a/frontend/src/_stores/resolverStore.js +++ b/frontend/src/_stores/resolverStore.js @@ -136,13 +136,13 @@ export const useResolveStore = create( }, removeAppSuggestions: (suggestionsArray) => { - if (suggestionsArray.length === 0) return new Promise((resolve) => resolve({ status: '' })); + if (suggestionsArray?.length === 0) return new Promise((resolve) => resolve({ status: '' })); const lookupHintsMap = new Map([...get().lookupTable.hints]); const lookupResolvedRefs = new Map([...get().lookupTable.resolvedRefs]); const currentSuggestions = get().suggestions.appHints; - suggestionsArray.forEach((suggestion) => { + suggestionsArray?.forEach((suggestion) => { const index = currentSuggestions.findIndex((s) => s.hint === suggestion); if (index === -1) return; diff --git a/frontend/src/_stores/utils.js b/frontend/src/_stores/utils.js index e7d2df97e4..07899b3341 100644 --- a/frontend/src/_stores/utils.js +++ b/frontend/src/_stores/utils.js @@ -505,7 +505,12 @@ export function findAllEntityReferences(node, allRefs) { if (typeof node === 'object') { for (let key in node) { const value = node[key]; - if (typeof value === 'string' && value.includes('{{') && value.includes('}}')) { + if ( + typeof value === 'string' && + value.includes('{{') && + value.includes('}}') && + (value.startsWith('{{components') || value.startsWith('queries')) + ) { const referenceExists = value; if (referenceExists) { From 6500825c9543909597939c43348395f8a41b9795 Mon Sep 17 00:00:00 2001 From: Johnson Cherian Date: Fri, 5 Apr 2024 16:35:57 +0530 Subject: [PATCH 03/11] feat: set is editor ready in viewer --- frontend/src/Editor/Viewer.jsx | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/src/Editor/Viewer.jsx b/frontend/src/Editor/Viewer.jsx index 3b4276a7cc..d9395a907a 100644 --- a/frontend/src/Editor/Viewer.jsx +++ b/frontend/src/Editor/Viewer.jsx @@ -210,6 +210,7 @@ class ViewerComponent extends React.Component { computeComponentState(components).then(async () => { this.setState({ initialComputationOfStateDone: true, defaultComponentStateComputed: true }); + useCurrentStateStore.getState().actions.setEditorReady(true); this.runQueries(dataQueries); const currentPageEvents = this.state.events.filter( From 07d6a6cc7b3f45c3e1c1c45208bccfe69860fe56 Mon Sep 17 00:00:00 2001 From: arpitnath Date: Fri, 5 Apr 2024 17:25:19 +0530 Subject: [PATCH 04/11] clean up --- frontend/src/Editor/Editor.jsx | 34 +++++++++------------------- frontend/src/_helpers/appUtils.js | 16 +++++++------ frontend/src/_stores/appDataStore.js | 11 ++++----- 3 files changed, 24 insertions(+), 37 deletions(-) diff --git a/frontend/src/Editor/Editor.jsx b/frontend/src/Editor/Editor.jsx index 25d087a3aa..8c2639cc87 100644 --- a/frontend/src/Editor/Editor.jsx +++ b/frontend/src/Editor/Editor.jsx @@ -792,29 +792,17 @@ const EditorComponent = (props) => { let dataQueries = JSON.parse(JSON.stringify(useDataQueriesStore.getState().dataQueries)); let allEvents = JSON.parse(JSON.stringify(useAppDataStore.getState().events)); - const entityReferencesInComponentDefinitions = findAllEntityReferences(currentComponents, []) - ?.map((entity) => { - if (entity && isValidUUID(entity)) { - return entity; - } - }) - ?.filter((e) => e !== undefined); + const entityReferencesInComponentDefinitions = findAllEntityReferences(currentComponents, [])?.filter( + (entity) => entity && isValidUUID(entity) + ); - const entityReferencesInQueryoOptions = findAllEntityReferences(dataQueries, []) - ?.map((entity) => { - if (entity && isValidUUID(entity)) { - return entity; - } - }) - ?.filter((e) => e !== undefined); + const entityReferencesInQueryOptions = findAllEntityReferences(dataQueries, [])?.filter( + (entity) => entity && isValidUUID(entity) + ); - const entityReferencesInEvents = findAllEntityReferences(allEvents, []) - ?.map((entity) => { - if (entity && isValidUUID(entity)) { - return entity; - } - }) - ?.filter((e) => e !== undefined); + const entityReferencesInEvents = findAllEntityReferences(allEvents, [])?.filter( + (entity) => entity && isValidUUID(entity) + ); const manager = useResolveStore.getState().referenceMapper; @@ -843,14 +831,14 @@ const EditorComponent = (props) => { }); } - if (Array.isArray(entityReferencesInQueryoOptions) && entityReferencesInQueryoOptions?.length > 0) { + if (Array.isArray(entityReferencesInQueryOptions) && entityReferencesInQueryOptions?.length > 0) { let newQueryOptions = {}; dataQueries?.forEach((query) => { newQueryOptions[query.id] = query.options; ``; }); - entityReferencesInQueryoOptions.forEach((entity) => { + entityReferencesInQueryOptions.forEach((entity) => { const entityrefExists = manager.has(entity); if (entityrefExists) { diff --git a/frontend/src/_helpers/appUtils.js b/frontend/src/_helpers/appUtils.js index beebecae5a..e1ddd1bc2c 100644 --- a/frontend/src/_helpers/appUtils.js +++ b/frontend/src/_helpers/appUtils.js @@ -1300,14 +1300,16 @@ export function runQuery( }, }); - useResolveStore.getState().actions.addAppSuggestions({ - queries: { - [queryName]: { - data: finalData, - isLoading: false, + if (mode === 'edit') { + useResolveStore.getState().actions.addAppSuggestions({ + queries: { + [queryName]: { + data: finalData, + isLoading: false, + }, }, - }, - }); + }); + } const basePath = `queries.${queryName}`; diff --git a/frontend/src/_stores/appDataStore.js b/frontend/src/_stores/appDataStore.js index f8227587c1..9cf9365fe5 100644 --- a/frontend/src/_stores/appDataStore.js +++ b/frontend/src/_stores/appDataStore.js @@ -111,13 +111,10 @@ export const useAppDataStore = create( } }); - const entityReferencesInEvents = findAllEntityReferences(updatedEvents, []) - ?.map((entity) => { - if (entity && isValidUUID(entity)) { - return entity; - } - }) - ?.filter((e) => e !== undefined); + const entityReferencesInEvents = findAllEntityReferences(updatedEvents, [])?.filter( + (entity) => entity && isValidUUID(entity) + ); + const manager = useResolveStore.getState().referenceMapper; let newEvents = JSON.parse(JSON.stringify(updatedEvents)); From 59de1703ac85ce50770d281275dcf0c8dc7ef557 Mon Sep 17 00:00:00 2001 From: arpitnath Date: Fri, 5 Apr 2024 17:45:01 +0530 Subject: [PATCH 05/11] adapt viewer to reference entity mapping --- frontend/src/Editor/Viewer.jsx | 167 ++++++++++++++++++++++++++++++++- 1 file changed, 162 insertions(+), 5 deletions(-) diff --git a/frontend/src/Editor/Viewer.jsx b/frontend/src/Editor/Viewer.jsx index d9395a907a..011a4bdc5a 100644 --- a/frontend/src/Editor/Viewer.jsx +++ b/frontend/src/Editor/Viewer.jsx @@ -25,13 +25,13 @@ import { import queryString from 'query-string'; import ViewerLogoIcon from './Icons/viewer-logo.svg'; import { DataSourceTypes } from './DataSourceManager/SourceComponents'; -import { resolveReferences, isQueryRunnable, setWindowTitle, pageTitles } from '@/_helpers/utils'; +import { resolveReferences, isQueryRunnable, setWindowTitle, pageTitles, isValidUUID } from '@/_helpers/utils'; import { withTranslation } from 'react-i18next'; import _ from 'lodash'; import { Navigate } from 'react-router-dom'; import Spinner from '@/_ui/Spinner'; import { withRouter } from '@/_hoc/withRouter'; -import { useEditorStore } from '@/_stores/editorStore'; +import { flushComponentsToRender, useEditorActions, useEditorStore } from '@/_stores/editorStore'; import { setCookie } from '@/_helpers/cookie'; import { useDataQueriesStore } from '@/_stores/dataQueriesStore'; import { useCurrentStateStore } from '@/_stores/currentStateStore'; @@ -46,6 +46,12 @@ import ViewerSidebarNavigation from './Viewer/ViewerSidebarNavigation'; import MobileHeader from './Viewer/MobileHeader'; import DesktopHeader from './Viewer/DesktopHeader'; import './Viewer/viewer.scss'; +import { useResolveStore } from '@/_stores/resolverStore'; +import { findComponentsWithReferences } from '@/_helpers/editorHelpers'; +import { findAllEntityReferences } from '@/_stores/utils'; +import { dfs } from '@/_stores/handleReferenceTransactions'; +// eslint-disable-next-line import/no-unresolved +import produce from 'immer'; class ViewerComponent extends React.Component { constructor(props) { @@ -166,6 +172,9 @@ class ViewerComponent extends React.Component { const currentPage = pages.find((page) => page.id === currentPageId); useDataQueriesStore.getState().actions.setDataQueries(dataQueries); + useEditorStore.getState().actions.updateEditorState({ + currentPageId: currentPageId, + }); this.props.setCurrentState({ queries: queryState, components: {}, @@ -189,6 +198,25 @@ class ViewerComponent extends React.Component { }); useEditorStore.getState().actions.toggleCurrentLayout(this.props?.currentLayout == 'mobile' ? 'mobile' : 'desktop'); this.props.updateState({ events: data.events ?? [] }); + const currentPageComponents = appDefData?.pages[currentPageId]?.components; + + if (currentPageComponents && !_.isEmpty(currentPageComponents)) { + const referenceManager = useResolveStore.getState().referenceMapper; + + const newComponents = Object.keys(currentPageComponents).map((componentId) => { + const component = currentPageComponents[componentId]; + + if (!referenceManager.get(componentId)) { + return { + id: componentId, + name: component.component.name, + }; + } + }); + + useResolveStore.getState().actions.addEntitiesToMap(newComponents); + } + this.setState( { currentUser, @@ -206,9 +234,100 @@ class ViewerComponent extends React.Component { events: data.events ?? [], }, () => { - const components = appDefData?.pages[currentPageId]?.components || {}; + // const components = appDefData?.pages[currentPageId]?.components || {}; - computeComponentState(components).then(async () => { + const appJson = JSON.parse(JSON.stringify(appDefData)); + const currentPageId = useEditorStore.getState().currentPageId; + const currentComponents = useEditorStore.getState().appDefinition?.pages?.[currentPageId]?.components; + let dataQueries = JSON.parse(JSON.stringify(useDataQueriesStore.getState().dataQueries)); + let allEvents = JSON.parse(JSON.stringify(useAppDataStore.getState().events)); + + const entityReferencesInComponentDefinitions = findAllEntityReferences(currentComponents, [])?.filter( + (entity) => entity && isValidUUID(entity) + ); + + const entityReferencesInQueryOptions = findAllEntityReferences(dataQueries, [])?.filter( + (entity) => entity && isValidUUID(entity) + ); + + const entityReferencesInEvents = findAllEntityReferences(allEvents, [])?.filter( + (entity) => entity && isValidUUID(entity) + ); + + const manager = useResolveStore.getState().referenceMapper; + + if ( + Array.isArray(entityReferencesInComponentDefinitions) && + entityReferencesInComponentDefinitions?.length > 0 + ) { + let newComponentDefinition = JSON.parse(JSON.stringify(currentComponents)); + + entityReferencesInComponentDefinitions.forEach((entity) => { + const entityrefExists = manager.has(entity); + + if (entityrefExists) { + const value = manager.get(entity); + newComponentDefinition = dfs(newComponentDefinition, entity, value); + } + }); + + const newAppDefinition = produce(appJson, (draft) => { + draft.pages[homePageId].components = newComponentDefinition; + }); + + useEditorStore.getState().actions.updateEditorState({ + isUpdatingEditorStateInProcess: false, + appDefinition: newAppDefinition, + }); + } + + if (Array.isArray(entityReferencesInQueryOptions) && entityReferencesInQueryOptions?.length > 0) { + let newQueryOptions = {}; + dataQueries?.forEach((query) => { + newQueryOptions[query.id] = query.options; + ``; + }); + + entityReferencesInQueryOptions.forEach((entity) => { + const entityrefExists = manager.has(entity); + + if (entityrefExists) { + const value = manager.get(entity); + newQueryOptions = dfs(newQueryOptions, entity, value); + } + }); + + dataQueries = dataQueries.map((query) => { + const queryId = query.id; + const dqOptions = newQueryOptions[queryId]; + + return { + ...query, + options: dqOptions, + }; + }); + + useDataQueriesStore.getState().actions.setDataQueries(dataQueries, 'mappingUpdate'); + } + + if (Array.isArray(entityReferencesInEvents) && entityReferencesInEvents?.length > 0) { + let newEvents = JSON.parse(JSON.stringify(allEvents)); + + entityReferencesInEvents.forEach((entity) => { + const entityrefExists = manager.has(entity); + + if (entityrefExists) { + const value = manager.get(entity); + newEvents = dfs(newEvents, entity, value); + } + }); + + this.props.updateState({ + events: newEvents, + }); + } + + computeComponentState(currentComponents).then(async () => { this.setState({ initialComputationOfStateDone: true, defaultComponentStateComputed: true }); useCurrentStateStore.getState().actions.setEditorReady(true); this.runQueries(dataQueries); @@ -771,14 +890,52 @@ class ViewerComponent extends React.Component { } const withStore = (Component) => (props) => { const currentState = useCurrentStateStore(); - const { currentLayout, queryConfirmationList } = useEditorStore( + const { currentLayout, queryConfirmationList, currentPageId } = useEditorStore( (state) => ({ currentLayout: state?.currentLayout, queryConfirmationList: state?.queryConfirmationList, + currentPageId: state?.currentPageId, }), shallow ); + + const { updateComponentsNeedsUpdateOnNextRender } = useEditorActions(); const { updateState } = useAppDataActions(); + + const lastUpdatedRef = useResolveStore((state) => state.lastUpdatedRefs, shallow); + + async function batchUpdateComponents(componentIds) { + if (componentIds.length === 0) return; + + let updatedComponentIds = []; + + for (let i = 0; i < componentIds.length; i += 10) { + const batch = componentIds.slice(i, i + 10); + batch.forEach((id) => { + updatedComponentIds.push(id); + }); + + updateComponentsNeedsUpdateOnNextRender(batch); + // Delay to allow UI to process + await new Promise((resolve) => setTimeout(resolve, 0)); + } + + // Flush only updated components + flushComponentsToRender(updatedComponentIds); + } + + React.useEffect(() => { + if (lastUpdatedRef.length > 0) { + const currentComponents = useEditorStore.getState().appDefinition?.pages?.[currentPageId]?.components || {}; + const componentIdsWithReferences = findComponentsWithReferences(currentComponents, lastUpdatedRef); + + if (componentIdsWithReferences.length > 0) { + batchUpdateComponents(componentIdsWithReferences); + } + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [lastUpdatedRef]); + return ( Date: Sat, 6 Apr 2024 17:05:19 +0530 Subject: [PATCH 06/11] handle page swich - on page switch, reset resolver stores for new references and hints to get updated - on current state initated on page switch should not out setting up updating resolver store to micro task - refactored and optimised the core functions to find references associated as graph --- frontend/src/Editor/CodeEditor/utils.js | 8 +- frontend/src/Editor/Editor.jsx | 249 ++++++++++++---------- frontend/src/_helpers/editorHelpers.js | 6 +- frontend/src/_hooks/useRenderCount.js | 7 - frontend/src/_stores/appDataStore.js | 1 - frontend/src/_stores/currentStateStore.js | 38 ++-- frontend/src/_stores/resolverStore.js | 5 + 7 files changed, 171 insertions(+), 143 deletions(-) diff --git a/frontend/src/Editor/CodeEditor/utils.js b/frontend/src/Editor/CodeEditor/utils.js index 22dc1eef40..b068ddb487 100644 --- a/frontend/src/Editor/CodeEditor/utils.js +++ b/frontend/src/Editor/CodeEditor/utils.js @@ -200,7 +200,6 @@ const resolveMultiDynamicReferences = (code, lookupTable) => { resolvedValue = resolvedValue.replace(variable, res); } else { - const currentState = useCurrentStateStore.getState(); const [resolvedCode] = resolveCode(variableToResolve, {}, true, [], true); resolvedValue = resolvedCode; @@ -241,7 +240,10 @@ export const resolveReferences = (query, validationSchema, customResolvers = {}) if (value.startsWith('#') || value.includes('table-')) { value = JSON.stringify(value); } - const { toResolveReference, jsExpression, jsExpMatch } = inferJSExpAndReferences(value, lookupTable.hints); + const { toResolveReference, jsExpression, jsExpMatch } = + lookupTable.hints || lookupTable.hints.has + ? inferJSExpAndReferences(value, lookupTable.hints) + : { toResolveReference: null, jsExpression: null, jsExpMatch: null }; if (!jsExpMatch && toResolveReference && lookupTable.hints.has(toResolveReference)) { const idToLookUp = lookupTable.hints.get(toResolveReference); @@ -307,7 +309,7 @@ const inferJSExpAndReferences = (code, hintsMap) => { const potentialReference = referenceChain ? referenceChain + '.' + segment : segment; // Check if the potential reference exists in hintsMap - if (hintsMap.has(potentialReference)) { + if (hintsMap.has && hintsMap.has(potentialReference)) { // If it does, update the referenceChain referenceChain = potentialReference; } else { diff --git a/frontend/src/Editor/Editor.jsx b/frontend/src/Editor/Editor.jsx index 8c2639cc87..5e0511ed59 100644 --- a/frontend/src/Editor/Editor.jsx +++ b/frontend/src/Editor/Editor.jsx @@ -632,8 +632,18 @@ const EditorComponent = (props) => { props.switchDarkMode(newMode); }; - const handleEvent = React.useCallback((eventName, event, options) => { - return onEvent(getEditorRef(), eventName, event, options, 'edit'); + const handleEvent = React.useCallback((eventName, events, options) => { + const latestEvents = useAppDataStore.getState().events; + const filteredEvents = latestEvents.filter((event) => { + const foundEvent = events.find((e) => e.id === event.id); + return foundEvent && foundEvent.name === eventName; + }); + + try { + return onEvent(getEditorRef(), eventName, filteredEvents, options, 'edit'); + } catch (error) { + console.error(error); + } // eslint-disable-next-line react-hooks/exhaustive-deps }, []); @@ -765,117 +775,9 @@ const EditorComponent = (props) => { await fetchDataSources(data.editing_version?.id), await fetchDataQueries(data.editing_version?.id, true, true), ]) - .then(() => { - useCurrentStateStore.getState().actions.setEditorReady(true); - - const currentPageId = useEditorStore.getState().currentPageId; - const currentComponents = useEditorStore.getState().appDefinition?.pages?.[currentPageId]?.components; - - const referenceManager = useResolveStore.getState().referenceMapper; - - const newComponents = Object.keys(currentComponents).map((componentId) => { - const component = currentComponents[componentId]; - - if (!referenceManager.get(componentId)) { - return { - id: componentId, - name: component.component.name, - }; - } - }); - - useResolveStore.getState().actions.addEntitiesToMap(newComponents); - }) - .then(() => { - const currentPageId = useEditorStore.getState().currentPageId; - const currentComponents = useEditorStore.getState().appDefinition?.pages?.[currentPageId]?.components; - let dataQueries = JSON.parse(JSON.stringify(useDataQueriesStore.getState().dataQueries)); - let allEvents = JSON.parse(JSON.stringify(useAppDataStore.getState().events)); - - const entityReferencesInComponentDefinitions = findAllEntityReferences(currentComponents, [])?.filter( - (entity) => entity && isValidUUID(entity) - ); - - const entityReferencesInQueryOptions = findAllEntityReferences(dataQueries, [])?.filter( - (entity) => entity && isValidUUID(entity) - ); - - const entityReferencesInEvents = findAllEntityReferences(allEvents, [])?.filter( - (entity) => entity && isValidUUID(entity) - ); - - const manager = useResolveStore.getState().referenceMapper; - - if ( - Array.isArray(entityReferencesInComponentDefinitions) && - entityReferencesInComponentDefinitions?.length > 0 - ) { - let newComponentDefinition = JSON.parse(JSON.stringify(currentComponents)); - - entityReferencesInComponentDefinitions.forEach((entity) => { - const entityrefExists = manager.has(entity); - - if (entityrefExists) { - const value = manager.get(entity); - newComponentDefinition = dfs(newComponentDefinition, entity, value); - } - }); - - const newAppDefinition = produce(appJson, (draft) => { - draft.pages[homePageId].components = newComponentDefinition; - }); - - updateEditorState({ - isUpdatingEditorStateInProcess: false, - appDefinition: newAppDefinition, - }); - } - - if (Array.isArray(entityReferencesInQueryOptions) && entityReferencesInQueryOptions?.length > 0) { - let newQueryOptions = {}; - dataQueries?.forEach((query) => { - newQueryOptions[query.id] = query.options; - ``; - }); - - entityReferencesInQueryOptions.forEach((entity) => { - const entityrefExists = manager.has(entity); - - if (entityrefExists) { - const value = manager.get(entity); - newQueryOptions = dfs(newQueryOptions, entity, value); - } - }); - - dataQueries = dataQueries.map((query) => { - const queryId = query.id; - const dqOptions = newQueryOptions[queryId]; - - return { - ...query, - options: dqOptions, - }; - }); - - useDataQueriesStore.getState().actions.setDataQueries(dataQueries, 'mappingUpdate'); - } - - if (Array.isArray(entityReferencesInEvents) && entityReferencesInEvents?.length > 0) { - let newEvents = JSON.parse(JSON.stringify(allEvents)); - - entityReferencesInEvents.forEach((entity) => { - const entityrefExists = manager.has(entity); - - if (entityrefExists) { - const value = manager.get(entity); - newEvents = dfs(newEvents, entity, value); - } - }); - - updateState({ - events: newEvents, - }); - } + .then(async () => { + await onEditorLoad(appJson, homePageId); + updateEntityReferences(appJson, homePageId); }) .finally(async () => { const currentPageEvents = data.events.filter( @@ -1478,6 +1380,117 @@ const EditorComponent = (props) => { } }; + const onEditorLoad = (appJson, pageId, isPageSwitch = false) => { + useCurrentStateStore.getState().actions.setEditorReady(true); + const currentComponents = appJson?.pages?.[pageId]?.components; + + const referenceManager = useResolveStore.getState().referenceMapper; + + const newComponents = Object.keys(currentComponents).map((componentId) => { + const component = currentComponents[componentId]; + + if (isPageSwitch || !referenceManager.get(componentId)) { + return { + id: componentId, + name: component.component.name, + }; + } + }); + + useResolveStore.getState().actions.addEntitiesToMap(newComponents); + }; + + const updateEntityReferences = (appJson, pageId) => { + const currentComponents = appJson?.pages?.[pageId]?.components; + + let dataQueries = JSON.parse(JSON.stringify(useDataQueriesStore.getState().dataQueries)); + let allEvents = JSON.parse(JSON.stringify(useAppDataStore.getState().events)); + + const entityReferencesInComponentDefinitions = findAllEntityReferences(currentComponents, [])?.filter( + (entity) => entity && isValidUUID(entity) + ); + + const entityReferencesInQueryOptions = findAllEntityReferences(dataQueries, [])?.filter( + (entity) => entity && isValidUUID(entity) + ); + + const entityReferencesInEvents = findAllEntityReferences(allEvents, [])?.filter( + (entity) => entity && isValidUUID(entity) + ); + + const manager = useResolveStore.getState().referenceMapper; + + if (Array.isArray(entityReferencesInComponentDefinitions) && entityReferencesInComponentDefinitions?.length > 0) { + let newComponentDefinition = JSON.parse(JSON.stringify(currentComponents)); + + entityReferencesInComponentDefinitions.forEach((entity) => { + const entityrefExists = manager.has(entity); + + if (entityrefExists) { + const value = manager.get(entity); + newComponentDefinition = dfs(newComponentDefinition, entity, value); + } + }); + + const newAppDefinition = produce(appJson, (draft) => { + draft.pages[pageId].components = newComponentDefinition; + }); + + handleLowPriorityWork(() => { + updateEditorState({ + isUpdatingEditorStateInProcess: false, + appDefinition: newAppDefinition, + }); + }); + } + + if (Array.isArray(entityReferencesInQueryOptions) && entityReferencesInQueryOptions?.length > 0) { + let newQueryOptions = {}; + dataQueries?.forEach((query) => { + newQueryOptions[query.id] = query.options; + ``; + }); + + entityReferencesInQueryOptions.forEach((entity) => { + const entityrefExists = manager.has(entity); + + if (entityrefExists) { + const value = manager.get(entity); + newQueryOptions = dfs(newQueryOptions, entity, value); + } + }); + + dataQueries = dataQueries.map((query) => { + const queryId = query.id; + const dqOptions = newQueryOptions[queryId]; + + return { + ...query, + options: dqOptions, + }; + }); + + useDataQueriesStore.getState().actions.setDataQueries(dataQueries, 'mappingUpdate'); + } + + if (Array.isArray(entityReferencesInEvents) && entityReferencesInEvents?.length > 0) { + let newEvents = JSON.parse(JSON.stringify(allEvents)); + + entityReferencesInEvents.forEach((entity) => { + const entityrefExists = manager.has(entity); + + if (entityrefExists) { + const value = manager.get(entity); + newEvents = dfs(newEvents, entity, value); + } + }); + + updateState({ + events: newEvents, + }); + } + }; + const removeComponents = () => { const selectedComponents = useEditorStore.getState()?.selectedComponents; if (!isVersionReleased && selectedComponents?.length > 1) { @@ -1592,7 +1605,9 @@ const EditorComponent = (props) => { }); }; - const switchPage = (pageId, queryParams = []) => { + const switchPage = async (pageId, queryParams = []) => { + useCurrentStateStore.getState().actions.setEditorReady(false); + useResolveStore.getState().actions.resetStore(); // This are fetched from store to handle runQueriesOnAppLoad const currentPageId = useEditorStore.getState().currentPageId; const appDefinition = useEditorStore.getState().appDefinition; @@ -1619,7 +1634,13 @@ const EditorComponent = (props) => { ...currentState.globals, urlparams: JSON.parse(JSON.stringify(queryString.parse(queryParamsString))), }; + useCurrentStateStore.getState().actions.setCurrentState({ globals, page }); + useResolveStore.getState().actions.pageSwitched(true); + + await onEditorLoad(appDefinition, pageId, true); + updateEntityReferences(appDefinition, pageId); + useResolveStore.getState().actions.updateJSHints(); setCurrentPageId(pageId); diff --git a/frontend/src/_helpers/editorHelpers.js b/frontend/src/_helpers/editorHelpers.js index 2971cd3bc0..00649a0750 100644 --- a/frontend/src/_helpers/editorHelpers.js +++ b/frontend/src/_helpers/editorHelpers.js @@ -174,7 +174,11 @@ export function findComponentsWithReferences(components, changedCurrentState) { return componentIdsWithReferences; } -export function handleLowPriorityWork(callback, timeout = null) { +export function handleLowPriorityWork(callback, timeout = null, immediate = false) { + if (immediate) { + callback(); + } + const options = timeout ? { timeout } : {}; window.requestIdleCallback(callback, options); } diff --git a/frontend/src/_hooks/useRenderCount.js b/frontend/src/_hooks/useRenderCount.js index 3ad64f6b9e..c07b5734e3 100644 --- a/frontend/src/_hooks/useRenderCount.js +++ b/frontend/src/_hooks/useRenderCount.js @@ -3,15 +3,8 @@ import { useRef, useEffect } from 'react'; function useRenderCount(componentName) { const renderCountRef = useRef(0); - useEffect(() => { - return () => { - console.log(`--Component ${componentName} rendered unmounting ${renderCountRef.current} times.`); - }; - }, []); - renderCountRef.current++; - console.log(`CountingRender- Component ${componentName} rendered ${renderCountRef.current} times.`); return renderCountRef.current; } diff --git a/frontend/src/_stores/appDataStore.js b/frontend/src/_stores/appDataStore.js index 9cf9365fe5..182fbee1b2 100644 --- a/frontend/src/_stores/appDataStore.js +++ b/frontend/src/_stores/appDataStore.js @@ -46,7 +46,6 @@ export const useAppDataStore = create( updateEditingVersion: (version) => set(() => ({ editingVersion: version })), updateApps: (apps) => set(() => ({ apps: apps })), updateState: (state) => set((prev) => ({ ...prev, ...state })), - updateAppDefinitionDiff: (appDefinitionDiff) => set(() => ({ appDefinitionDiff: appDefinitionDiff })), updateAppVersion: (appId, versionId, pageId, appDefinitionDiff, isUserSwitchedVersion = false) => { return new Promise((resolve, reject) => { diff --git a/frontend/src/_stores/currentStateStore.js b/frontend/src/_stores/currentStateStore.js index bb7af52df0..d0c82505e3 100644 --- a/frontend/src/_stores/currentStateStore.js +++ b/frontend/src/_stores/currentStateStore.js @@ -1,10 +1,7 @@ import { shallow } from 'zustand/shallow'; import { create, zustandDevTools } from './utils'; -import _, { debounce, merge, omit } from 'lodash'; +import _, { omit } from 'lodash'; import { useResolveStore } from './resolverStore'; -// eslint-disable-next-line import/no-unresolved -import { diff } from 'deep-object-diff'; -import { useEditorStore } from './editorStore'; import { handleLowPriorityWork } from '@/_helpers/editorHelpers'; const initialState = { @@ -88,19 +85,26 @@ useCurrentStateStore.subscribe((state) => { const isStoreIntialized = useResolveStore.getState().storeReady; if (!isStoreIntialized) { - handleLowPriorityWork(() => { - useResolveStore.getState().actions.updateAppSuggestions({ - queries: state.queries, - components: state.components, - globals: state.globals, - page: state.page, - variables: state.variables, - client: state.client, - server: state.server, - constants: state.constants, - }); - }); - console.log('Resolver store initialized with current state.'); + const isPageSwitched = useResolveStore.getState().isPageSwitched; + + handleLowPriorityWork( + () => { + useResolveStore.getState().actions.updateAppSuggestions({ + queries: state.queries, + components: state.components, + globals: state.globals, + page: state.page, + variables: state.variables, + client: state.client, + server: state.server, + constants: state.constants, + }); + useResolveStore.getState().actions.pageSwitched(false); + }, + null, + isPageSwitched + ); + return useResolveStore.getState().actions.updateStoreState({ storeReady: true }); } }, shallow); diff --git a/frontend/src/_stores/resolverStore.js b/frontend/src/_stores/resolverStore.js index 4fc70a1d5b..8483d4b2ce 100644 --- a/frontend/src/_stores/resolverStore.js +++ b/frontend/src/_stores/resolverStore.js @@ -60,6 +60,7 @@ const initialState = { }, lastUpdatedRefs: [], referenceMapper: new ReferencesBiMap(), + isPageSwitched: false, }; export const useResolveStore = create( @@ -69,6 +70,10 @@ export const useResolveStore = create( updateStoreState: (state) => { set(() => ({ ...state, storeReady: true })); }, + resetStore: () => { + set(() => initialState); + }, + pageSwitched: (bool) => set(() => ({ isPageSwitched: bool })), updateAppSuggestions: (refState) => { const { suggestionList, hintsMap, resolvedRefs } = createReferencesLookup(refState, false, true); From 770e207af14991d10562b67a324d9a8a84e06112 Mon Sep 17 00:00:00 2001 From: arpitnath Date: Sat, 6 Apr 2024 17:16:57 +0530 Subject: [PATCH 07/11] fixes saving trigger and throwing an error when appdefinition is null on first load --- frontend/src/Editor/Editor.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/Editor/Editor.jsx b/frontend/src/Editor/Editor.jsx index 5e0511ed59..e5e1d04799 100644 --- a/frontend/src/Editor/Editor.jsx +++ b/frontend/src/Editor/Editor.jsx @@ -983,7 +983,7 @@ const EditorComponent = (props) => { updateEditorState({ isUpdatingEditorStateInProcess: false, }); - } else if (!isEmpty(editingVersion)) { + } else if (!isEmpty(editingVersion) && !isEmpty(appDiffOptions) && appDefinition) { //! The computeComponentPropertyDiff function manages the calculation of differences in table columns by requiring complete column data. Without this complete data, the resulting JSON structure may be incorrect. const paramDiff = computeComponentPropertyDiff(appDefinitionDiff, appDefinition, appDiffOptions); const updateDiff = computeAppDiff(paramDiff, currentPageId, appDiffOptions, currentLayout); From 7d8c2b9719170f3655041901492262eeb2f5635b Mon Sep 17 00:00:00 2001 From: arpitnath Date: Sun, 7 Apr 2024 00:31:21 +0530 Subject: [PATCH 08/11] cherry-picked 2c38b85 handles references mapping with new associations --- server/src/helpers/import_export.helpers.ts | 53 ++++++++++++++ .../src/services/app_import_export.service.ts | 72 ++++++++++++++++--- 2 files changed, 116 insertions(+), 9 deletions(-) create mode 100644 server/src/helpers/import_export.helpers.ts diff --git a/server/src/helpers/import_export.helpers.ts b/server/src/helpers/import_export.helpers.ts new file mode 100644 index 0000000000..db9b02ddec --- /dev/null +++ b/server/src/helpers/import_export.helpers.ts @@ -0,0 +1,53 @@ +export function updateEntityReferences(node, resourceMapping: Record = {}) { + if (typeof node === 'object') { + for (const key in node) { + let value = node[key]; + if (typeof value === 'string' && value.includes('{{') && value.includes('}}')) { + const referenceExists = value; + + if (referenceExists) { + const ref = value.replace('{{', '').replace('}}', ''); + + const entityName = ref.split('.')[1]; + + if (resourceMapping[entityName]) { + const newValue = value.replace(entityName, resourceMapping[entityName]); + + node[key] = newValue; + } + } + } else if (typeof value === 'object') { + value = updateEntityReferences(value, resourceMapping); + } + } + } + + return node; +} + +export function findAllEntityReferences(node, allRefs): [] { + if (typeof node === 'object') { + for (const key in node) { + const value = node[key]; + if (typeof value === 'string' && value.includes('{{') && value.includes('}}')) { + const referenceExists = value; + + if (referenceExists) { + const ref = value.replace('{{', '').replace('}}', ''); + + const entityName = ref.split('.')[1]; + + allRefs.push(entityName); + } + } else if (typeof value === 'object') { + findAllEntityReferences(value, allRefs); + } + } + } + return allRefs; +} + +export function isValidUUID(uuid) { + const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; + return uuidRegex.test(uuid); +} diff --git a/server/src/services/app_import_export.service.ts b/server/src/services/app_import_export.service.ts index d30e0f936e..a1267b2c60 100644 --- a/server/src/services/app_import_export.service.ts +++ b/server/src/services/app_import_export.service.ts @@ -32,7 +32,7 @@ import { Component } from 'src/entities/component.entity'; import { Layout } from 'src/entities/layout.entity'; import { EventHandler, Target } from 'src/entities/event_handler.entity'; import { v4 as uuid } from 'uuid'; - +import { findAllEntityReferences, isValidUUID, updateEntityReferences } from 'src/helpers/import_export.helpers'; interface AppResourceMappings { defaultDataSourceIdMapping: Record; dataQueryMapping: Record; @@ -237,21 +237,19 @@ export class AppImportExportService { ? true : isTooljetVersionWithNormalizedAppDefinitionSchem(importedAppTooljetVersion); - const shouldUpdateForGridCompatibility = !cloning; - const importedApp = await this.createImportedAppForUser(this.entityManager, schemaUnifiedAppParams, user); - await this.setupImportedAppAssociations( + const resourceMapping = await this.setupImportedAppAssociations( this.entityManager, importedApp, schemaUnifiedAppParams, user, externalResourceMappings, isNormalizedAppDefinitionSchema, - shouldUpdateForGridCompatibility, tooljetVersion ); await this.createAdminGroupPermissions(this.entityManager, importedApp); + await this.updateEntityReferencesForImportedApp(this.entityManager, resourceMapping); // NOTE: App slug updation callback doesn't work while wrapped in transaction // hence updating slug explicitly @@ -262,6 +260,64 @@ export class AppImportExportService { return importedApp; } + async updateEntityReferencesForImportedApp(manager: EntityManager, resourceMapping: AppResourceMappings) { + const mappings = { ...resourceMapping.componentsMapping, ...resourceMapping.dataQueryMapping }; + const newComponentIds = Object.values(resourceMapping.componentsMapping); + const newQueriesIds = Object.values(resourceMapping.dataQueryMapping); + + if (newComponentIds.length > 0) { + const components = await manager + .createQueryBuilder(Component, 'components') + .where('components.id IN(:...componentIds)', { componentIds: newComponentIds }) + .select([ + 'components.id', + 'components.properties', + 'components.styles', + 'components.general', + 'components.validation', + 'components.generalStyles', + 'components.displayPreferences', + ]) + .getMany(); + + const toUpdateComponents = components.filter((component) => { + const entityReferencesInComponentDefinitions = findAllEntityReferences(component, []).filter( + (entity) => entity && isValidUUID(entity) + ); + + if (entityReferencesInComponentDefinitions.length > 0) { + return updateEntityReferences(component, mappings); + } + }); + + if (!isEmpty(toUpdateComponents)) { + await manager.save(toUpdateComponents); + } + } + + if (newQueriesIds.length > 0) { + const dataQueries = await manager + .createQueryBuilder(DataQuery, 'dataQueries') + .where('dataQueries.id IN(:...dataQueryIds)', { dataQueryIds: newQueriesIds }) + .select(['dataQueries.id', 'dataQueries.options']) + .getMany(); + + const toUpdateDataQueries = dataQueries.filter((dataQuery) => { + const entityReferencesInQueryOptions = findAllEntityReferences(dataQuery, []).filter( + (entity) => entity && isValidUUID(entity) + ); + + if (entityReferencesInQueryOptions.length > 0) { + return updateEntityReferences(dataQuery, mappings); + } + }); + + if (!isEmpty(toUpdateDataQueries)) { + await manager.save(toUpdateDataQueries); + } + } + } + async createImportedAppForUser(manager: EntityManager, appParams: any, user: User): Promise { return await catchDbException(async () => { const importedApp = manager.create(App, { @@ -328,7 +384,6 @@ export class AppImportExportService { user: User, externalResourceMappings: Record, isNormalizedAppDefinitionSchema: boolean, - shouldUpdateForGridCompatibility: boolean, tooljetVersion: string ) { // Old version without app version @@ -393,7 +448,6 @@ export class AppImportExportService { importingPages, importingComponents, importingEvents, - shouldUpdateForGridCompatibility, tooljetVersion ); @@ -599,7 +653,6 @@ export class AppImportExportService { importingPages: Page[], importingComponents: Component[], importingEvents: EventHandler[], - shouldUpdateForGridCompatibility: boolean, tooljetVersion: string ): Promise { appResourceMappings = { ...appResourceMappings }; @@ -1656,9 +1709,10 @@ export class AppImportExportService { .createQueryBuilder(EventHandler, 'event') .where('event.appVersionId = :versionId', { versionId }) .getMany(); + const mappings = { ...oldDataQueryToNewMapping, ...oldComponentToNewComponentMapping } as Record; for (const event of allEvents) { - const eventDefinition = event.event; + const eventDefinition = updateEntityReferences(event.event, mappings); if (eventDefinition?.actionId === 'run-query' && oldDataQueryToNewMapping[eventDefinition.queryId]) { eventDefinition.queryId = oldDataQueryToNewMapping[eventDefinition.queryId]; From 14e46f70a39c726056e3c13fa15fd81f356bb735 Mon Sep 17 00:00:00 2001 From: arpitnath Date: Sun, 7 Apr 2024 00:44:26 +0530 Subject: [PATCH 09/11] on version creation, should handle references mapping with new associations --- server/src/services/apps.service.ts | 77 +++++++++++++++++++++++++++-- 1 file changed, 74 insertions(+), 3 deletions(-) diff --git a/server/src/services/apps.service.ts b/server/src/services/apps.service.ts index e98474dd4f..5ce9db17e5 100644 --- a/server/src/services/apps.service.ts +++ b/server/src/services/apps.service.ts @@ -25,11 +25,17 @@ import { DataBaseConstraints } from 'src/helpers/db_constraints.constants'; import { Page } from 'src/entities/page.entity'; import { AppVersionUpdateDto } from '@dto/app-version-update.dto'; import { Layout } from 'src/entities/layout.entity'; - import { Component } from 'src/entities/component.entity'; import { EventHandler } from 'src/entities/event_handler.entity'; - +import { findAllEntityReferences, isValidUUID, updateEntityReferences } from 'src/helpers/import_export.helpers'; +import { isEmpty } from 'lodash'; const uuid = require('uuid'); + +interface AppResourceMappings { + dataQueryMapping: Record; + componentsMapping: Record; +} + @Injectable() export class AppsService { constructor( @@ -391,6 +397,11 @@ export class AppsService { const { oldComponentToNewComponentMapping, oldPageToNewPageMapping } = await this.createNewPagesAndComponentsForVersion(manager, appVersion, versionFrom.id, versionFrom.homePageId); + await this.updateEntityReferencesForNewVersion(manager, { + componentsMapping: oldComponentToNewComponentMapping, + dataQueryMapping: oldDataQueryToNewMapping, + }); + await this.updateEventActionsForNewVersionWithNewMappingIds( manager, appVersion.id, @@ -404,6 +415,64 @@ export class AppsService { }, manager); } + async updateEntityReferencesForNewVersion(manager: EntityManager, resourceMapping: AppResourceMappings) { + const mappings = { ...resourceMapping.componentsMapping, ...resourceMapping.dataQueryMapping }; + const newComponentIds = Object.values(resourceMapping.componentsMapping); + const newQueriesIds = Object.values(resourceMapping.dataQueryMapping); + + if (newComponentIds.length > 0) { + const components = await manager + .createQueryBuilder(Component, 'components') + .where('components.id IN(:...componentIds)', { componentIds: newComponentIds }) + .select([ + 'components.id', + 'components.properties', + 'components.styles', + 'components.general', + 'components.validation', + 'components.generalStyles', + 'components.displayPreferences', + ]) + .getMany(); + + const toUpdateComponents = components.filter((component) => { + const entityReferencesInComponentDefinitions = findAllEntityReferences(component, []).filter( + (entity) => entity && isValidUUID(entity) + ); + + if (entityReferencesInComponentDefinitions.length > 0) { + return updateEntityReferences(component, mappings); + } + }); + + if (!isEmpty(toUpdateComponents)) { + await manager.save(toUpdateComponents); + } + } + + if (newQueriesIds.length > 0) { + const dataQueries = await manager + .createQueryBuilder(DataQuery, 'dataQueries') + .where('dataQueries.id IN(:...dataQueryIds)', { dataQueryIds: newQueriesIds }) + .select(['dataQueries.id', 'dataQueries.options']) + .getMany(); + + const toUpdateDataQueries = dataQueries.filter((dataQuery) => { + const entityReferencesInQueryOptions = findAllEntityReferences(dataQuery, []).filter( + (entity) => entity && isValidUUID(entity) + ); + + if (entityReferencesInQueryOptions.length > 0) { + return updateEntityReferences(dataQuery, mappings); + } + }); + + if (!isEmpty(toUpdateDataQueries)) { + await manager.save(toUpdateDataQueries); + } + } + } + async updateEventActionsForNewVersionWithNewMappingIds( manager: EntityManager, versionId: string, @@ -415,8 +484,10 @@ export class AppsService { where: { appVersionId: versionId }, }); + const mappings = { ...oldDataQueryToNewMapping, ...oldComponentToNewComponentMapping } as Record; + for (const event of allEvents) { - const eventDefinition = event.event; + const eventDefinition = updateEntityReferences(event.event, mappings); if (eventDefinition?.actionId === 'run-query') { eventDefinition.queryId = oldDataQueryToNewMapping[eventDefinition.queryId]; From 7a233daa7b3b67c4cf38a5b83cbd8999029cb1db Mon Sep 17 00:00:00 2001 From: arpitnath Date: Sun, 7 Apr 2024 14:38:21 +0530 Subject: [PATCH 10/11] handle version switch with new reference architecture --- .../AppVersionsManager/AppVersionsManager.jsx | 13 +++++++++++- frontend/src/Editor/Container.jsx | 6 +++++- frontend/src/Editor/Editor.jsx | 21 +++++++++++-------- frontend/src/Editor/EditorSelecto.jsx | 13 ++++-------- 4 files changed, 33 insertions(+), 20 deletions(-) diff --git a/frontend/src/Editor/AppVersionsManager/AppVersionsManager.jsx b/frontend/src/Editor/AppVersionsManager/AppVersionsManager.jsx index 6c71dc6a43..16497a8dff 100644 --- a/frontend/src/Editor/AppVersionsManager/AppVersionsManager.jsx +++ b/frontend/src/Editor/AppVersionsManager/AppVersionsManager.jsx @@ -6,6 +6,7 @@ import { toast } from 'react-hot-toast'; import { shallow } from 'zustand/shallow'; import { useAppVersionStore } from '@/_stores/appVersionStore'; import { useEditorStore } from '@/_stores/editorStore'; +import { useAppDataStore } from '@/_stores/appDataStore'; const appVersionLoadingStatus = Object.freeze({ loading: 'loading', @@ -56,7 +57,17 @@ export const AppVersionsManager = function ({ const darkMode = localStorage.getItem('darkMode') === 'true'; const selectVersion = (id) => { - appVersionService + const currentVersionId = useAppDataStore.getState().currentVersionId; + + const isSameVersionSelected = currentVersionId === id; + + if (isSameVersionSelected) { + return toast('You are already editing this version', { + icon: '⚠️', + }); + } + + return appVersionService .getAppVersionData(appId, id) .then((data) => { const isCurrentVersionReleased = data.currentVersionId ? true : false; diff --git a/frontend/src/Editor/Container.jsx b/frontend/src/Editor/Container.jsx index 42c64bfb8f..d2aeee011b 100644 --- a/frontend/src/Editor/Container.jsx +++ b/frontend/src/Editor/Container.jsx @@ -46,8 +46,8 @@ export const Container = ({ socket, handleUndo, handleRedo, - currentPageId, }) => { + const currentPageId = useEditorStore.getState().currentPageId; const appDefinition = useEditorStore.getState().appDefinition; // Dont update first time to skip // redundant save on app definition load @@ -279,6 +279,10 @@ export const Container = ({ appDefinitionChanged(newDefinition, opts); } + return () => { + firstUpdate.current = true; + }; + // eslint-disable-next-line react-hooks/exhaustive-deps }, [boxes]); diff --git a/frontend/src/Editor/Editor.jsx b/frontend/src/Editor/Editor.jsx index e5e1d04799..1d85b00343 100644 --- a/frontend/src/Editor/Editor.jsx +++ b/frontend/src/Editor/Editor.jsx @@ -753,12 +753,12 @@ const EditorComponent = (props) => { useCurrentStateStore.getState().actions.setCurrentState({ page: currentpageData, - }), - updateEditorState({ - isLoading: false, - appDefinition: appJson, - isUpdatingEditorStateInProcess: false, - }); + }); + updateEditorState({ + isLoading: false, + appDefinition: appJson, + isUpdatingEditorStateInProcess: false, + }); updateState({ components: appJson.pages[homePageId]?.components }); @@ -776,7 +776,7 @@ const EditorComponent = (props) => { await fetchDataQueries(data.editing_version?.id, true, true), ]) .then(async () => { - await onEditorLoad(appJson, homePageId); + await onEditorLoad(appJson, homePageId, versionSwitched); updateEntityReferences(appJson, homePageId); }) .finally(async () => { @@ -817,6 +817,9 @@ const EditorComponent = (props) => { updateEditorState({ isLoading: true, }); + useCurrentStateStore.getState().actions.setCurrentState({}); + useCurrentStateStore.getState().actions.setEditorReady(false); + useResolveStore.getState().actions.resetStore(); callBack(appData, null, true); initComponentVersioning(); @@ -1380,7 +1383,7 @@ const EditorComponent = (props) => { } }; - const onEditorLoad = (appJson, pageId, isPageSwitch = false) => { + const onEditorLoad = (appJson, pageId, isPageSwitchOrVersionSwitch = false) => { useCurrentStateStore.getState().actions.setEditorReady(true); const currentComponents = appJson?.pages?.[pageId]?.components; @@ -1389,7 +1392,7 @@ const EditorComponent = (props) => { const newComponents = Object.keys(currentComponents).map((componentId) => { const component = currentComponents[componentId]; - if (isPageSwitch || !referenceManager.get(componentId)) { + if (isPageSwitchOrVersionSwitch || !referenceManager.get(componentId)) { return { id: componentId, name: component.component.name, diff --git a/frontend/src/Editor/EditorSelecto.jsx b/frontend/src/Editor/EditorSelecto.jsx index db85ebda18..d99855439b 100644 --- a/frontend/src/Editor/EditorSelecto.jsx +++ b/frontend/src/Editor/EditorSelecto.jsx @@ -3,18 +3,13 @@ import Selecto from 'react-selecto'; import { useEditorStore, EMPTY_ARRAY } from '@/_stores/editorStore'; import { shallow } from 'zustand/shallow'; -const EditorSelecto = ({ - selectionRef, - canvasContainerRef, - currentPageId, - setSelectedComponent, - appDefinition, - selectionDragRef, -}) => { - const { setSelectionInProgress, setSelectedComponents } = useEditorStore( +const EditorSelecto = ({ selectionRef, canvasContainerRef, setSelectedComponent, selectionDragRef }) => { + const { setSelectionInProgress, setSelectedComponents, currentPageId, appDefinition } = useEditorStore( (state) => ({ setSelectionInProgress: state?.actions?.setSelectionInProgress, setSelectedComponents: state?.actions?.setSelectedComponents, + currentPageId: state?.currentPageId, + appDefinition: state?.appDefinition, }), shallow ); From d08e214add0e2b601fb6c1d44945387b8d2e2585 Mon Sep 17 00:00:00 2001 From: arpitnath Date: Mon, 8 Apr 2024 10:57:50 +0530 Subject: [PATCH 11/11] fixes: cursor postion for selection hint --- .../src/Editor/CodeEditor/autocompleteExtensionConfig.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/frontend/src/Editor/CodeEditor/autocompleteExtensionConfig.js b/frontend/src/Editor/CodeEditor/autocompleteExtensionConfig.js index 8d034b9139..ab08f79e82 100644 --- a/frontend/src/Editor/CodeEditor/autocompleteExtensionConfig.js +++ b/frontend/src/Editor/CodeEditor/autocompleteExtensionConfig.js @@ -63,7 +63,7 @@ function orderSuggestions(suggestions, validationType) { return [...matchingSuggestions, ...otherSuggestions]; } -export const generateHints = (hints, isFxHinter = false, totalReferences = 1) => { +export const generateHints = (hints, totalReferences = 1) => { if (!hints) return []; const suggestions = hints.map(({ hint, type }) => { @@ -90,9 +90,7 @@ export const generateHints = (hints, isFxHinter = false, totalReferences = 1) => insert: completion.label, }; - let anchorSelection = isFxHinter - ? pickedCompletionConfig.insert.length - : pickedCompletionConfig.insert.length + 2; + let anchorSelection = pickedCompletionConfig.insert.length + 2; if (completion.type === 'js_methods') { pickedCompletionConfig.from = from;