diff --git a/frontend/src/Editor/AppVersionsManager/AppVersionsManager.jsx b/frontend/src/Editor/AppVersionsManager/AppVersionsManager.jsx index f43445cec5..5db49eed9d 100644 --- a/frontend/src/Editor/AppVersionsManager/AppVersionsManager.jsx +++ b/frontend/src/Editor/AppVersionsManager/AppVersionsManager.jsx @@ -7,6 +7,7 @@ import { shallow } from 'zustand/shallow'; import { useAppVersionStore } from '@/_stores/appVersionStore'; import { useEditorStore } from '@/_stores/editorStore'; import { useEnvironmentsAndVersionsStore } from '@/_stores/environmentsAndVersionsStore'; +import { useAppDataStore } from '@/_stores/appDataStore'; const appVersionLoadingStatus = Object.freeze({ loading: 'loading', @@ -85,7 +86,17 @@ const RenderComponent = ({ 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/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..ab08f79e82 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 })); }; @@ -64,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 }) => { @@ -91,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; 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 960e735edf..b068ddb487 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', @@ -199,8 +200,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; } @@ -210,40 +210,40 @@ 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; - 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('}}'))) { 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); + 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); @@ -253,10 +253,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; @@ -309,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/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/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 542ff28901..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'; @@ -38,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); @@ -50,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 diff --git a/frontend/src/Editor/Components/PasswordInput.jsx b/frontend/src/Editor/Components/PasswordInput.jsx index 8b0b8e5b50..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'; @@ -44,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); 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 74c59ec1ee..5407d5a99f 100644 --- a/frontend/src/Editor/Components/Tabs.jsx +++ b/frontend/src/Editor/Components/Tabs.jsx @@ -1,7 +1,7 @@ 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({ @@ -10,7 +10,6 @@ export const Tabs = function Tabs({ width, height, containerProps, - currentState, removeComponent, setExposedVariable, setExposedVariables, @@ -25,9 +24,9 @@ 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) @@ -40,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); } @@ -77,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'; diff --git a/frontend/src/Editor/Components/TextInput.jsx b/frontend/src/Editor/Components/TextInput.jsx index d0db47de78..6084aa16d9 100644 --- a/frontend/src/Editor/Components/TextInput.jsx +++ b/frontend/src/Editor/Components/TextInput.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'; const tinycolor = require('tinycolor2'); @@ -45,8 +45,8 @@ export const TextInput = function TextInput({ const [visibility, setVisibility] = useState(properties.visibility); const { isValid, validationError } = validate(value); 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 [loading, setLoading] = useState(loadingState); diff --git a/frontend/src/Editor/Container.jsx b/frontend/src/Editor/Container.jsx index 0322489fc6..9b62b777ec 100644 --- a/frontend/src/Editor/Container.jsx +++ b/frontend/src/Editor/Container.jsx @@ -6,7 +6,7 @@ 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'; @@ -25,7 +25,7 @@ import DragContainer from './DragContainer'; import { compact, correctBounds } from './gridUtils'; import { isPDFSupported } from '@/_stores/utils'; import toast from 'react-hot-toast'; -import { isOnlyLayoutUpdate } from '@/_helpers/editorHelpers'; +import { isOnlyLayoutUpdate, handleLowPriorityWork } from '@/_helpers/editorHelpers'; import GhostWidget from './GhostWidget'; import { useDraggedSubContainer, useGridStore } from '@/_stores/gridStore'; @@ -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]); @@ -594,22 +598,26 @@ export const Container = ({ ...copyOfBoxes, ...updatedBoxes, }; - 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; - }, {}); + handleLowPriorityWork(() => { + const diffState = diff(boxes, newBoxes); - return { - ...prev, - ...updatedComponentsAsperDiff, - }; + 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); } @@ -819,7 +827,8 @@ export const Container = ({ .map(([id, box]) => { const canShowInCurrentLayout = box.component.definition.others[currentLayout === 'mobile' ? 'showOnMobile' : 'showOnDesktop'].value; - if (box.parent || !resolveReferences(canShowInCurrentLayout, useCurrentStateStore.getState())) { + + if (box.parent || !resolveWidgetFieldValue(canShowInCurrentLayout)) { return ''; } return ( diff --git a/frontend/src/Editor/DragContainer.jsx b/frontend/src/Editor/DragContainer.jsx index 7c719cb851..8355f0a678 100644 --- a/frontend/src/Editor/DragContainer.jsx +++ b/frontend/src/Editor/DragContainer.jsx @@ -40,15 +40,14 @@ export default function DragContainer({ 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 - setTimeout(() => - onDrag( - lastDraggedEventsRef.current.events.map((ev) => ({ - id: ev.target.id, - x: ev.translate[0], - y: ev.translate[1], - parent: preant, - })) - ) + + onDrag( + lastDraggedEventsRef.current.events.map((ev) => ({ + id: ev.target.id, + x: ev.translate[0], + y: ev.translate[1], + parent: preant, + })) ); } if (useGridStore.getState().isGroupHandleHoverd) { @@ -363,9 +362,9 @@ export default function DragContainer({ resizeData.gw = _gridWidth; } // Adding the new updates to the macro task queue to unblock UI - setTimeout(() => { - onResizeStop([resizeData]); - }); + // setTimeout(() => { + // }); + onResizeStop([resizeData]); } catch (error) { console.error('ResizeEnd error ->', error); } @@ -432,9 +431,9 @@ export default function DragContainer({ if (groupResizeDataRef.current.length) { // Adding the new updates to the macro task queue to unblock UI - setTimeout(() => { - onResizeStop([newBoxs]); - }); + // setTimeout(() => { + // }); + onResizeStop([newBoxs]); } else { events.forEach((ev) => { const currentWidget = boxes.find(({ id }) => { @@ -562,24 +561,24 @@ 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, - x: left, - y: Math.round(top / 10) * 10, - parent: isParentChangeAllowed ? draggedOverElemId : undefined, - }, - ]) - ); + // setTimeout(() => + // ); + onDrag([ + { + id: e.target.id, + x: left, + y: Math.round(top / 10) * 10, + parent: isParentChangeAllowed ? draggedOverElemId : undefined, + }, + ]); } const box = boxes.find((box) => box.id === e.target.id); setTimeout(() => useEditorStore.getState().actions.setSelectedComponents([{ ...box }])); @@ -696,31 +695,29 @@ 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 - setTimeout(() => - onDrag( - events.map((ev) => { - let posX = ev.lastEvent.translate[0]; - let posY = ev.lastEvent.translate[1]; - if (posLeft < 0) { - posX = ev.lastEvent.translate[0] - posLeft; - } - if (posTop < 0) { - posY = ev.lastEvent.translate[1] - posTop; - } - if (posRight < 0) { - posX = ev.lastEvent.translate[0] + posRight; - } - if (posBottom < 0) { - posY = ev.lastEvent.translate[1] + posBottom; - } - return { - id: ev.target.id, - x: posX, - y: posY, - parent: parentId, - }; - }) - ) + onDrag( + events.map((ev) => { + let posX = ev.lastEvent.translate[0]; + let posY = ev.lastEvent.translate[1]; + if (posLeft < 0) { + posX = ev.lastEvent.translate[0] - posLeft; + } + if (posTop < 0) { + posY = ev.lastEvent.translate[1] - posTop; + } + if (posRight < 0) { + posX = ev.lastEvent.translate[0] + posRight; + } + if (posBottom < 0) { + posY = ev.lastEvent.translate[1] + posBottom; + } + return { + id: ev.target.id, + x: posX, + y: posY, + parent: parentId, + }; + }) ); } catch (error) { console.error('Error dragging group', error); diff --git a/frontend/src/Editor/DraggableBox.jsx b/frontend/src/Editor/DraggableBox.jsx index 653a5186ff..d51387fa8c 100644 --- a/frontend/src/Editor/DraggableBox.jsx +++ b/frontend/src/Editor/DraggableBox.jsx @@ -131,7 +131,7 @@ const DraggableBox = React.memo( const configWidgetHandlerForModalComponent = !isSelectedComponent && component.component === 'Modal' && - resolveWidgetFieldValue(component.definition.properties.useDefaultButton, currentState)?.value === false; + resolveWidgetFieldValue(component.definition.properties.useDefaultButton?.value) === false; const onComponentHover = useCallback( (id) => { diff --git a/frontend/src/Editor/Editor.jsx b/frontend/src/Editor/Editor.jsx index 38ab16ec5e..1f3af82f68 100644 --- a/frontend/src/Editor/Editor.jsx +++ b/frontend/src/Editor/Editor.jsx @@ -89,7 +89,7 @@ import { HotkeysProvider } from 'react-hotkeys-hook'; import { useResolveStore } from '@/_stores/resolverStore'; import { dfs } from '@/_stores/handleReferenceTransactions'; import { decimalToHex } from './editorConstants'; -import { findComponentsWithReferences, generatePath, handleLowPriorityWork } from '@/_helpers/editorHelpers'; +import { findComponentsWithReferences, handleLowPriorityWork } from '@/_helpers/editorHelpers'; setAutoFreeze(false); enablePatches(); @@ -282,9 +282,9 @@ 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; @@ -293,8 +293,6 @@ const EditorComponent = (props) => { // eslint-disable-next-line react-hooks/exhaustive-deps }, [JSON.stringify({ appDefinition, currentPageId, dataQueries })]); - const prevCurrentStateRef = useRef(currentState); - /** ** 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, @@ -634,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 }, []); @@ -743,12 +751,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 }); @@ -765,129 +773,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, []) - ?.map((entity) => { - if (entity && isValidUUID(entity)) { - return entity; - } - }) - ?.filter((e) => e !== undefined); - - const entityReferencesInQueryoOptions = findAllEntityReferences(dataQueries, []) - ?.map((entity) => { - if (entity && isValidUUID(entity)) { - return entity; - } - }) - ?.filter((e) => e !== undefined); - - const entityReferencesInEvents = findAllEntityReferences(allEvents, []) - ?.map((entity) => { - if (entity && isValidUUID(entity)) { - return entity; - } - }) - ?.filter((e) => e !== undefined); - - 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(entityReferencesInQueryoOptions) && entityReferencesInQueryoOptions?.length > 0) { - let newQueryOptions = {}; - dataQueries?.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); - } - }); - - 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, versionSwitched); + updateEntityReferences(appJson, homePageId); }) .finally(async () => { const currentPageEvents = data.events.filter( @@ -927,6 +815,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(); @@ -1093,7 +984,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); @@ -1131,24 +1022,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' && @@ -1199,7 +1092,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]; @@ -1488,6 +1381,117 @@ const EditorComponent = (props) => { } }; + const onEditorLoad = (appJson, pageId, isPageSwitchOrVersionSwitch = 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 (isPageSwitchOrVersionSwitch || !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) { @@ -1602,7 +1606,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; @@ -1629,7 +1635,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/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 ); 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/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/Editor/SubContainer.jsx b/frontend/src/Editor/SubContainer.jsx index 1fd04a9fa0..c664c87fea 100644 --- a/frontend/src/Editor/SubContainer.jsx +++ b/frontend/src/Editor/SubContainer.jsx @@ -7,12 +7,12 @@ import update from 'immutability-helper'; import _, { isEmpty } from 'lodash'; import { componentTypes } from './WidgetManager/components'; import { addNewWidgetToTheEditor, onComponentOptionChanged, onComponentOptionsChanged } from '@/_helpers/appUtils'; -import { resolveReferences } from '@/_helpers/utils'; +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'; @@ -450,7 +450,7 @@ export const SubContainer = ({ 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 ( 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,10 +234,102 @@ 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); const currentPageEvents = this.state.events.filter( @@ -770,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 ( { _.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 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/_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/_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 f8227587c1..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) => { @@ -111,13 +110,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)); diff --git a/frontend/src/_stores/currentStateStore.js b/frontend/src/_stores/currentStateStore.js index bb7af52df0..8d319124b9 100644 --- a/frontend/src/_stores/currentStateStore.js +++ b/frontend/src/_stores/currentStateStore.js @@ -2,9 +2,6 @@ import { shallow } from 'zustand/shallow'; import { create, zustandDevTools } from './utils'; 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 = { @@ -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 48c6b94fcd..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); @@ -136,13 +141,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) { diff --git a/server/src/services/app_import_export.service.ts b/server/src/services/app_import_export.service.ts index d30e0f936e..147c024887 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'; - interface AppResourceMappings { defaultDataSourceIdMapping: Record; dataQueryMapping: Record; diff --git a/server/src/services/apps.service.ts b/server/src/services/apps.service.ts index fa28f609bd..f1f54bb27a 100644 --- a/server/src/services/apps.service.ts +++ b/server/src/services/apps.service.ts @@ -25,12 +25,19 @@ 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 { VersionReleaseDto } from '@dto/version-release.dto'; +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( @@ -392,6 +399,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, @@ -405,6 +417,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, @@ -416,8 +486,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];