diff --git a/frontend/.version b/frontend/.version index ac067ed8bf..26c014497f 100644 --- a/frontend/.version +++ b/frontend/.version @@ -1 +1 @@ -2.61.0 \ No newline at end of file +2.61.0 diff --git a/frontend/src/Editor/CodeEditor/PreviewBox.jsx b/frontend/src/Editor/CodeEditor/PreviewBox.jsx index 61ac9dfc27..2a5d0884c5 100644 --- a/frontend/src/Editor/CodeEditor/PreviewBox.jsx +++ b/frontend/src/Editor/CodeEditor/PreviewBox.jsx @@ -1,5 +1,5 @@ import React, { useEffect, useState } from 'react'; -import { computeCoercion, getCurrentNodeType, resolveReferences } from './utils'; +import { computeCoercion, getCurrentNodeType, hasDeepChildren, resolveReferences } from './utils'; import CodeHinter from '.'; import { copyToClipboard } from '@/_helpers/appUtils'; import { Alert } from '@/_ui/Alert/Alert'; @@ -17,6 +17,7 @@ export const PreviewBox = ({ setErrorStateActive, setErrorMessage, customVariables, + isWorkspaceVariable, }) => { const [resolvedValue, setResolvedValue] = useState(''); const [error, setError] = useState(null); @@ -68,7 +69,7 @@ export const PreviewBox = ({ useEffect(() => { const [valid, _error, newValue, resolvedValue] = resolveReferences(currentValue, validationSchema, customVariables); - if (!validationSchema || isEmpty(validationSchema)) { + if (isWorkspaceVariable || !validationSchema || isEmpty(validationSchema)) { return setResolvedValue(newValue); } @@ -115,6 +116,7 @@ export const PreviewBox = ({ resolvedValue={content} coersionData={coersionData} withValidation={!isEmpty(validationSchema)} + isWorkspaceVariable={isWorkspaceVariable} /> copyToClipboard(error ? error?.value : content)} @@ -125,7 +127,14 @@ export const PreviewBox = ({ ); }; -const RenderResolvedValue = ({ error, previewType, resolvedValue, coersionData, withValidation }) => { +const RenderResolvedValue = ({ + error, + previewType, + resolvedValue, + coersionData, + withValidation, + isWorkspaceVariable, +}) => { const computeCoersionPreview = (resolvedValue, coersionData) => { if (coersionData?.typeBeforeCoercion === coersionData?.typeAfterCoercion) return resolvedValue; @@ -140,12 +149,13 @@ const RenderResolvedValue = ({ error, previewType, resolvedValue, coersionData, return resolvedValue + coersionData?.coercionPreview; }; - const previewValueType = - withValidation || (coersionData && coersionData?.typeBeforeCoercion) - ? `${coersionData?.typeBeforeCoercion} ${ - coersionData?.coercionPreview ? ` → ${coersionData?.typeAfterCoercion}` : '' - }` - : previewType; + const previewValueType = isWorkspaceVariable + ? previewType + : withValidation || (coersionData && coersionData?.typeBeforeCoercion) + ? `${coersionData?.typeBeforeCoercion} ${ + coersionData?.coercionPreview ? ` → ${coersionData?.typeAfterCoercion}` : '' + }` + : previewType; const previewContent = !withValidation ? resolvedValue : computeCoersionPreview(resolvedValue, coersionData); @@ -341,6 +351,8 @@ const PreviewCodeBlock = ({ code, isExpectValue = false }) => { if (showJSONTree) { const darkMode = localStorage.getItem('darkMode') === 'true'; + const hasDeepChild = hasDeepChildren(prettyPrintedJson); + return (
{ enableClipboard={false} rootName={false} theme={darkMode ? 'dark' : 'light'} - groupArraysAfterLength={500} + groupArraysAfterLength={hasDeepChild ? 10 : 100} />
); diff --git a/frontend/src/Editor/CodeEditor/SingleLineCodeEditor.jsx b/frontend/src/Editor/CodeEditor/SingleLineCodeEditor.jsx index 7da44df604..7d1606dff7 100644 --- a/frontend/src/Editor/CodeEditor/SingleLineCodeEditor.jsx +++ b/frontend/src/Editor/CodeEditor/SingleLineCodeEditor.jsx @@ -193,6 +193,7 @@ const EditorInput = ({ class: 'cm-completionInfo-top cm-custom-completion-info', }; }, + maxRenderedOptions: 10, }); const customKeyMaps = [...defaultKeymap, ...completionKeymap]; diff --git a/frontend/src/Editor/CodeEditor/utils.js b/frontend/src/Editor/CodeEditor/utils.js index 53ddfaf6bf..6d82df0a98 100644 --- a/frontend/src/Editor/CodeEditor/utils.js +++ b/frontend/src/Editor/CodeEditor/utils.js @@ -101,26 +101,25 @@ const resolveWorkspaceVariables = (query) => { let resolvedStr = query; let error = null; let valid = false; + // Resolve %%object%% const serverRegex = /(%%.+?%%)/g; - const serverMatch = resolvedStr.match(serverRegex)?.[0]; + const serverMatches = resolvedStr.match(serverRegex); - if (serverMatch) { - const code = serverMatch.replace(/%%/g, ''); + if (serverMatches) { + serverMatches.forEach((serverMatch) => { + const code = serverMatch.replace(/%%/g, ''); - if (code.includes('server.')) { - resolvedStr = resolvedStr.replace(serverMatch, 'HiddenEnvironmentVariable'); - error = 'Server variables cannot be resolved in the client.'; - } else { - const [resolvedCode, err] = resolveCode(code); - - if (!resolvedCode) { - error = err ? err : `Cannot resolve ${query}`; + if (code.includes('server.') && !/^server\.[A-Za-z0-9]+$/.test(code)) { + resolvedStr = resolvedStr.replace(serverMatch, 'HiddenEnvironmentVariable'); } else { + const resolvedCode = resolveCode(code); + resolvedStr = resolvedStr.replace(serverMatch, resolvedCode); - valid = true; } - } + }); + + valid = true; } return [valid, error, resolvedStr]; @@ -408,3 +407,19 @@ export const validateComponentProperty = (resolvedValue, validation) => { return validate(resolvedValue, schema, defaultValue, true); }; + +export function hasDeepChildren(obj, currentDepth = 1, maxDepth = 3) { + if (currentDepth > maxDepth) { + return true; + } + + for (const key in obj) { + if (typeof obj[key] === 'object' && obj[key] !== null) { + if (hasDeepChildren(obj[key], currentDepth + 1, maxDepth)) { + return true; + } + } + } + + return false; +} diff --git a/frontend/src/Editor/Editor.jsx b/frontend/src/Editor/Editor.jsx index 5119d929a8..ba152abe85 100644 --- a/frontend/src/Editor/Editor.jsx +++ b/frontend/src/Editor/Editor.jsx @@ -360,6 +360,19 @@ const EditorComponent = (props) => { () => { const components = appDefinition?.pages?.[currentPageId]?.components || {}; computeComponentState(components); + + const isPageSwitched = useResolveStore.getState().isPageSwitched; + + if (isPageSwitched) { + const currentStateObj = useCurrentStateStore.getState(); + + useResolveStore.getState().actions.addAppSuggestions({ + queries: currentStateObj.queries, + components: currentStateObj.components, + page: currentStateObj.page, + }); + useResolveStore.getState().actions.pageSwitched(false); + } }, // eslint-disable-next-line react-hooks/exhaustive-deps [currentPageId] @@ -745,7 +758,7 @@ const EditorComponent = (props) => { user_id: userId, events, } = appData; - useResolveStore.getState().actions.updateJSHints(); + const startingPageHandle = props.params.pageHandle; setWindowTitle({ page: pageTitles.EDITOR, appName }); useAppVersionStore.getState().actions.updateEditingVersion(editing_version); @@ -766,7 +779,7 @@ const EditorComponent = (props) => { await processNewAppDefinition(appData, startingPageHandle, false, ({ homePageId }) => { handleLowPriorityWork(async () => { - useResolveStore.getState().actions.updateLastUpdatedRefs(['constants']); + useResolveStore.getState().actions.updateLastUpdatedRefs(['constants', 'client']); await useDataSourcesStore.getState().actions.fetchGlobalDataSources(organizationId); await fetchDataSources(editing_version?.id); commonLowPriorityActions(events, { homePageId }); @@ -782,6 +795,7 @@ const EditorComponent = (props) => { }; const processNewAppDefinition = async (data, startingPageHandle, versionSwitched = false, onComplete) => { + useResolveStore.getState().actions.updateJSHints(); const appDefData = buildAppDefinition(data); const appJson = appDefData; @@ -1433,6 +1447,7 @@ const EditorComponent = (props) => { useCurrentStateStore.getState().actions.setEditorReady(true); const currentComponents = appJson?.pages?.[pageId]?.components; + const currentDataQueries = useDataQueriesStore.getState().dataQueries; const referenceManager = useResolveStore.getState().referenceMapper; @@ -1446,8 +1461,17 @@ const EditorComponent = (props) => { }; } }); + const newDataQueries = currentDataQueries.map((dq) => { + if (!referenceManager.get(dq.id)) { + return { + id: dq.id, + name: dq.name, + }; + } + }); - useResolveStore.getState().actions.addEntitiesToMap(newComponents); + useResolveStore.getState().actions.addEntitiesToMap([...newComponents, ...newDataQueries]); + // useResolveStore.getState().actions.addEntitiesToMap(newDataQueries); }; const updateEntityReferences = (appJson, pageId) => { @@ -1668,11 +1692,11 @@ const EditorComponent = (props) => { switchPage: true, pageId: newPageId, }); - props?.navigate(`/${getWorkspaceId()}/apps/${slug ?? appId}/${newHandle}`, { - state: { - isSwitchingPage: true, - }, - }); + // props?.navigate(`/${getWorkspaceId()}/apps/${slug ?? appId}/${newHandle}`, { + // state: { + // isSwitchingPage: true, + // }, + // }); const page = { id: newPageId, @@ -1707,14 +1731,15 @@ const EditorComponent = (props) => { return; } - clearAllQueuedTasks(); + await clearAllQueuedTasks(); + useResolveStore.getState().actions.resetStore(); useEditorStore.getState().actions.setPageProgress(true); 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; - const pageHandle = getCurrentState().pageHandle; + + const pageHandle = useCurrentStateStore.getState().page?.handle; if (currentPageId === pageId && pageHandle === appDefinition?.pages[pageId]?.handle) { return; @@ -1723,6 +1748,7 @@ const EditorComponent = (props) => { if (!name || !handle) return; const copyOfAppDefinition = JSON.parse(JSON.stringify(appDefinition)); + navigateToPage(queryParams, handle); const page = { @@ -1743,9 +1769,6 @@ const EditorComponent = (props) => { await onEditorLoad(appDefinition, pageId, true); updateEntityReferences(appDefinition, pageId); - handleLowPriorityWork(() => { - useResolveStore.getState().actions.updateJSHints(); - }); setCurrentPageId(pageId); @@ -1754,9 +1777,14 @@ const EditorComponent = (props) => { .events.filter((event) => event.target === 'page' && event.sourceId === page.id); handleEvent('onPageLoad', currentPageEvents); - handleLowPriorityWork(() => { - useEditorStore.getState().actions.setPageProgress(false); - }, 100); + handleLowPriorityWork( + () => { + useEditorStore.getState().actions.setPageProgress(false); + useResolveStore.getState().actions.updateJSHints(); + }, + null, + true + ); }; const deletePageRequest = (pageId, isHomePage = false, pageName = '') => { diff --git a/frontend/src/Editor/Viewer.jsx b/frontend/src/Editor/Viewer.jsx index 7255a01195..a2df89748a 100644 --- a/frontend/src/Editor/Viewer.jsx +++ b/frontend/src/Editor/Viewer.jsx @@ -326,12 +326,14 @@ class ViewerComponent extends React.Component { queryState[query.name] = { ...exposedVariables, ...this.props.currentState.queries[query.name], + id: query.id, }; } else { const dataSourceTypeDetail = DataSourceTypes.find((source) => source.kind === query.kind); queryState[query.name] = { ...dataSourceTypeDetail.exposedVariables, ...this.props.currentState.queries[query.name], + id: query.id, }; } }); @@ -754,7 +756,7 @@ class ViewerComponent extends React.Component { if (currentPageComponents && !_.isEmpty(currentPageComponents)) { const referenceManager = useResolveStore.getState().referenceMapper; - + const currentDataQueries = useDataQueriesStore.getState().dataQueries; const newComponents = Object.keys(currentPageComponents).map((componentId) => { const component = currentPageComponents[componentId]; @@ -765,9 +767,18 @@ class ViewerComponent extends React.Component { }; } }); + const newDataQueries = currentDataQueries.map((dq) => { + if (!referenceManager.get(dq.id)) { + return { + id: dq.id, + name: dq.name, + }; + } + }); try { useResolveStore.getState().actions.addEntitiesToMap(newComponents); + useResolveStore.getState().actions.addEntitiesToMap(newDataQueries); } catch (error) { console.error(error); } @@ -1062,6 +1073,18 @@ const withStore = (Component) => (props) => { flushComponentsToRender(updatedComponentIds); } + React.useEffect(() => { + const currentComponentsDef = appDefinition?.pages?.[currentPageId]?.components || {}; + const currentComponents = Object.keys(currentComponentsDef); + + setTimeout(() => { + if (currentComponents.length > 0) { + batchUpdateComponents(currentComponents); + } + }, 400); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [currentPageId]); + React.useEffect(() => { if (lastUpdatedRef.length > 0) { const currentComponents = appDefinition?.pages?.[currentPageId]?.components || {}; diff --git a/frontend/src/_helpers/editorHelpers.js b/frontend/src/_helpers/editorHelpers.js index a0d1dacd18..6cb57414df 100644 --- a/frontend/src/_helpers/editorHelpers.js +++ b/frontend/src/_helpers/editorHelpers.js @@ -149,7 +149,10 @@ function findReferenceInComponent(node, changedCurrentState) { 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.includes('%%client')) + ) { // Check if the referenced entity is in the state if (changedCurrentState.some((state) => value.includes(state))) { return true; diff --git a/frontend/src/_stores/currentStateStore.js b/frontend/src/_stores/currentStateStore.js index e2e16b123d..3f4d746312 100644 --- a/frontend/src/_stores/currentStateStore.js +++ b/frontend/src/_stores/currentStateStore.js @@ -105,7 +105,7 @@ useCurrentStateStore.subscribe((state) => { () => { useResolveStore.getState().actions.updateAppSuggestions({ queries: state.queries, - components: state.components, + components: !isPageSwitched ? state.components : {}, globals: state.globals, page: state.page, variables: state.variables, @@ -113,7 +113,6 @@ useCurrentStateStore.subscribe((state) => { server: state.server, constants: state.constants, }); - useResolveStore.getState().actions.pageSwitched(false); }, null, isPageSwitched diff --git a/frontend/src/_stores/resolverStore.js b/frontend/src/_stores/resolverStore.js index 14f84c615e..5deb238ac9 100644 --- a/frontend/src/_stores/resolverStore.js +++ b/frontend/src/_stores/resolverStore.js @@ -71,7 +71,7 @@ export const useResolveStore = create( set(() => ({ ...state, storeReady: true })); }, resetStore: () => { - set(() => initialState); + set(() => ({ ...initialState, referenceMapper: new ReferencesBiMap() })); }, pageSwitched: (bool) => set(() => ({ isPageSwitched: bool })), updateAppSuggestions: (refState) => { diff --git a/server/.version b/server/.version index ac067ed8bf..26c014497f 100644 --- a/server/.version +++ b/server/.version @@ -1 +1 @@ -2.61.0 \ No newline at end of file +2.61.0