diff --git a/app/client/cypress/e2e/Regression/ServerSide/GenerateCRUD/MongoURI_Spec.ts b/app/client/cypress/e2e/Regression/ServerSide/GenerateCRUD/MongoURI_Spec.ts index 005bd2f19f..b175a2010f 100644 --- a/app/client/cypress/e2e/Regression/ServerSide/GenerateCRUD/MongoURI_Spec.ts +++ b/app/client/cypress/e2e/Regression/ServerSide/GenerateCRUD/MongoURI_Spec.ts @@ -155,7 +155,7 @@ describe("Validate Mongo URI CRUD with JSON Form", () => { table.OpenNFilterTable("title", "contains", "USB"); for (let i = 0; i < 3; i++) { - table.ReadTableRowColumnData(i, 5, "v1").then(($cellData) => { + table.ReadTableRowColumnData(i, 6, "v1").then(($cellData) => { expect($cellData).contains("USB"); }); } diff --git a/app/client/src/actions/evaluationActions.ts b/app/client/src/actions/evaluationActions.ts index a205914c47..eedd569060 100644 --- a/app/client/src/actions/evaluationActions.ts +++ b/app/client/src/actions/evaluationActions.ts @@ -4,11 +4,10 @@ import { ReduxActionTypes, } from "@appsmith/constants/ReduxActionConstants"; import { intersection, union } from "lodash"; -import type { DataTree } from "entities/DataTree/dataTreeFactory"; import type { DependencyMap } from "utils/DynamicBindingUtils"; -import type { Diff } from "deep-diff"; import type { QueryActionConfig } from "entities/Action"; import type { DatasourceConfiguration } from "entities/Datasource"; +import type { DiffWithReferenceState } from "workers/Evaluation/helpers"; export const FIRST_EVAL_REDUX_ACTIONS = [ // Pages @@ -158,8 +157,8 @@ export function shouldLog(action: ReduxAction) { } export const setEvaluatedTree = ( - updates: Diff[], -): ReduxAction<{ updates: Diff[] }> => { + updates: DiffWithReferenceState[], +): ReduxAction<{ updates: DiffWithReferenceState[] }> => { return { type: ReduxActionTypes.SET_EVALUATED_TREE, payload: { updates }, diff --git a/app/client/src/ce/workers/Evaluation/__tests__/dataTreeUtils.test.ts b/app/client/src/ce/workers/Evaluation/__tests__/dataTreeUtils.test.ts index a9ec24b0d6..5ebf71f292 100644 --- a/app/client/src/ce/workers/Evaluation/__tests__/dataTreeUtils.test.ts +++ b/app/client/src/ce/workers/Evaluation/__tests__/dataTreeUtils.test.ts @@ -1,5 +1,8 @@ import type { DataTree } from "entities/DataTree/dataTreeFactory"; import { makeEntityConfigsAsObjProperties } from "@appsmith/workers/Evaluation/dataTreeUtils"; +import { smallDataSet } from "workers/Evaluation/__tests__/generateOpimisedUpdates.test"; +import produce from "immer"; +import { cloneDeep } from "lodash"; const unevalTreeFromMainThread = { Api2: { @@ -143,11 +146,215 @@ const unevalTreeFromMainThread = { }; describe("7. Test util methods", () => { - it("3. makeDataTreeEntityConfigAsProperty method", () => { - const dataTree = makeEntityConfigsAsObjProperties( - unevalTreeFromMainThread as unknown as DataTree, - ); + describe("makeDataTreeEntityConfigAsProperty", () => { + it("should not introduce __evaluation__ property", () => { + const dataTree = makeEntityConfigsAsObjProperties( + unevalTreeFromMainThread as unknown as DataTree, + ); - expect(dataTree.Api2).not.toHaveProperty("__evaluation__"); + expect(dataTree.Api2).not.toHaveProperty("__evaluation__"); + }); + describe("identicalEvalPathsPatches decompress updates", () => { + it("should decompress identicalEvalPathsPatches updates into evalProps and state", () => { + const state = { + Table1: { + filteredTableData: smallDataSet, + selectedRows: [], + pageSize: 0, + __evaluation__: { + evaluatedValues: {}, + }, + }, + } as any; + + const identicalEvalPathsPatches = { + "Table1.__evaluation__.evaluatedValues.['filteredTableData']": + "Table1.filteredTableData", + }; + const evalProps = { + Table1: { + __evaluation__: { + evaluatedValues: { + someProp: "abc", + }, + }, + }, + } as any; + const dataTree = makeEntityConfigsAsObjProperties(state as any, { + sanitizeDataTree: true, + evalProps, + identicalEvalPathsPatches, + }); + const expectedState = produce(state, (draft: any) => { + draft.Table1.__evaluation__.evaluatedValues.someProp = "abc"; + draft.Table1.__evaluation__.evaluatedValues.filteredTableData = + smallDataSet; + }); + + expect(dataTree).toEqual(expectedState); + //evalProps should have decompressed updates in it coming from identicalEvalPathsPatches + const expectedEvalProps = produce(evalProps, (draft: any) => { + draft.Table1.__evaluation__.evaluatedValues.filteredTableData = + smallDataSet; + }); + expect(evalProps).toEqual(expectedEvalProps); + }); + + it("should not make any updates to evalProps when the identicalEvalPathsPatches is empty", () => { + const state = { + Table1: { + filteredTableData: smallDataSet, + selectedRows: [], + pageSize: 0, + __evaluation__: { + evaluatedValues: {}, + }, + }, + } as any; + + const identicalEvalPathsPatches = {}; + const initialEvalProps = {} as any; + const evalProps = cloneDeep(initialEvalProps); + const dataTree = makeEntityConfigsAsObjProperties(state, { + sanitizeDataTree: true, + evalProps, + identicalEvalPathsPatches, + }); + + expect(dataTree).toEqual(dataTree); + //evalProps not be mutated with any updates + expect(evalProps).toEqual(initialEvalProps); + }); + + it("should ignore non relevant identicalEvalPathsPatches updates into evalProps and state", () => { + const state = { + Table1: { + filteredTableData: smallDataSet, + selectedRows: [], + pageSize: 0, + __evaluation__: { + evaluatedValues: {}, + }, + }, + } as any; + //ignore non existent widget state + const identicalEvalPathsPatches = { + "SomeWidget.__evaluation__.evaluatedValues.['filteredTableData']": + "SomeWidget.filteredTableData", + }; + + const initialEvalProps = { + Table1: { + __evaluation__: { + evaluatedValues: { + someProp: "abc", + }, + }, + }, + } as any; + const evalProps = cloneDeep(initialEvalProps); + const dataTree = makeEntityConfigsAsObjProperties(state, { + sanitizeDataTree: true, + evalProps, + identicalEvalPathsPatches, + }); + const expectedState = produce(state, (draft: any) => { + draft.Table1.__evaluation__.evaluatedValues.someProp = "abc"; + }); + + expect(dataTree).toEqual(expectedState); + //evalProps not be mutated with any updates + expect(evalProps).toEqual(initialEvalProps); + }); + }); + + describe("serialise", () => { + it("should clean out all functions in the generated state", () => { + const state = { + Table1: { + filteredTableData: smallDataSet, + selectedRows: [], + // eslint-disable-next-line @typescript-eslint/no-empty-function + someFn: () => {}, + pageSize: 0, + __evaluation__: { + evaluatedValues: {}, + }, + }, + } as any; + + const identicalEvalPathsPatches = { + "Table1.__evaluation__.evaluatedValues.['filteredTableData']": + "Table1.filteredTableData", + }; + const evalProps = { + Table1: { + __evaluation__: { + evaluatedValues: { + someProp: "abc", + // eslint-disable-next-line @typescript-eslint/no-empty-function + someEvalFn: () => {}, + }, + }, + }, + } as any; + const dataTree = makeEntityConfigsAsObjProperties(state, { + sanitizeDataTree: true, + evalProps, + identicalEvalPathsPatches, + }) as any; + const expectedState = produce(state, (draft: any) => { + draft.Table1.__evaluation__.evaluatedValues.someProp = "abc"; + delete draft.Table1.someFn; + draft.Table1.__evaluation__.evaluatedValues.filteredTableData = + smallDataSet; + }); + + expect(dataTree).toEqual(expectedState); + //function introduced by evalProps is cleaned out + expect( + dataTree.Table1.__evaluation__.evaluatedValues.someEvalFn, + ).toBeUndefined(); + }); + + it("should serialise bigInteger values", () => { + const someBigInt = BigInt(121221); + const state = { + Table1: { + pageSize: someBigInt, + __evaluation__: { + evaluatedValues: {}, + }, + }, + } as any; + + const identicalEvalPathsPatches = { + "Table1.__evaluation__.evaluatedValues.['pageSize']": + "Table1.pageSize", + }; + const evalProps = { + Table1: { + __evaluation__: { + evaluatedValues: { + someProp: someBigInt, + }, + }, + }, + } as any; + + const dataTree = makeEntityConfigsAsObjProperties(state, { + sanitizeDataTree: true, + evalProps, + identicalEvalPathsPatches, + }); + const expectedState = produce(state, (draft: any) => { + draft.Table1.pageSize = "121221"; + draft.Table1.__evaluation__.evaluatedValues.pageSize = "121221"; + draft.Table1.__evaluation__.evaluatedValues.someProp = "121221"; + }); + + expect(dataTree).toEqual(expectedState); + }); + }); }); }); diff --git a/app/client/src/ce/workers/Evaluation/dataTreeUtils.ts b/app/client/src/ce/workers/Evaluation/dataTreeUtils.ts index aba8187923..3d84a84e57 100644 --- a/app/client/src/ce/workers/Evaluation/dataTreeUtils.ts +++ b/app/client/src/ce/workers/Evaluation/dataTreeUtils.ts @@ -1,7 +1,7 @@ import type { DataTree } from "entities/DataTree/dataTreeFactory"; -import { set } from "lodash"; +import { get, set, unset } from "lodash"; import type { EvalProps } from "workers/common/DataTreeEvaluator"; -import { removeFunctions } from "@appsmith/workers/Evaluation/evaluationUtils"; +import { removeFunctionsAndSerialzeBigInt } from "@appsmith/workers/Evaluation/evaluationUtils"; /** * This method loops through each entity object of dataTree and sets the entity config from prototype as object properties. @@ -12,21 +12,61 @@ export function makeEntityConfigsAsObjProperties( option = {} as { sanitizeDataTree?: boolean; evalProps?: EvalProps; + identicalEvalPathsPatches?: Record; }, ): DataTree { - const { evalProps, sanitizeDataTree = true } = option; + const { + evalProps, + identicalEvalPathsPatches, + sanitizeDataTree = true, + } = option; const newDataTree: DataTree = {}; for (const entityName of Object.keys(dataTree)) { const entity = dataTree[entityName]; newDataTree[entityName] = Object.assign({}, entity); } const dataTreeToReturn = sanitizeDataTree - ? JSON.parse(JSON.stringify(newDataTree)) + ? removeFunctionsAndSerialzeBigInt(newDataTree) : newDataTree; if (!evalProps) return dataTreeToReturn; - const sanitizedEvalProps = removeFunctions(evalProps) as EvalProps; + //clean up deletes widget states + Object.entries(identicalEvalPathsPatches || {}).forEach( + ([evalPath, statePath]) => { + const [entity] = statePath.split("."); + if (!dataTreeToReturn[entity]) { + delete identicalEvalPathsPatches?.[evalPath]; + } + }, + ); + + // decompressIdenticalEvalPaths + Object.entries(identicalEvalPathsPatches || {}).forEach( + ([evalPath, statePath]) => { + const referencePathValue = get(dataTreeToReturn, statePath); + set(evalProps, evalPath, referencePathValue); + }, + ); + + const alreadySanitisedDataSet = {} as EvalProps; + Object.keys(identicalEvalPathsPatches || {}).forEach((evalPath) => { + const val = get(evalProps, evalPath); + //serialised already + alreadySanitisedDataSet[evalPath] = val; + //we are seperating it from evalProps because we don't want to serialise this identical data unecessarily again + unset(evalProps, evalPath); + }); + + const sanitizedEvalProps = removeFunctionsAndSerialzeBigInt( + evalProps, + ) as EvalProps; + Object.entries(alreadySanitisedDataSet).forEach(([path, val]) => { + // add it to sanitised Eval props + set(sanitizedEvalProps, path, val); + //restore it to evalProps + set(evalProps, path, val); + }); for (const [entityName, entityEvalProps] of Object.entries( sanitizedEvalProps, )) { diff --git a/app/client/src/ce/workers/Evaluation/evaluationUtils.ts b/app/client/src/ce/workers/Evaluation/evaluationUtils.ts index ed761f60a0..3f5f4092b2 100644 --- a/app/client/src/ce/workers/Evaluation/evaluationUtils.ts +++ b/app/client/src/ce/workers/Evaluation/evaluationUtils.ts @@ -418,17 +418,17 @@ export function isDataTreeEntity(entity: unknown) { return !!entity && typeof entity === "object" && "ENTITY_TYPE" in entity; } +export const removeFunctionsAndSerialzeBigInt = (value: any) => + JSON.parse( + JSON.stringify(value, (_, v) => (typeof v === "bigint" ? v.toString() : v)), + ); // We need to remove functions from data tree to avoid any unexpected identifier while JSON parsing // Check issue https://github.com/appsmithorg/appsmith/issues/719 export const removeFunctions = (value: any) => { if (_.isFunction(value)) { return "Function call"; } else if (_.isObject(value)) { - return JSON.parse( - JSON.stringify(value, (_, v) => - typeof v === "bigint" ? v.toString() : v, - ), - ); + return removeFunctionsAndSerialzeBigInt(value); } else { return value; } diff --git a/app/client/src/components/editorComponents/GlobalSearch/Description.tsx b/app/client/src/components/editorComponents/GlobalSearch/Description.tsx index b1e355d987..2323d3a731 100644 --- a/app/client/src/components/editorComponents/GlobalSearch/Description.tsx +++ b/app/client/src/components/editorComponents/GlobalSearch/Description.tsx @@ -199,4 +199,4 @@ function Description(props: Props) { ); } -export default Description; +export default React.memo(Description); diff --git a/app/client/src/components/editorComponents/GlobalSearch/GlobalSearchHooks.tsx b/app/client/src/components/editorComponents/GlobalSearch/GlobalSearchHooks.tsx index 8ccc7971db..5bd3c68988 100644 --- a/app/client/src/components/editorComponents/GlobalSearch/GlobalSearchHooks.tsx +++ b/app/client/src/components/editorComponents/GlobalSearch/GlobalSearchHooks.tsx @@ -35,7 +35,6 @@ import type { AppState } from "@appsmith/reducers"; import { getCurrentAppWorkspace } from "@appsmith/selectors/workspaceSelectors"; import { importRemixIcon } from "design-system-old"; import AnalyticsUtil from "utils/AnalyticsUtil"; - const AddLineIcon = importRemixIcon( () => import("remixicon-react/AddLineIcon"), ); @@ -44,12 +43,13 @@ export const useFilteredFileOperations = (query = "") => { const { appWideDS = [], otherDS = [] } = useAppWideAndOtherDatasource(); const recentDatasourceIds = useSelector(getRecentDatasourceIds); // helper map for sorting based on recent usage - const recentlyUsedOrderMap = recentDatasourceIds.reduce( - (map: Record, id, index) => { - map[id] = index; - return map; - }, - {}, + const recentlyUsedOrderMap = useMemo( + () => + recentDatasourceIds.reduce((map: Record, id, index) => { + map[id] = index; + return map; + }, {}), + [recentDatasourceIds], ); /** * Work around to get the rest api cloud image. @@ -89,7 +89,15 @@ export const useFilteredFileOperations = (query = "") => { canCreateDatasource, pagePermissions, ), - [query, appWideDS, otherDS], + [ + appWideDS, + canCreateActions, + canCreateDatasource, + otherDS, + pagePermissions, + query, + recentlyUsedOrderMap, + ], ); }; @@ -198,8 +206,8 @@ export const getFilteredAndSortedFileOperations = ( export const useFilteredWidgets = (query: string) => { const allWidgets = useSelector(getAllPageWidgets); - const pages = useSelector(getPageList) || []; - const pageMap = keyBy(pages, "pageId"); + const pages = useSelector(getPageList); + const pageMap = useMemo(() => keyBy(pages || [], "pageId"), [pages]); const searchableWidgets = useMemo( () => allWidgets.filter( @@ -218,7 +226,7 @@ export const useFilteredWidgets = (query: string) => { return isWidgetNameMatching || isPageNameMatching; }); - }, [allWidgets, query, pages]); + }, [query, searchableWidgets, pageMap]); }; export const useFilteredActions = (query: string) => { @@ -234,7 +242,7 @@ export const useFilteredActions = (query: string) => { return isActionNameMatching || isPageNameMatching; }); - }, [actions, query, pages]); + }, [query, actions, pageMap]); }; export const useFilteredJSCollections = (query: string) => { @@ -252,12 +260,14 @@ export const useFilteredJSCollections = (query: string) => { return isActionNameMatching || isPageNameMatching; }); - }, [jsActions, query, pages]); + }, [query, jsActions, pageMap]); }; export const useFilteredPages = (query: string) => { - const pages = useSelector(getPageList) || []; + const pages = useSelector(getPageList); + return useMemo(() => { + if (!pages) return []; if (!query) return attachKind(pages, SEARCH_ITEM_TYPES.page); return attachKind( pages.filter( diff --git a/app/client/src/components/editorComponents/GlobalSearch/ResultsNotFound.tsx b/app/client/src/components/editorComponents/GlobalSearch/ResultsNotFound.tsx index 30e6b18fe1..c96b0ee3d2 100644 --- a/app/client/src/components/editorComponents/GlobalSearch/ResultsNotFound.tsx +++ b/app/client/src/components/editorComponents/GlobalSearch/ResultsNotFound.tsx @@ -71,4 +71,4 @@ function ResultsNotFound() { ); } -export default ResultsNotFound; +export default React.memo(ResultsNotFound); diff --git a/app/client/src/components/editorComponents/GlobalSearch/SearchBox.tsx b/app/client/src/components/editorComponents/GlobalSearch/SearchBox.tsx index 543a0bbc87..d09e3842bb 100644 --- a/app/client/src/components/editorComponents/GlobalSearch/SearchBox.tsx +++ b/app/client/src/components/editorComponents/GlobalSearch/SearchBox.tsx @@ -116,7 +116,7 @@ function SearchBox({ category, query, setCategory, setQuery }: SearchBoxProps) { setQuery(query); (document.querySelector("#global-search") as HTMLInputElement)?.focus(); }, - [listenToChange], + [listenToChange, setQuery], ); return ( @@ -162,4 +162,4 @@ function SearchBox({ category, query, setCategory, setQuery }: SearchBoxProps) { ); } -export default SearchBox; +export default React.memo(SearchBox); diff --git a/app/client/src/components/editorComponents/GlobalSearch/SearchResults.tsx b/app/client/src/components/editorComponents/GlobalSearch/SearchResults.tsx index e317d54fa2..ee2eb7fa70 100644 --- a/app/client/src/components/editorComponents/GlobalSearch/SearchResults.tsx +++ b/app/client/src/components/editorComponents/GlobalSearch/SearchResults.tsx @@ -477,4 +477,4 @@ function SearchResults({ ); } -export default SearchResults; +export default React.memo(SearchResults); diff --git a/app/client/src/components/editorComponents/GlobalSearch/index.tsx b/app/client/src/components/editorComponents/GlobalSearch/index.tsx index bbbb42f6b3..a9a4624b07 100644 --- a/app/client/src/components/editorComponents/GlobalSearch/index.tsx +++ b/app/client/src/components/editorComponents/GlobalSearch/index.tsx @@ -5,7 +5,7 @@ import React, { useRef, useState, } from "react"; -import { useDispatch, useSelector } from "react-redux"; +import { shallowEqual, useDispatch, useSelector } from "react-redux"; import styled, { ThemeProvider } from "styled-components"; import { useParams } from "react-router"; import history, { NavigationMethod } from "utils/history"; @@ -140,7 +140,7 @@ const getSortedResults = ( }; const filterCategoryList = getFilterCategoryList(); - +const emptyObj = {}; function GlobalSearch() { const currentPageId = useSelector(getCurrentPageId) as string; const modalOpen = useSelector(isModalOpenSelector); @@ -160,7 +160,7 @@ function GlobalSearch() { (category: SearchCategory) => { dispatch(setGlobalSearchFilterContext({ category: category })); }, - [dispatch, setGlobalSearchFilterContext], + [dispatch], ); const params = useParams(); @@ -197,29 +197,32 @@ function GlobalSearch() { return state.entities.datasources.list.filter( (datasource) => datasource.id !== TEMP_DATASOURCE_ID, ); - }); + }, shallowEqual); const datasourcesList = useMemo(() => { return reducerDatasources.map((datasource) => ({ ...datasource, pageId: params?.pageId, })); - }, [reducerDatasources]); + }, [params?.pageId, reducerDatasources]); const filteredDatasources = useMemo(() => { if (!query) return datasourcesList; return datasourcesList.filter((datasource) => isMatching(datasource.name, query), ); - }, [reducerDatasources, query]); + }, [datasourcesList, query]); const recentEntities = useRecentEntities(); const recentEntityIds = recentEntities .map((r) => getEntityId(r)) .filter(Boolean); - const recentEntityIndex = (entity: any) => { - const id = - entity.id || entity.widgetId || entity.config?.id || entity.pageId; - return recentEntityIds.indexOf(id); - }; + const recentEntityIndex = useCallback( + (entity: any) => { + const id = + entity.id || entity.widgetId || entity.config?.id || entity.pageId; + return recentEntityIds.indexOf(id); + }, + [recentEntityIds], + ); const resetSearchQuery = useSelector(searchQuerySelector); const lastSelectedWidgetId = useSelector(getLastSelectedWidget); @@ -276,16 +279,20 @@ function GlobalSearch() { currentPageId, ); }, [ - filteredWidgets, + category, + currentPageId, filteredActions, - filteredJSCollections, filteredDatasources, + filteredFileOperations, + filteredJSCollections, + filteredPages, + filteredWidgets, query, - recentEntities, + recentEntityIndex, ]); const activeItem = useMemo(() => { - return searchResults[activeItemIndex] || {}; + return searchResults[activeItemIndex] || emptyObj; }, [searchResults, activeItemIndex]); const getNextActiveItem = (nextIndex: number) => { diff --git a/app/client/src/components/editorComponents/GlobalSearch/useRecentEntities.tsx b/app/client/src/components/editorComponents/GlobalSearch/useRecentEntities.tsx index 8128646919..a84bbbfd87 100644 --- a/app/client/src/components/editorComponents/GlobalSearch/useRecentEntities.tsx +++ b/app/client/src/components/editorComponents/GlobalSearch/useRecentEntities.tsx @@ -11,10 +11,11 @@ import { get } from "lodash"; import type { JSCollectionData } from "reducers/entityReducers/jsActionsReducer"; import { FocusEntity } from "navigation/FocusEntity"; import type { DataTreeEntityObject } from "entities/DataTree/dataTreeFactory"; +import { useMemo } from "react"; const recentEntitiesSelector = (state: AppState) => state.ui.globalSearch.recentEntities || []; - +const emptyArr: any = []; const useResentEntities = (): Array< DataTreeEntityObject & { entityType: FocusEntity; @@ -28,50 +29,56 @@ const useResentEntities = (): Array< return state.entities.datasources.list; }); - const pages = useSelector(getPageList) || []; - - return (recentEntities || []) - .map((entity) => { - const { id, pageId, type } = entity; - if (type === FocusEntity.PAGE) { - const result = pages.find((page) => page.pageId === id); - if (result) { - return { - ...result, - entityType: type, - kind: SEARCH_ITEM_TYPES.page, - }; - } else { - return null; - } - } else if (type === FocusEntity.DATASOURCE) { - const datasource = reducerDatasources.find( - (reducerDatasource) => reducerDatasource.id === id, - ); - return ( - datasource && { - ...datasource, - entityType: type, - pageId, + const pages = useSelector(getPageList); + const result = useMemo( + () => + (recentEntities || emptyArr) + .map((entity) => { + const { id, pageId, type } = entity; + if (type === FocusEntity.PAGE) { + if (!pages) return null; + const result = pages.find((page) => page.pageId === id); + if (result) { + return { + ...result, + entityType: type, + kind: SEARCH_ITEM_TYPES.page, + }; + } else { + return null; + } + } else if (type === FocusEntity.DATASOURCE) { + const datasource = reducerDatasources.find( + (reducerDatasource) => reducerDatasource.id === id, + ); + return ( + datasource && { + ...datasource, + entityType: type, + pageId, + } + ); + } else if (type === FocusEntity.API || type === FocusEntity.QUERY) + return { + ...actions.find((action) => action?.config?.id === id), + entityType: type, + }; + else if (type === FocusEntity.JS_OBJECT) + return { + ...jsActions.find( + (action: JSCollectionData) => action?.config?.id === id, + ), + entityType: type, + }; + else if (type === FocusEntity.PROPERTY_PANE) { + return { ...get(widgetsMap, id, null), entityType: type }; } - ); - } else if (type === FocusEntity.API || type === FocusEntity.QUERY) - return { - ...actions.find((action) => action?.config?.id === id), - entityType: type, - }; - else if (type === FocusEntity.JS_OBJECT) - return { - ...jsActions.find( - (action: JSCollectionData) => action?.config?.id === id, - ), - entityType: type, - }; - else if (type === FocusEntity.PROPERTY_PANE) { - return { ...get(widgetsMap, id, null), entityType: type }; - } - }) - .filter(Boolean); + }) + .filter(Boolean), + [recentEntities, actions, jsActions, pages, reducerDatasources, widgetsMap], + ); + + return result; }; export default useResentEntities; diff --git a/app/client/src/components/editorComponents/PropertyPaneSidebar.tsx b/app/client/src/components/editorComponents/PropertyPaneSidebar.tsx index 419a3db54e..3f85e44cfb 100644 --- a/app/client/src/components/editorComponents/PropertyPaneSidebar.tsx +++ b/app/client/src/components/editorComponents/PropertyPaneSidebar.tsx @@ -15,7 +15,6 @@ import useHorizontalResize from "utils/hooks/useHorizontalResize"; import { getIsDraggingForSelection } from "selectors/canvasSelectors"; import MultiSelectPropertyPane from "pages/Editor/MultiSelectPropertyPane"; import { getIsDraggingOrResizing } from "selectors/widgetSelectors"; -import equal from "fast-deep-equal"; import { selectedWidgetsPresentInCanvas } from "selectors/propertyPaneSelectors"; import { getIsAppSettingsPaneOpen } from "selectors/appSettingsPaneSelectors"; import AppSettingsPane from "pages/Editor/AppSettingsPane"; @@ -72,7 +71,9 @@ export const PropertyPaneSidebar = memo((props: Props) => { const keepThemeWhileDragging = prevSelectedWidgetId.current === undefined && shouldNotRenderPane; - const selectedWidgets = useSelector(selectedWidgetsPresentInCanvas, equal); + const selectedWidgetsLength = useSelector( + (state) => selectedWidgetsPresentInCanvas(state).length, + ); const isDraggingForSelection = useSelector(getIsDraggingForSelection); @@ -96,19 +97,19 @@ export const PropertyPaneSidebar = memo((props: Props) => { switch (true) { case isAppSettingsPaneOpen: return ; - case selectedWidgets.length > 1: + case selectedWidgetsLength > 1: return ; - case selectedWidgets.length === 1: + case selectedWidgetsLength === 1: if (shouldNotRenderPane) return ; else return ; - case selectedWidgets.length === 0: + case selectedWidgetsLength === 0: return ; default: return ; } }, [ isAppSettingsPaneOpen, - selectedWidgets.length, + selectedWidgetsLength, isDraggingForSelection, shouldNotRenderPane, keepThemeWhileDragging, diff --git a/app/client/src/constants/WidgetValidation.ts b/app/client/src/constants/WidgetValidation.ts index 74e0ed8886..cfe96b1ace 100644 --- a/app/client/src/constants/WidgetValidation.ts +++ b/app/client/src/constants/WidgetValidation.ts @@ -24,6 +24,7 @@ export type ValidationResponse = { parsed: any; messages?: Array; transformed?: any; + isParsedValueTheSame?: boolean; }; export type Validator = ( diff --git a/app/client/src/pages/Editor/Explorer/hooks.ts b/app/client/src/pages/Editor/Explorer/hooks.ts index 26a581389d..ea3f43ee69 100644 --- a/app/client/src/pages/Editor/Explorer/hooks.ts +++ b/app/client/src/pages/Editor/Explorer/hooks.ts @@ -1,11 +1,9 @@ -import type { MutableRefObject } from "react"; -import { useEffect, useState, useMemo, useCallback } from "react"; +import { useEffect, useMemo } from "react"; import { useSelector } from "react-redux"; import type { AppState } from "@appsmith/reducers"; import { compact, get, groupBy } from "lodash"; import type { Datasource } from "entities/Datasource"; import { isStoredDatasource } from "entities/Action"; -import { debounce } from "lodash"; import type { WidgetProps } from "widgets/BaseWidget"; import log from "loglevel"; import produce from "immer"; @@ -39,13 +37,6 @@ const findWidgets = (widgets: CanvasStructure, keyword: string) => { } }; -const findDataSources = (dataSources: Datasource[], keyword: string) => { - return dataSources.filter( - (dataSource: Datasource) => - dataSource.name.toLowerCase().indexOf(keyword.toLowerCase()) > -1, - ); -}; - export const useDatasourcesPageMapInCurrentApplication = () => { const actions = useActions(); const reducerDatasources = useSelector((state: AppState) => { @@ -76,56 +67,72 @@ export const useDatasourcesPageMapInCurrentApplication = () => { export const useCurrentApplicationDatasource = () => { const actions = useSelector(getActions); const allDatasources = useSelector(getDatasources); - const datasourceIdsUsedInCurrentApplication = actions.reduce( - (acc, action: ActionData) => { - if ( - isStoredDatasource(action.config.datasource) && - action.config.datasource.id - ) { - acc.add(action.config.datasource.id); - } - return acc; - }, - new Set(), - ); - return allDatasources.filter((ds) => - datasourceIdsUsedInCurrentApplication.has(ds.id), - ); + const currentApplicationDatasource = useMemo(() => { + const datasourceIdsUsedInCurrentApplication = actions.reduce( + (acc, action: ActionData) => { + if ( + isStoredDatasource(action.config.datasource) && + action.config.datasource.id + ) { + acc.add(action.config.datasource.id); + } + return acc; + }, + new Set(), + ); + return allDatasources.filter((ds) => + datasourceIdsUsedInCurrentApplication.has(ds.id), + ); + }, [actions, allDatasources]); + + return currentApplicationDatasource; }; export const useOtherDatasourcesInWorkspace = () => { const actions = useSelector(getActions); const allDatasources = useSelector(getDatasources); - const datasourceIdsUsedInCurrentApplication = actions.reduce( - (acc, action: ActionData) => { - if ( - isStoredDatasource(action.config.datasource) && - action.config.datasource.id - ) { - acc.add(action.config.datasource.id); - } - return acc; - }, - new Set(), - ); - return allDatasources.filter( - (ds) => - !datasourceIdsUsedInCurrentApplication.has(ds.id) && - ds.id !== TEMP_DATASOURCE_ID, - ); + const otherDatasourcesInWorkspace = useMemo(() => { + const datasourceIdsUsedInCurrentApplication = actions.reduce( + (acc, action: ActionData) => { + if ( + isStoredDatasource(action.config.datasource) && + action.config.datasource.id + ) { + acc.add(action.config.datasource.id); + } + return acc; + }, + new Set(), + ); + return allDatasources.filter( + (ds) => + !datasourceIdsUsedInCurrentApplication.has(ds.id) && + ds.id !== TEMP_DATASOURCE_ID, + ); + }, [actions, allDatasources]); + return otherDatasourcesInWorkspace; }; export const useAppWideAndOtherDatasource = () => { const datasourcesUsedInApplication = useCurrentApplicationDatasource(); const otherDatasourceInWorkspace = useOtherDatasourcesInWorkspace(); - + const appWideDS = useMemo( + () => + [...datasourcesUsedInApplication].sort((ds1, ds2) => + ds1.name?.toLowerCase()?.localeCompare(ds2.name?.toLowerCase()), + ), + [datasourcesUsedInApplication], + ); + const otherDS = useMemo( + () => + [...otherDatasourceInWorkspace].sort((ds1, ds2) => + ds1.name?.toLowerCase()?.localeCompare(ds2.name?.toLowerCase()), + ), + [otherDatasourceInWorkspace], + ); return { - appWideDS: datasourcesUsedInApplication.sort((ds1, ds2) => - ds1.name?.toLowerCase()?.localeCompare(ds2.name?.toLowerCase()), - ), - otherDS: otherDatasourceInWorkspace.sort((ds1, ds2) => - ds1.name?.toLowerCase()?.localeCompare(ds2.name?.toLowerCase()), - ), + appWideDS, + otherDS, }; }; @@ -143,69 +150,6 @@ export const useDatasourceSuggestions = () => { ); }; -export const useFilteredDatasources = (searchKeyword?: string) => { - const pageIds = usePageIds(searchKeyword); - const datasources = useDatasourcesPageMapInCurrentApplication(); - return useMemo(() => { - if (searchKeyword) { - const start = performance.now(); - const filteredDatasources = produce(datasources, (draft) => { - for (const [key, value] of Object.entries(draft)) { - if (pageIds.includes(key)) { - draft[key] = value; - } else { - draft[key] = findDataSources(value, searchKeyword); - } - } - }); - log.debug("Filtered datasources in:", performance.now() - start, "ms"); - return filteredDatasources; - } - - return datasources; - }, [searchKeyword, datasources]); -}; - -export const useJSCollections = (searchKeyword?: string) => { - const reducerActions = useSelector( - (state: AppState) => state.entities.jsActions, - ); - const pageIds = usePageIds(searchKeyword); - - const actions = useMemo(() => { - return groupBy(reducerActions, "config.pageId"); - }, [reducerActions]); - - return useMemo(() => { - if (searchKeyword) { - const start = performance.now(); - const filteredActions = produce(actions, (draft) => { - for (const [key, value] of Object.entries(draft)) { - if (pageIds.includes(key)) { - draft[key] = value; - } else { - value.forEach((action, index) => { - const searchMatches = - action.config.name - .toLowerCase() - .indexOf(searchKeyword.toLowerCase()) > -1; - if (searchMatches) { - draft[key][index] = action; - } else { - delete draft[key][index]; - } - }); - } - draft[key] = draft[key].filter(Boolean); - } - }); - log.debug("Filtered actions in:", performance.now() - start, "ms"); - return filteredActions; - } - return actions; - }, [searchKeyword, actions]); -}; - export const useActions = (searchKeyword?: string) => { const reducerActions = useSelector( (state: AppState) => state.entities.actions, @@ -243,7 +187,7 @@ export const useActions = (searchKeyword?: string) => { return filteredActions; } return actions; - }, [searchKeyword, actions]); + }, [searchKeyword, actions, pageIds]); }; export const useWidgets = (searchKeyword?: string) => { @@ -272,7 +216,7 @@ export const useWidgets = (searchKeyword?: string) => { return filteredDSLs; } return pageCanvasStructures; - }, [searchKeyword, pageCanvasStructures]); + }, [searchKeyword, pageCanvasStructures, pageIds]); }; export const usePageIds = (searchKeyword?: string) => { @@ -300,50 +244,6 @@ export const usePageIds = (searchKeyword?: string) => { }, [searchKeyword, pages]); }; -export const useFilteredEntities = ( - ref: MutableRefObject, -) => { - const start = performance.now(); - const [searchKeyword, setSearchKeyword] = useState(null); - - const search = debounce((e: any) => { - const keyword = e.target.value; - if (keyword.trim().length > 0) { - setSearchKeyword(keyword); - } else { - setSearchKeyword(null); - } - }, 300); - - const event = new Event("cleared"); - useEffect(() => { - const el: HTMLInputElement | null = ref.current; - - el?.addEventListener("keydown", search); - el?.addEventListener("cleared", search); - return () => { - el?.removeEventListener("keydown", search); - el?.removeEventListener("cleared", search); - }; - }, [ref, search]); - - const clearSearch = useCallback(() => { - const el: HTMLInputElement | null = ref.current; - - if (el && el.value.trim().length > 0) { - el.value = ""; - el?.dispatchEvent(event); - } - }, [ref, event]); - - const stop = performance.now(); - log.debug("Explorer hook props calculations took", stop - start, "ms"); - return { - searchKeyword: searchKeyword ?? undefined, - clearSearch, - }; -}; - export const useEntityUpdateState = (entityId: string) => { return useSelector((state: AppState) => get(state, "ui.explorer.entity.updatingEntity")?.includes(entityId), diff --git a/app/client/src/pages/Editor/PropertyPane/PropertyPaneConnections.tsx b/app/client/src/pages/Editor/PropertyPane/PropertyPaneConnections.tsx index dffeadc383..14702836a4 100644 --- a/app/client/src/pages/Editor/PropertyPane/PropertyPaneConnections.tsx +++ b/app/client/src/pages/Editor/PropertyPane/PropertyPaneConnections.tsx @@ -108,31 +108,40 @@ const useDependencyList = (name: string) => { ); const guidedTour = useSelector(inGuidedTour); - const getEntityId = useCallback((name) => { - const entity = dataTree[name]; + const getEntityId = useCallback( + (name) => { + const entity = dataTree[name]; - if (isWidget(entity)) { - return entity.widgetId; - } else if (isAction(entity)) { - return entity.actionId; - } - }, []); + if (isWidget(entity)) { + return entity.widgetId; + } else if (isAction(entity)) { + return entity.actionId; + } + }, + [dataTree], + ); const entityDependencies = useMemo(() => { if (guidedTour) return null; return getDependenciesFromInverseDependencies(inverseDependencyMap, name); }, [name, inverseDependencyMap, guidedTour]); - const dependencyOptions = - entityDependencies?.directDependencies.map((e) => ({ - label: e, - value: getEntityId(e) ?? e, - })) ?? []; - const inverseDependencyOptions = - entityDependencies?.inverseDependencies.map((e) => ({ - label: e, - value: getEntityId(e), - })) ?? []; + const dependencyOptions = useMemo( + () => + entityDependencies?.directDependencies.map((e) => ({ + label: e, + value: getEntityId(e) ?? e, + })) ?? [], + [entityDependencies?.directDependencies, getEntityId], + ); + const inverseDependencyOptions = useMemo( + () => + entityDependencies?.inverseDependencies.map((e) => ({ + label: e, + value: getEntityId(e), + })) ?? [], + [entityDependencies?.inverseDependencies, getEntityId], + ); return { dependencyOptions, diff --git a/app/client/src/pages/Editor/PropertyPane/PropertyPaneTab.tsx b/app/client/src/pages/Editor/PropertyPane/PropertyPaneTab.tsx index 329acb3d02..798b234b6c 100644 --- a/app/client/src/pages/Editor/PropertyPane/PropertyPaneTab.tsx +++ b/app/client/src/pages/Editor/PropertyPane/PropertyPaneTab.tsx @@ -1,4 +1,4 @@ -import React from "react"; +import React, { useCallback } from "react"; import styled from "styled-components"; import { useDispatch, useSelector } from "react-redux"; @@ -37,17 +37,20 @@ export function PropertyPaneTab(props: PropertyPaneTabProps) { getSelectedPropertyTabIndex(state, props.panelPropertyPath), ); - const setSelectedIndex = (index: number) => { - dispatch(setSelectedPropertyTabIndex(index, props.panelPropertyPath)); - }; - + const setSelectedIndex = useCallback( + (index: number) => { + dispatch(setSelectedPropertyTabIndex(index, props.panelPropertyPath)); + }, + [dispatch, props.panelPropertyPath], + ); + const onValueChange = useCallback( + (value) => { + setSelectedIndex(tabs.indexOf(value) || 0); + }, + [setSelectedIndex], + ); return ( - { - setSelectedIndex(tabs.indexOf(value) || 0); - }} - value={tabs[selectedIndex]} - > + {props.contentComponent && Content} {props.styleComponent && Style} diff --git a/app/client/src/pages/Editor/PropertyPane/PropertyPaneView.tsx b/app/client/src/pages/Editor/PropertyPane/PropertyPaneView.tsx index d5b0c92dff..5b8a51bd80 100644 --- a/app/client/src/pages/Editor/PropertyPane/PropertyPaneView.tsx +++ b/app/client/src/pages/Editor/PropertyPane/PropertyPaneView.tsx @@ -16,7 +16,6 @@ import type { WidgetType } from "constants/WidgetConstants"; import { WIDGET_ID_SHOW_WALKTHROUGH } from "constants/WidgetConstants"; import type { InteractionAnalyticsEventDetail } from "utils/AppsmithUtils"; import { INTERACTION_ANALYTICS_EVENT } from "utils/AppsmithUtils"; -import { emitInteractionAnalyticsEvent } from "utils/AppsmithUtils"; import AnalyticsUtil from "utils/AnalyticsUtil"; import { buildDeprecationWidgetMessage, isWidgetDeprecated } from "../utils"; import { Button, Callout } from "design-system"; @@ -63,7 +62,7 @@ function PropertyPaneView( } & IPanelProps, ) { const dispatch = useDispatch(); - const { ...panel } = props; + const panel = props; const widgetProperties = useSelector( getWidgetPropsForPropertyPaneView, equal, @@ -77,7 +76,7 @@ function PropertyPaneView( } return true; - }, [widgetProperties?.type, excludeList]); + }, [widgetProperties]); const { searchText, setSearchText } = useSearchText(""); const { pushFeature } = useContext(WalkthroughContext) || {}; const widgets = useSelector(getWidgets); @@ -174,19 +173,6 @@ function PropertyPaneView( */ const onCopy = useCallback(() => dispatch(copyWidget(false)), [dispatch]); - const handleTabKeyDownForButton = useCallback( - (propertyName: string) => (e: React.KeyboardEvent) => { - if (e.key === "Tab") - emitInteractionAnalyticsEvent(containerRef?.current, { - key: e.key, - propertyName, - propertyType: "BUTTON", - widgetType: widgetProperties?.type, - }); - }, - [], - ); - /** * actions shown on the right of title */ @@ -220,7 +206,7 @@ function PropertyPaneView( ), }, ]; - }, [onCopy, onDelete, handleTabKeyDownForButton]); + }, [onCopy, onDelete]); useEffect(() => { setSearchText(""); diff --git a/app/client/src/reducers/evaluationReducers/treeReducer.ts b/app/client/src/reducers/evaluationReducers/treeReducer.ts index fb27a2bf75..f3a3a9d1ed 100644 --- a/app/client/src/reducers/evaluationReducers/treeReducer.ts +++ b/app/client/src/reducers/evaluationReducers/treeReducer.ts @@ -5,6 +5,8 @@ import { applyChange } from "deep-diff"; import type { DataTree } from "entities/DataTree/dataTreeFactory"; import { createImmerReducer } from "utils/ReducerUtils"; import * as Sentry from "@sentry/react"; +import { get } from "lodash"; +import type { DiffWithReferenceState } from "workers/Evaluation/helpers"; export type EvaluatedTreeState = DataTree; @@ -15,7 +17,7 @@ const evaluatedTreeReducer = createImmerReducer(initialState, { state: EvaluatedTreeState, action: ReduxAction<{ dataTree: DataTree; - updates: Diff[]; + updates: DiffWithReferenceState[]; removedPaths: [string]; }>, ) => { @@ -29,7 +31,20 @@ const evaluatedTreeReducer = createImmerReducer(initialState, { continue; } try { - applyChange(state, undefined, update); + //these are the decompression updates, there are cases where identical values are present in the state + //over here we have the path which has the identical value and apply as an update + if (update.kind === "referenceState") { + const { path, referencePath } = update; + + const patch = { + kind: "N", + path, + rhs: get(state, referencePath), + } as Diff; + applyChange(state, undefined, patch); + } else { + applyChange(state, undefined, update); + } } catch (e) { Sentry.captureException(e, { extra: { diff --git a/app/client/src/sagas/EvaluationsSaga.ts b/app/client/src/sagas/EvaluationsSaga.ts index ecfed117df..2f83307390 100644 --- a/app/client/src/sagas/EvaluationsSaga.ts +++ b/app/client/src/sagas/EvaluationsSaga.ts @@ -68,7 +68,6 @@ import { TriggerKind, } from "constants/AppsmithActionConstants/ActionConstants"; import { validate } from "workers/Evaluation/validations"; -import { diff } from "deep-diff"; import { REPLAY_DELAY } from "entities/Replay/replayUtils"; import type { EvaluationVersion } from "@appsmith/api/ApplicationApi"; @@ -103,7 +102,6 @@ import { waitForWidgetConfigBuild } from "./InitSagas"; import { logDynamicTriggerExecution } from "@appsmith/sagas/analyticsSaga"; const APPSMITH_CONFIGS = getAppsmithConfigs(); - export const evalWorker = new GracefulWorkerService( new Worker( new URL("../workers/Evaluation/evaluation.worker.ts", import.meta.url), @@ -132,7 +130,6 @@ export function* updateDataTreeHandler( const { configTree, - dataTree, dependencies, errors, evalMetaUpdates = [], @@ -147,6 +144,7 @@ export function* updateDataTreeHandler( undefinedEvalValuesMap, unEvalUpdates, jsVarsCreatedEvent, + updates, } = evalTreeResponse; const appMode: ReturnType = yield select(getAppMode); @@ -157,14 +155,13 @@ export function* updateDataTreeHandler( PerformanceTracker.startAsyncTracking( PerformanceTransactionName.SET_EVALUATED_TREE, ); - const oldDataTree: ReturnType = yield select(getDataTree); - - const updates = diff(oldDataTree, dataTree) || []; if (!isEmpty(staleMetaIds)) { yield put(resetWidgetsMetaState(staleMetaIds)); } + yield put(setEvaluatedTree(updates)); + ConfigTreeActions.setConfigTree(configTree); PerformanceTracker.stopAsyncTracking( diff --git a/app/client/src/selectors/propertyPaneSelectors.tsx b/app/client/src/selectors/propertyPaneSelectors.tsx index 42700721a3..e52ba99ae5 100644 --- a/app/client/src/selectors/propertyPaneSelectors.tsx +++ b/app/client/src/selectors/propertyPaneSelectors.tsx @@ -72,22 +72,28 @@ const getCurrentWidgetName = createSelector( export const getWidgetPropsForPropertyPane = createSelector( getCurrentWidgetProperties, getCurrentAppPositioningType, - getDataTree, + (state) => { + const currentWidget = getCurrentWidgetProperties(state); + if (!currentWidget) return; + const evaluatedWidget = find(getDataTree(state), { + widgetId: currentWidget.widgetId, + }) as WidgetEntity; + if (!evaluatedWidget) return; + return evaluatedWidget[EVALUATION_PATH]; + }, ( widget: WidgetProps | undefined, appPositioningType, - evaluatedTree: DataTree, + evaluatedValue: any, ): WidgetProps | undefined => { if (!widget) return undefined; - const evaluatedWidget = find(evaluatedTree, { - widgetId: widget.widgetId, - }) as WidgetEntity; + const widgetProperties = { ...widget, appPositioningType, }; - if (evaluatedWidget) { - widgetProperties[EVALUATION_PATH] = evaluatedWidget[EVALUATION_PATH]; + if (evaluatedValue) { + widgetProperties[EVALUATION_PATH] = evaluatedValue; } return widgetProperties; }, diff --git a/app/client/src/widgets/withWidgetProps.tsx b/app/client/src/widgets/withWidgetProps.tsx index ffa13ce115..4db38d71c4 100644 --- a/app/client/src/widgets/withWidgetProps.tsx +++ b/app/client/src/widgets/withWidgetProps.tsx @@ -63,6 +63,7 @@ function withWidgetProps(WrappedWidget: typeof BaseWidget) { type, widgetId, } = props; + const isPreviewMode = useSelector(previewModeSelector); const canvasWidget = useSelector((state: AppState) => getWidget(state, widgetId), @@ -82,6 +83,7 @@ function withWidgetProps(WrappedWidget: typeof BaseWidget) { const evaluatedWidget = useSelector((state: AppState) => getWidgetEvalValues(state, widgetName), ); + const isLoading = useSelector((state: AppState) => getIsWidgetLoading(state, widgetName), ); @@ -316,7 +318,6 @@ function withWidgetProps(WrappedWidget: typeof BaseWidget) { : undefined, }; } - return ; } diff --git a/app/client/src/workers/Evaluation/__tests__/generateOpimisedUpdates.test.ts b/app/client/src/workers/Evaluation/__tests__/generateOpimisedUpdates.test.ts new file mode 100644 index 0000000000..4951f3dab0 --- /dev/null +++ b/app/client/src/workers/Evaluation/__tests__/generateOpimisedUpdates.test.ts @@ -0,0 +1,248 @@ +import produce from "immer"; +import { range } from "lodash"; +import { generateOptimisedUpdates } from "../helpers"; + +export const smallDataSet = [ + { + address: "Paseo de Soledad Tur 245 Puerta 4 \nValencia, 50285", + company: "Gonzalez Inc", + }, + { + address: "Ronda Rosalina Menéndez 72\nCuenca, 38057", + company: "Gallego, Pedrosa and Conesa", + }, + { + address: "833 جزيني Gateway\nمشفقland, AL 63852", + company: "اهرام-الحويطات", + }, +]; +//size of about 300 elements +const largeDataSet = range(100).flatMap(() => smallDataSet) as any; + +const oldState = { + Table1: { + ENTITY_TYPE: "WIDGET", + primaryColumns: { + customColumn2: { + isDisabled: false, + }, + }, + tableData: [], + filteredTableData: smallDataSet, + selectedRows: [], + pageSize: 0, + triggerRowSelection: false, + type: "TABLE_WIDGET_V2", + __evaluation__: { + errors: { + transientTableData: [], + tableData: [], + processedTableData: [], + }, + evaluatedValues: { + filteredTableData: smallDataSet, + transientTableData: {}, + tableData: [], + processedTableData: [], + "primaryColumns.customColumn2.isDisabled": false, + }, + }, + }, + Select1: { + value: "", + ENTITY_TYPE: "WIDGET", + options: [ + { + label: "courtney68@example.net", + value: 1, + }, + { + label: "osamusato@example.com", + value: 2, + }, + ], + type: "SELECT_WIDGET", + __evaluation__: { + errors: { + options: [], + }, + evaluatedValues: { + "meta.value": "", + value: "", + }, + }, + }, +}; + +describe("optimised diff updates", () => { + describe("regular diff", () => { + test("should generate regular diff updates when a simple property changes in the widget property segment", () => { + const newState = produce(oldState, (draft) => { + draft.Table1.pageSize = 17; + }); + const updates = generateOptimisedUpdates(oldState, newState); + expect(updates).toEqual([ + { kind: "E", path: ["Table1", "pageSize"], lhs: 0, rhs: 17 }, + ]); + }); + test("should generate regular diff updates when a simple property changes in the __evaluation__ segment ", () => { + const validationError = "Some validation error"; + const newState = produce(oldState, (draft) => { + draft.Table1.__evaluation__.evaluatedValues.tableData = + validationError as any; + }); + const updates = generateOptimisedUpdates(oldState, newState); + expect(updates).toEqual([ + { + kind: "E", + path: ["Table1", "__evaluation__", "evaluatedValues", "tableData"], + lhs: [], + rhs: validationError, + }, + ]); + }); + }); + + describe("diffs with identicalEvalPathsPatches", () => { + test("should not generate any updates when both the states are the same", () => { + const updates = generateOptimisedUpdates(oldState, oldState); + expect(updates).toEqual([]); + const identicalEvalPathsPatches = { + "Table1.__evaluation__.evaluatedValues.['tableData']": + "Table1.tableData", + }; + const updatesWithCompressionMap = generateOptimisedUpdates( + oldState, + oldState, + identicalEvalPathsPatches, + ); + expect(updatesWithCompressionMap).toEqual([]); + }); + test("should generate the correct table data updates and reference state patches", () => { + const identicalEvalPathsPatches = { + "Table1.__evaluation__.evaluatedValues.['tableData']": + "Table1.tableData", + }; + const newState = produce(oldState, (draft) => { + draft.Table1.tableData = largeDataSet; + }); + const updates = generateOptimisedUpdates( + oldState, + newState, + identicalEvalPathsPatches, + ); + expect(updates).toEqual([ + { kind: "N", path: ["Table1", "tableData"], rhs: largeDataSet }, + { + kind: "referenceState", + path: ["Table1", "__evaluation__", "evaluatedValues", "tableData"], + referencePath: "Table1.tableData", + }, + ]); + }); + test("should not generate granular updates and generate a patch which replaces the complete collection when any change is made to the large collection ", () => { + const largeDataSetWithSomeSimpleChange = produce( + largeDataSet, + (draft: any) => { + //making a change to the first row of the collection + draft[0].address = "some new address"; + //making a change to the second row of the collection + draft[1].address = "some other new address"; + }, + ) as any; + const identicalEvalPathsPatches = { + "Table1.__evaluation__.evaluatedValues.['tableData']": + "Table1.tableData", + }; + const evalVal = "some eval value" as any; + + const newState = produce(oldState, (draft) => { + //this value eval value should be ignores since we have provided identicalEvalPathsPatches which takes precedence + draft.Table1.__evaluation__.evaluatedValues.tableData = evalVal as any; + draft.Table1.tableData = largeDataSetWithSomeSimpleChange; + }); + + const updates = generateOptimisedUpdates( + oldState, + newState, + identicalEvalPathsPatches, + ); + //should not see evalVal since identical eval path patches takes precedence + expect(JSON.stringify(updates)).not.toContain(evalVal); + expect(updates).toEqual([ + { + kind: "N", + path: ["Table1", "tableData"], + //we should not see granular updates but complete replacement + rhs: largeDataSetWithSomeSimpleChange, + }, + { + //compression patch + kind: "referenceState", + path: ["Table1", "__evaluation__", "evaluatedValues", "tableData"], + referencePath: "Table1.tableData", + }, + ]); + }); + test("should not generate compression patches when there are no identical eval paths provided ", () => { + const tableDataEvaluationValue = "someValidation error"; + const largeDataSetWithSomeSimpleChange = produce( + largeDataSet, + (draft: any) => { + //making a change to the first row of the collection + draft[0].address = "some new address"; + //making a change to the second row of the collection + draft[1].address = "some other new address"; + }, + ) as any; + // empty indentical eval paths + const identicalEvalPathsPatches = {}; + const newState = produce(oldState, (draft) => { + draft.Table1.tableData = largeDataSetWithSomeSimpleChange; + draft.Table1.__evaluation__.evaluatedValues.tableData = + tableDataEvaluationValue as any; + }); + const updates = generateOptimisedUpdates( + oldState, + newState, + identicalEvalPathsPatches, + ); + expect(updates).toEqual([ + { + //considered as regular diff since there are no identical eval paths provided + kind: "E", + path: ["Table1", "__evaluation__", "evaluatedValues", "tableData"], + lhs: [], + rhs: tableDataEvaluationValue, + }, + { + kind: "N", + path: ["Table1", "tableData"], + //we should not see granular updates but complete replacement + rhs: largeDataSetWithSomeSimpleChange, + }, + ]); + }); + test("should not generate any update when the new state has the same value as the old state", () => { + const oldStateSetWithSomeData = produce(oldState, (draft) => { + draft.Table1.tableData = largeDataSet; + draft.Table1.__evaluation__.evaluatedValues.tableData = largeDataSet; + }); + const identicalEvalPathsPatches = { + "Table1.__evaluation__.evaluatedValues.['tableData']": + "Table1.tableData", + }; + const newStateSetWithTheSameData = produce(oldState, (draft) => { + //deliberating making a new instance of largeDataSet + draft.Table1.tableData = [...largeDataSet] as any; + }); + //since the old state has the same value as the new value..we wont generate a patch unnecessarily... + const updates = generateOptimisedUpdates( + oldStateSetWithSomeData, + newStateSetWithTheSameData, + identicalEvalPathsPatches, + ); + expect(updates).toEqual([]); + }); + }); +}); diff --git a/app/client/src/workers/Evaluation/evalTreeWithChanges.ts b/app/client/src/workers/Evaluation/evalTreeWithChanges.ts index ed3305bb3a..cf3e8338ba 100644 --- a/app/client/src/workers/Evaluation/evalTreeWithChanges.ts +++ b/app/client/src/workers/Evaluation/evalTreeWithChanges.ts @@ -14,6 +14,7 @@ import { MAIN_THREAD_ACTION } from "@appsmith/workers/Evaluation/evalWorkerActio import type { UpdateDataTreeMessageData } from "sagas/EvalWorkerActionSagas"; import type { JSUpdate } from "utils/JSPaneUtils"; import { setEvalContext } from "./evaluate"; +import { generateOptimisedUpdatesAndSetPrevState } from "./helpers"; export function evalTreeWithChanges( updatedValuePaths: string[][], @@ -63,6 +64,8 @@ export function evalTreeWithChanges( dataTree = makeEntityConfigsAsObjProperties(dataTreeEvaluator.evalTree, { evalProps: dataTreeEvaluator.evalProps, + identicalEvalPathsPatches: + dataTreeEvaluator.getEvalPathsIdenticalToState(), }); /** Make sure evalMetaUpdates is sanitized to prevent postMessage failure */ @@ -75,8 +78,12 @@ export function evalTreeWithChanges( configTree = dataTreeEvaluator.oldConfigTree; } - const evalTreeResponse: EvalTreeResponseData = { + const updates = generateOptimisedUpdatesAndSetPrevState( dataTree, + dataTreeEvaluator, + ); + const evalTreeResponse: EvalTreeResponseData = { + updates, dependencies, errors, evalMetaUpdates, diff --git a/app/client/src/workers/Evaluation/handlers/evalTree.ts b/app/client/src/workers/Evaluation/handlers/evalTree.ts index 8192d0d632..c171fae218 100644 --- a/app/client/src/workers/Evaluation/handlers/evalTree.ts +++ b/app/client/src/workers/Evaluation/handlers/evalTree.ts @@ -23,6 +23,7 @@ import JSObjectCollection from "workers/Evaluation/JSObject/Collection"; import { setEvalContext } from "../evaluate"; import { getJSVariableCreatedEvents } from "../JSObject/JSVariableEvents"; import { errorModifier } from "../errorModifier"; +import { generateOptimisedUpdatesAndSetPrevState } from "../helpers"; export let replayMap: Record> | undefined; export let dataTreeEvaluator: DataTreeEvaluator | undefined; @@ -82,6 +83,8 @@ export default function (request: EvalWorkerSyncRequest) { const dataTreeResponse = dataTreeEvaluator.evalAndValidateFirstTree(); dataTree = makeEntityConfigsAsObjProperties(dataTreeResponse.evalTree, { evalProps: dataTreeEvaluator.evalProps, + identicalEvalPathsPatches: + dataTreeEvaluator?.getEvalPathsIdenticalToState(), }); staleMetaIds = dataTreeResponse.staleMetaIds; } else if (dataTreeEvaluator.hasCyclicalDependency || forceEvaluation) { @@ -122,6 +125,8 @@ export default function (request: EvalWorkerSyncRequest) { dataTree = makeEntityConfigsAsObjProperties(dataTreeResponse.evalTree, { evalProps: dataTreeEvaluator.evalProps, + identicalEvalPathsPatches: + dataTreeEvaluator?.getEvalPathsIdenticalToState(), }); staleMetaIds = dataTreeResponse.staleMetaIds; } else { @@ -167,6 +172,8 @@ export default function (request: EvalWorkerSyncRequest) { dataTree = makeEntityConfigsAsObjProperties(dataTreeEvaluator.evalTree, { evalProps: dataTreeEvaluator.evalProps, + identicalEvalPathsPatches: + dataTreeEvaluator?.getEvalPathsIdenticalToState(), }); evalMetaUpdates = JSON.parse( @@ -201,6 +208,8 @@ export default function (request: EvalWorkerSyncRequest) { makeEntityConfigsAsObjProperties(unevalTree, { sanitizeDataTree: false, evalProps: dataTreeEvaluator?.evalProps, + identicalEvalPathsPatches: + dataTreeEvaluator?.getEvalPathsIdenticalToState(), }), widgetTypeConfigMap, configTree, @@ -210,8 +219,13 @@ export default function (request: EvalWorkerSyncRequest) { const jsVarsCreatedEvent = getJSVariableCreatedEvents(jsUpdates); - const evalTreeResponse: EvalTreeResponseData = { + const updates = generateOptimisedUpdatesAndSetPrevState( dataTree, + dataTreeEvaluator, + ); + + const evalTreeResponse: EvalTreeResponseData = { + updates, dependencies, errors, evalMetaUpdates, diff --git a/app/client/src/workers/Evaluation/helpers.ts b/app/client/src/workers/Evaluation/helpers.ts index e88e84a7e8..52dcb2cb03 100644 --- a/app/client/src/workers/Evaluation/helpers.ts +++ b/app/client/src/workers/Evaluation/helpers.ts @@ -1,3 +1,18 @@ +import type { Diff } from "deep-diff"; +import { diff } from "deep-diff"; +import type { DataTree } from "entities/DataTree/dataTreeFactory"; +import equal from "fast-deep-equal"; +import produce from "immer"; +import { get, isNumber, isObject, set } from "lodash"; + +export interface DiffReferenceState { + kind: "referenceState"; + path: any[]; + referencePath: string; +} +export type DiffWithReferenceState = + | Diff + | DiffReferenceState; // Finds the first index which is a duplicate value // Returns -1 if there are no duplicates // Returns the index of the first duplicate entry it finds @@ -51,3 +66,210 @@ export const countOccurrences = ( } return n; }; + +const LARGE_COLLECTION_SIZE = 100; +// for object paths which have a "." in the object key like "a.['b.c']" +const REGEX_NESTED_OBJECT_PATH = /(.+)\.\[\'(.*)\'\]/; + +const generateWithKey = (basePath: any, key: any) => { + const segmentedPath = [...basePath, key]; + + if (isNumber(key)) { + return { + path: basePath.join(".") + ".[" + key + "]", + segmentedPath, + }; + } + if (key.includes(".")) { + return { + path: basePath.join(".") + ".['" + key + "']", + segmentedPath, + }; + } + return { + path: basePath.join(".") + "." + key, + segmentedPath, + }; +}; + +const isLargeCollection = (val: any) => { + if (!Array.isArray(val)) return false; + const rowSize = !isObject(val[0]) ? 1 : Object.keys(val[0]).length; + + const size = val.length * rowSize; + + return size > LARGE_COLLECTION_SIZE; +}; + +const normaliseEvalPath = (identicalEvalPathsPatches: any) => + Object.keys(identicalEvalPathsPatches || {}).reduce( + (acc: any, evalPath: string) => { + //for object paths which have a "." in the object key like "a.['b.c']", we need to extract these + // paths and break them to appropriate patch paths + + const matches = evalPath.match(REGEX_NESTED_OBJECT_PATH); + if (!matches || !matches.length) { + //regular paths like "a.b.c" + acc[evalPath] = identicalEvalPathsPatches[evalPath]; + return acc; + } + + const [, firstSeg, nestedPathSeg] = matches; + // normalise non nested paths like "a.['b']" + if (!nestedPathSeg.includes(".")) { + const key = [firstSeg, nestedPathSeg].join("."); + acc[key] = identicalEvalPathsPatches[evalPath]; + return acc; + } + // object paths which have a "." like "a.['b.c']" + const key = [firstSeg, `['${nestedPathSeg}']`].join("."); + acc[key] = identicalEvalPathsPatches[evalPath]; + return acc; + }, + {}, + ); +//completely new updates which the diff will not traverse through needs to be attached +const generateMissingSetPathsUpdates = ( + ignoreLargeKeys: any, + ignoreLargeKeysHasBeenAttached: any, + dataTree: any, +): DiffWithReferenceState[] => + Object.keys(ignoreLargeKeys) + .filter((evalPath) => !ignoreLargeKeysHasBeenAttached.has(evalPath)) + .map((evalPath) => { + const statePath = ignoreLargeKeys[evalPath]; + //for object paths which have a "." in the object key like "a.['b.c']", we need to extract these + // paths and break them to appropriate patch paths + + //get the matching value from the widget properies in the data tree + const val = get(dataTree, statePath); + + const matches = evalPath.match(REGEX_NESTED_OBJECT_PATH); + if (!matches || !matches.length) { + //regular paths like "a.b.c" + + return { + kind: "N", + path: evalPath.split("."), + rhs: val, + }; + } + // object paths which have a "." like "a.['b.c']" + const [, firstSeg, nestedPathSeg] = matches; + const segmentedPath = [...firstSeg.split("."), nestedPathSeg]; + + return { + kind: "N", + path: segmentedPath, + rhs: val, + }; + }); + +const generateDiffUpdates = ( + oldDataTree: any, + dataTree: any, + ignoreLargeKeys: any, +): DiffWithReferenceState[] => { + const attachDirectly: DiffWithReferenceState[] = []; + const ignoreLargeKeysHasBeenAttached = new Set(); + const attachLater: DiffWithReferenceState[] = []; + const updates = + diff(oldDataTree, dataTree, (path, key) => { + if (!path.length || key === "__evaluation__") return false; + + const { path: setPath, segmentedPath } = generateWithKey(path, key); + + // if ignore path is present...this segment of code generates the data compression patches + if (!!ignoreLargeKeys[setPath]) { + const originalStateVal = get(oldDataTree, segmentedPath); + const correspondingStatePath = ignoreLargeKeys[setPath]; + const statePathValue = get(dataTree, correspondingStatePath); + if (!equal(originalStateVal, statePathValue)) { + //reference state patches are a patch that does not have a patch value but it provides a path which contains the same value + //this is helpful in making the payload sent to the main thread small + attachLater.push({ + kind: "referenceState", + path: segmentedPath, + referencePath: correspondingStatePath, + }); + } + ignoreLargeKeysHasBeenAttached.add(setPath); + return true; + } + const rhs = get(dataTree, segmentedPath); + + const lhs = get(oldDataTree, segmentedPath); + + const isLhsLarge = isLargeCollection(lhs); + const isRhsLarge = isLargeCollection(rhs); + if (!isLhsLarge && !isRhsLarge) { + //perform diff on this node + return false; + } + + //if either of values are large just directly attach it don't have to generate very granular updates + + if ((!isLhsLarge && isRhsLarge) || (isLhsLarge && !isRhsLarge)) { + attachDirectly.push({ kind: "N", path: segmentedPath, rhs }); + return true; + } + + //if the values are different attach the update directly + !equal(lhs, rhs) && + attachDirectly.push({ kind: "N", path: segmentedPath, rhs }); + + //ignore diff on this node + return true; + }) || []; + + const missingSetPaths = generateMissingSetPathsUpdates( + ignoreLargeKeys, + ignoreLargeKeysHasBeenAttached, + dataTree, + ); + + const largeDataSetUpdates = [ + ...attachDirectly, + ...missingSetPaths, + ...attachLater, + ]; + return [...updates, ...largeDataSetUpdates]; +}; + +export const generateOptimisedUpdates = ( + oldDataTree: any, + dataTree: any, + identicalEvalPathsPatches?: Record, +): DiffWithReferenceState[] => { + const ignoreLargeKeys = normaliseEvalPath(identicalEvalPathsPatches); + const updates = generateDiffUpdates(oldDataTree, dataTree, ignoreLargeKeys); + return updates; +}; + +export const decompressIdenticalEvalPaths = ( + dataTree: any, + identicalEvalPathsPatches: Record, +) => + produce(dataTree, (draft: any) => + Object.entries(identicalEvalPathsPatches || {}).forEach(([key, value]) => { + const referencePathValue = get(dataTree, value); + set(draft, key, referencePathValue); + }), + ); + +export const generateOptimisedUpdatesAndSetPrevState = ( + dataTree: any, + dataTreeEvaluator: any, +) => { + const identicalEvalPathsPatches = + dataTreeEvaluator?.getEvalPathsIdenticalToState(); + + const updates = generateOptimisedUpdates( + dataTreeEvaluator?.getPrevState(), + dataTree, + identicalEvalPathsPatches, + ); + + dataTreeEvaluator?.setPrevState(dataTree); + return updates; +}; diff --git a/app/client/src/workers/Evaluation/types.ts b/app/client/src/workers/Evaluation/types.ts index 50e7da1f04..b24fb36bfe 100644 --- a/app/client/src/workers/Evaluation/types.ts +++ b/app/client/src/workers/Evaluation/types.ts @@ -1,6 +1,5 @@ import type { ConfigTree, - DataTree, unEvalAndConfigTree, } from "entities/DataTree/dataTreeFactory"; import type { ActionValidationConfigMap } from "constants/PropertyControlConstants"; @@ -19,6 +18,7 @@ import type { EvalMetaUpdates } from "@appsmith/workers/common/DataTreeEvaluator import type { WorkerRequest } from "@appsmith/workers/common/types"; import type { DataTreeDiff } from "@appsmith/workers/Evaluation/evaluationUtils"; import type { APP_MODE } from "entities/App"; +import type { DiffWithReferenceState } from "./helpers"; export type EvalWorkerSyncRequest = WorkerRequest; export type EvalWorkerASyncRequest = WorkerRequest< @@ -42,7 +42,6 @@ export interface EvalTreeRequestData { } export interface EvalTreeResponseData { - dataTree: DataTree; dependencies: DependencyMap; errors: EvalError[]; evalMetaUpdates: EvalMetaUpdates; @@ -58,6 +57,7 @@ export interface EvalTreeResponseData { isNewWidgetAdded: boolean; undefinedEvalValuesMap: Record; jsVarsCreatedEvent?: { path: string; type: string }[]; + updates: DiffWithReferenceState[]; } export type JSVarMutatedEvents = Record; diff --git a/app/client/src/workers/common/DataTreeEvaluator/index.ts b/app/client/src/workers/common/DataTreeEvaluator/index.ts index ba827109c3..9b995cd64b 100644 --- a/app/client/src/workers/common/DataTreeEvaluator/index.ts +++ b/app/client/src/workers/common/DataTreeEvaluator/index.ts @@ -113,6 +113,7 @@ import { import { isJSObjectFunction } from "workers/Evaluation/JSObject/utils"; import { getValidatedTree, + setToEvalPathsIdenticalToState, validateActionProperty, validateAndParseWidgetProperty, } from "./validationUtils"; @@ -164,8 +165,18 @@ export default class DataTreeEvaluator { * Sanitized eval values and errors */ evalProps: EvalProps = {}; + //when attaching values to __evaluations__ segment of the state there are cases where this value is identical to the widget property + //in those cases do not it to the dataTree and update this map. The main thread can decompress these updates and we can minimise the data transfer + evalPathsIdenticalToState: any = {}; undefinedEvalValuesMap: Record = {}; + prevState = {}; + setPrevState(state: any) { + this.prevState = state; + } + getPrevState() { + return this.prevState; + } public hasCyclicalDependency = false; constructor( widgetConfigMap: WidgetTypeConfigMap, @@ -177,6 +188,9 @@ export default class DataTreeEvaluator { this.widgetConfigMap = widgetConfigMap; } + getEvalPathsIdenticalToState(): Record { + return this.evalPathsIdenticalToState || {}; + } getEvalTree() { return this.evalTree; } @@ -344,6 +358,7 @@ export default class DataTreeEvaluator { evaluatedTree, { evalProps: this.evalProps, + evalPathsIdenticalToState: this.evalPathsIdenticalToState, }, this.oldConfigTree, ), @@ -1028,6 +1043,7 @@ export default class DataTreeEvaluator { evalPropertyValue, unEvalPropertyValue, evalProps: this.evalProps, + evalPathsIdenticalToState: this.evalPathsIdenticalToState, }); this.setParsedValue({ @@ -1078,13 +1094,19 @@ export default class DataTreeEvaluator { } } - set( - this.evalProps, - getEvalValuePath(fullPropertyPath), - evalPropertyValue, - ); - set(currentTree, fullPropertyPath, evalPropertyValue); + if (!propertyPath) return currentTree; + const evalPath = getEvalValuePath(fullPropertyPath); + setToEvalPathsIdenticalToState({ + evalPath, + evalPathsIdenticalToState: this.evalPathsIdenticalToState, + evalProps: this.evalProps, + isParsedValueTheSame: true, + statePath: fullPropertyPath, + value: evalPropertyValue, + }); + + set(currentTree, fullPropertyPath, evalPropertyValue); return currentTree; } case ENTITY_TYPE.JSACTION: { @@ -1113,14 +1135,22 @@ export default class DataTreeEvaluator { !hasUnEvalValueModified && prevEvaluatedValue ? prevEvaluatedValue : evalPropertyValue; - set( - this.evalProps, - getEvalValuePath(fullPropertyPath, { - isPopulated: true, - fullPath: true, - }), - evalValue, - ); + + const evalPath = getEvalValuePath(fullPropertyPath, { + isPopulated: true, + //what is the purpose of this argument + fullPath: true, + }); + + setToEvalPathsIdenticalToState({ + evalPath, + evalPathsIdenticalToState: this.evalPathsIdenticalToState, + evalProps: this.evalProps, + isParsedValueTheSame: true, + statePath: fullPropertyPath, + value: evalValue, + }); + set(currentTree, fullPropertyPath, evalValue); JSObjectCollection.setVariableValue( evalValue, @@ -1461,6 +1491,7 @@ export default class DataTreeEvaluator { fullPath, ) as unknown as string, evalProps: this.evalProps, + evalPathsIdenticalToState: this.evalPathsIdenticalToState, }); }); } diff --git a/app/client/src/workers/common/DataTreeEvaluator/validationUtils.ts b/app/client/src/workers/common/DataTreeEvaluator/validationUtils.ts index 9ca8429ff3..4dc2aa1df8 100644 --- a/app/client/src/workers/common/DataTreeEvaluator/validationUtils.ts +++ b/app/client/src/workers/common/DataTreeEvaluator/validationUtils.ts @@ -6,7 +6,7 @@ import type { WidgetEntity, WidgetEntityConfig, } from "entities/DataTree/dataTreeFactory"; -import { get, isUndefined, set } from "lodash"; +import { get, isObject, isUndefined, set } from "lodash"; import type { EvaluationError } from "utils/DynamicBindingUtils"; import { getEvalValuePath, @@ -21,10 +21,40 @@ import { } from "@appsmith/workers/Evaluation/evaluationUtils"; import { validate } from "workers/Evaluation/validations"; import type { EvalProps } from "."; +import type { ValidationResponse } from "constants/WidgetValidation"; +const LARGE_COLLECTION_SIZE = 100; + +const getIsLargeCollection = (val: any) => { + if (!Array.isArray(val)) return false; + const rowSize = !isObject(val[0]) ? 1 : Object.keys(val[0]).length; + + const size = val.length * rowSize; + + return size > LARGE_COLLECTION_SIZE; +}; +export function setToEvalPathsIdenticalToState({ + evalPath, + evalPathsIdenticalToState, + evalProps, + isParsedValueTheSame, + statePath, + value, +}: any) { + const isLargeCollection = getIsLargeCollection(value); + + if (isParsedValueTheSame && isLargeCollection) { + evalPathsIdenticalToState[evalPath] = statePath; + } else { + delete evalPathsIdenticalToState[evalPath]; + + set(evalProps, evalPath, value); + } +} export function validateAndParseWidgetProperty({ configTree, currentTree, + evalPathsIdenticalToState, evalPropertyValue, evalProps, fullPropertyPath, @@ -38,6 +68,7 @@ export function validateAndParseWidgetProperty({ evalPropertyValue: unknown; unEvalPropertyValue: string; evalProps: EvalProps; + evalPathsIdenticalToState: any; }): unknown { const { propertyPath } = getEntityNameAndPropertyPath(fullPropertyPath); if (isPathDynamicTrigger(widget, propertyPath)) { @@ -85,14 +116,21 @@ export function validateAndParseWidgetProperty({ configTree, }); } - set( + + const evalPath = getEvalValuePath(fullPropertyPath, { + isPopulated: false, + fullPath: true, + }); + const isParsedValueTheSame = parsed === evaluatedValue; + + setToEvalPathsIdenticalToState({ + evalPath, + evalPathsIdenticalToState, evalProps, - getEvalValuePath(fullPropertyPath, { - isPopulated: false, - fullPath: true, - }), - evaluatedValue, - ); + isParsedValueTheSame, + statePath: fullPropertyPath, + value: evaluatedValue, + }); return parsed; } @@ -115,7 +153,7 @@ export function validateWidgetProperty( export function validateActionProperty( config: ValidationConfig, value: unknown, -) { +): ValidationResponse { if (!config) { return { isValid: true, @@ -127,10 +165,10 @@ export function validateActionProperty( export function getValidatedTree( tree: DataTree, - option: { evalProps: EvalProps }, + option: { evalProps: EvalProps; evalPathsIdenticalToState: any }, configTree: ConfigTree, ) { - const { evalProps } = option; + const { evalPathsIdenticalToState, evalProps } = option; return Object.keys(tree).reduce((tree, entityKey: string) => { const entity = tree[entityKey]; if (!isWidget(entity)) { @@ -152,15 +190,21 @@ export function getValidatedTree( ? value : transformed; + const isParsedValueTheSame = parsed === evaluatedValue; const fullPropertyPath = `${entityKey}.${property}`; - set( + const evalPath = getEvalValuePath(fullPropertyPath, { + isPopulated: false, + fullPath: true, + }); + + setToEvalPathsIdenticalToState({ + evalPath, + evalPathsIdenticalToState, evalProps, - getEvalValuePath(fullPropertyPath, { - isPopulated: false, - fullPath: true, - }), - evaluatedValue, - ); + isParsedValueTheSame, + statePath: fullPropertyPath, + value: evaluatedValue, + }); resetValidationErrorsForEntityProperty({ evalProps, diff --git a/app/client/src/workers/common/JSLibrary/resetJSLibraries.ts b/app/client/src/workers/common/JSLibrary/resetJSLibraries.ts index b25dc300b2..49e45ca18c 100644 --- a/app/client/src/workers/common/JSLibrary/resetJSLibraries.ts +++ b/app/client/src/workers/common/JSLibrary/resetJSLibraries.ts @@ -5,7 +5,6 @@ import forge from "node-forge"; import { defaultLibraries } from "./index"; import { JSLibraries, libraryReservedIdentifiers } from "./index"; import { invalidEntityIdentifiers } from "../DependencyMap/utils"; - const defaultLibImplementations = { lodash: _, moment: moment,