From e5b2a26c6585a9fa6882b1853a3a622c1c230dfc Mon Sep 17 00:00:00 2001 From: Vemparala Surya Vamsi <121419957+vsvamsi1@users.noreply.github.com> Date: Fri, 11 Jul 2025 12:24:44 +0530 Subject: [PATCH] chore: ce changes related to decoupling webworker (#41033) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Description We are improving the LCP by reducing the time to reach the first evaluation, aiming for a 1.8 to 2.2 second reduction. To achieve this, we’ve implemented the following changes: Code Splitting of Widgets: During page load, only the widgets required for an evaluation are loaded and registered. For every evaluation cycle we keep discovering widget types and load them as required. Web Worker Offloading: Macro tasks such as clearCache and JavaScript library installation have been moved to the web worker setup. These are now executed in a separate thread, allowing the firstUnevaluatedTree to be computed in parallel with JS library installation. Parallel JS Library Loading: All JavaScript libraries are now loaded in parallel within the web worker, instead of sequentially, improving efficiency. Deferred Rendering of AppViewer: We now render the AppViewer and Header component only after registering the remaining widgets. This ensures that heavy rendering tasks—such as expensive selector computations and loading additional chunks related to the AppViewer—can execute in parallel with the first evaluation, further enhancing performance. ## Automation /ok-to-test tags="@tag.All" ### :mag: Cypress test results > [!TIP] > 🟢 🟢 🟢 All cypress tests have passed! 🎉 🎉 🎉 > Workflow run: > Commit: b648036bd7b74ae742f5c5d7f6cfd770867a2828 > Cypress dashboard. > Tags: `@tag.All` > Spec: >
Thu, 10 Jul 2025 19:22:25 UTC ## Communication Should the DevRel and Marketing teams inform users about this change? - [ ] Yes - [ ] No ## Summary by CodeRabbit ## Summary by CodeRabbit * **New Features** * Widgets are now loaded and registered asynchronously, improving app startup and modularity. * Widget registration and configuration changes are now versioned, ensuring selectors and UI update appropriately. * Widget initialization and factory cache management are more robust, with explicit cache clearing after widget registration. * Added new Redux actions and selectors to manage first page load, deferred JS library loading, and page rendering state. * Theme handling and widget initialization in AppViewer are streamlined for faster evaluation and rendering. * Deferred loading of JavaScript libraries on first page load improves performance. * Conditional rendering gates added to AppViewer and Navigation components based on evaluation state. * **Bug Fixes** * Prevented errors when conditionally rendering widgets and navigation components before evaluation is complete. * Improved widget property pane and configuration tests to ensure all widgets are properly loaded and validated. * **Refactor** * Widget import and registration logic was refactored to support dynamic, on-demand loading. * Evaluation and initialization sagas were modularized for better maintainability and performance. * Widget factory and memoization logic were enhanced to allow explicit cache clearing and version tracking. * JavaScript library loading logic was parallelized for faster startup. * Theme application extracted into a dedicated component for clarity and reuse. * **Tests** * Expanded and updated widget and evaluation saga test suites to cover asynchronous widget loading, cache management, and first evaluation scenarios. * Added tests verifying widget factory cache behavior and first evaluation integration. * **Chores** * Updated internal dependencies and selectors to track widget configuration version changes, ensuring UI consistency. --- .../factory/__tests__/WidgetFactory.test.ts | 39 ++ .../src/WidgetProvider/factory/decorators.ts | 45 +- .../factory/registrationHelper.tsx | 48 +- .../factory/widgetConfigVersion.ts | 10 + app/client/src/actions/JSLibraryActions.ts | 10 + app/client/src/actions/evaluationActions.ts | 7 + .../src/ce/constants/ReduxActionConstants.tsx | 9 + app/client/src/ce/reducers/index.tsx | 2 + app/client/src/ce/sagas/PageSagas.tsx | 11 +- .../ce/selectors/moduleInstanceSelectors.ts | 3 + .../form/ToggleComponentToJson.tsx | 2 +- .../src/components/propertyControls/index.ts | 2 +- .../src/entities/Engine/AppViewerEngine.ts | 23 +- .../hooks/useAnvilDnDListenerStates.ts | 4 +- .../canvas/FixedLayoutViewerCanvas.tsx | 11 +- .../src/pages/AppViewer/Navigation/index.tsx | 13 +- app/client/src/pages/AppViewer/index.tsx | 41 +- .../src/pages/Templates/TemplateView.tsx | 4 +- .../firstEvaluationReducer.ts | 26 + .../src/reducers/evaluationReducers/index.ts | 2 + app/client/src/sagas/EvalWorkerActionSagas.ts | 8 +- app/client/src/sagas/EvaluationsSaga.test.ts | 223 ++++-- app/client/src/sagas/EvaluationsSaga.ts | 216 +++++- app/client/src/sagas/InitSagas.ts | 8 + app/client/src/selectors/actionSelectors.tsx | 2 + app/client/src/selectors/editorSelectors.tsx | 2 + .../src/selectors/evaluationSelectors.ts | 7 + app/client/src/selectors/widgetSelectors.ts | 18 + app/client/src/utils/WidgetSizeUtils.ts | 1 + app/client/src/utils/WorkerUtil.ts | 10 +- app/client/src/utils/editor/EditorUtils.ts | 14 +- .../src/utils/testPropertyPaneConfig.test.ts | 171 ++--- app/client/src/widgets/index.ts | 654 ++++++++++++------ .../workers/Evaluation/asyncWorkerActions.ts | 4 + .../workers/Evaluation/handlers/jsLibrary.ts | 103 ++- .../dataTreeEvaluator.test.ts | 22 +- 36 files changed, 1262 insertions(+), 513 deletions(-) create mode 100644 app/client/src/WidgetProvider/factory/__tests__/WidgetFactory.test.ts create mode 100644 app/client/src/WidgetProvider/factory/widgetConfigVersion.ts create mode 100644 app/client/src/reducers/evaluationReducers/firstEvaluationReducer.ts create mode 100644 app/client/src/selectors/evaluationSelectors.ts diff --git a/app/client/src/WidgetProvider/factory/__tests__/WidgetFactory.test.ts b/app/client/src/WidgetProvider/factory/__tests__/WidgetFactory.test.ts new file mode 100644 index 0000000000..62bc519f2a --- /dev/null +++ b/app/client/src/WidgetProvider/factory/__tests__/WidgetFactory.test.ts @@ -0,0 +1,39 @@ +import WidgetFactory from "../index"; +import { clearAllWidgetFactoryCache } from "../decorators"; +import type BaseWidget from "widgets/BaseWidget"; + +describe("WidgetFactory Cache Tests", () => { + beforeAll(() => { + // Clear the widget factory state before each test + WidgetFactory.widgetsMap.clear(); + clearAllWidgetFactoryCache(); + }); + + afterAll(() => { + // Clean up after each test + WidgetFactory.widgetsMap.clear(); + clearAllWidgetFactoryCache(); + }); + + it("should return stale data after widget registration until cache is cleared", () => { + // Initial state - no widgets + let widgetTypes = WidgetFactory.getWidgetTypes(); + + expect(widgetTypes).toEqual([]); + + // Add a widget to the map + WidgetFactory.widgetsMap.set("TEST_WIDGET", {} as typeof BaseWidget); + + // getWidgetTypes should still return empty array (stale cache) + widgetTypes = WidgetFactory.getWidgetTypes(); + expect(widgetTypes).toEqual([]); + + // Clear the cache + clearAllWidgetFactoryCache(); + + // Now getWidgetTypes should return the updated widget type + widgetTypes = WidgetFactory.getWidgetTypes(); + expect(widgetTypes).toContain("TEST_WIDGET"); + expect(widgetTypes).toHaveLength(1); + }); +}); diff --git a/app/client/src/WidgetProvider/factory/decorators.ts b/app/client/src/WidgetProvider/factory/decorators.ts index 1dbc737333..2e8ff09946 100644 --- a/app/client/src/WidgetProvider/factory/decorators.ts +++ b/app/client/src/WidgetProvider/factory/decorators.ts @@ -1,13 +1,47 @@ import memo from "micro-memoize"; +type AnyFn = (...args: unknown[]) => unknown; + +interface MemoizedWithClear { + (...args: unknown[]): unknown; + clearCache: () => void; +} + +// Track all memoized functions +const memoizedFunctions = new Set(); + +// Function to clear memoized cache +function clearMemoizedCache(fn: { + cache: { keys: unknown[]; values: unknown[] }; +}) { + fn.cache.keys.length = fn.cache.values.length = 0; +} + +// Create a memoize wrapper that adds cache clearing capability +function memoizeWithClear(fn: AnyFn): MemoizedWithClear { + const memoized = memo(fn, { + maxSize: 100, + }) as unknown as MemoizedWithClear; + + // Add clearCache method to the memoized function + memoized.clearCache = () => { + clearMemoizedCache( + memoized as unknown as { cache: { keys: unknown[]; values: unknown[] } }, + ); + }; + + // Add to tracked functions + memoizedFunctions.add(memoized); + + return memoized; +} + export function memoize( target: unknown, methodName: unknown, descriptor: PropertyDescriptor, ) { - descriptor.value = memo(descriptor.value, { - maxSize: 100, - }); + descriptor.value = memoizeWithClear(descriptor.value); } export function freeze( @@ -25,3 +59,8 @@ export function freeze( return Object.freeze(result); }; } + +// Function to clear all memoized caches +export function clearAllWidgetFactoryCache() { + memoizedFunctions.forEach((fn) => fn.clearCache()); +} diff --git a/app/client/src/WidgetProvider/factory/registrationHelper.tsx b/app/client/src/WidgetProvider/factory/registrationHelper.tsx index 19fec2ef56..41c31ce3bf 100644 --- a/app/client/src/WidgetProvider/factory/registrationHelper.tsx +++ b/app/client/src/WidgetProvider/factory/registrationHelper.tsx @@ -3,6 +3,7 @@ import type { CanvasWidgetStructure } from "WidgetProvider/types"; import type BaseWidget from "widgets/BaseWidget"; import WidgetFactory from "."; import { withBaseWidgetHOC } from "widgets/BaseWidgetHOC/withBaseWidgetHOC"; +import { incrementWidgetConfigsVersion } from "./widgetConfigVersion"; /* * Function to create builder for the widgets and register them in widget factory @@ -11,28 +12,31 @@ import { withBaseWidgetHOC } from "widgets/BaseWidgetHOC/withBaseWidgetHOC"; * extracted this into a seperate file to break the circular reference. * */ + export const registerWidgets = (widgets: (typeof BaseWidget)[]) => { - const widgetAndBuilders = widgets.map((widget) => { - const { eagerRender = false, needsMeta = false } = widget.getConfig(); - - // TODO: Fix this the next time the file is edited - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const ProfiledWidget: any = withBaseWidgetHOC( - widget, - needsMeta, - eagerRender, - ); - - return [ - widget, - (widgetProps: CanvasWidgetStructure) => ( - - ), - ] as [ - typeof BaseWidget, - (widgetProps: CanvasWidgetStructure) => React.ReactNode, - ]; + widgets.forEach((widget) => { + registerWidget(widget); }); - - WidgetFactory.initialize(widgetAndBuilders); + // Increment version to trigger selectors that depend on widget configs + incrementWidgetConfigsVersion(); +}; + +export const registerWidget = (widget: typeof BaseWidget) => { + const { eagerRender = false, needsMeta = false } = widget.getConfig(); + + // TODO: Fix this the next time the file is edited + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const ProfiledWidget: any = withBaseWidgetHOC(widget, needsMeta, eagerRender); + + const widgetAndBuilder: [ + typeof BaseWidget, + (widgetProps: CanvasWidgetStructure) => React.ReactNode, + ] = [ + widget, + (widgetProps: CanvasWidgetStructure) => ( + + ), + ]; + + WidgetFactory.initialize([widgetAndBuilder]); }; diff --git a/app/client/src/WidgetProvider/factory/widgetConfigVersion.ts b/app/client/src/WidgetProvider/factory/widgetConfigVersion.ts new file mode 100644 index 0000000000..733ef01758 --- /dev/null +++ b/app/client/src/WidgetProvider/factory/widgetConfigVersion.ts @@ -0,0 +1,10 @@ +// Global version counter that increments when widgets are registered +let widgetConfigsVersion = 0; + +// Export getter for selectors to depend on +export const getWidgetConfigsVersion = () => widgetConfigsVersion; + +// Export incrementer for registration helper to use +export const incrementWidgetConfigsVersion = () => { + widgetConfigsVersion++; +}; diff --git a/app/client/src/actions/JSLibraryActions.ts b/app/client/src/actions/JSLibraryActions.ts index c96972e54b..fa032a1196 100644 --- a/app/client/src/actions/JSLibraryActions.ts +++ b/app/client/src/actions/JSLibraryActions.ts @@ -12,6 +12,16 @@ export function fetchJSLibraries( }; } +export function deferLoadingJSLibraries( + applicationId: string, + customJSLibraries?: ApiResponse, +) { + return { + type: ReduxActionTypes.DEFER_LOADING_JS_LIBRARIES, + payload: { applicationId, customJSLibraries }, + }; +} + export function installLibraryInit(payload: Partial) { return { type: ReduxActionTypes.INSTALL_LIBRARY_INIT, diff --git a/app/client/src/actions/evaluationActions.ts b/app/client/src/actions/evaluationActions.ts index 4857b05ea4..d2a34b9ff7 100644 --- a/app/client/src/actions/evaluationActions.ts +++ b/app/client/src/actions/evaluationActions.ts @@ -13,6 +13,7 @@ import type { ConditionalOutput, DynamicValues, } from "reducers/evaluationReducers/formEvaluationReducer"; +import type { ReduxActionWithoutPayload } from "./ReduxActionTypes"; export const shouldTriggerEvaluation = (action: ReduxAction) => { return ( @@ -79,6 +80,12 @@ export const setDependencyMap = ( }; }; +export const setIsFirstPageLoad = (): ReduxActionWithoutPayload => { + return { + type: ReduxActionTypes.IS_FIRST_PAGE_LOAD, + }; +}; + // These actions require the entire tree to be re-evaluated const FORCE_EVAL_ACTIONS = { [ReduxActionTypes.INSTALL_LIBRARY_SUCCESS]: true, diff --git a/app/client/src/ce/constants/ReduxActionConstants.tsx b/app/client/src/ce/constants/ReduxActionConstants.tsx index 028a3dc491..36f44caa2a 100644 --- a/app/client/src/ce/constants/ReduxActionConstants.tsx +++ b/app/client/src/ce/constants/ReduxActionConstants.tsx @@ -14,6 +14,7 @@ const JSLibraryActionTypes = { TOGGLE_INSTALLER: "TOGGLE_INSTALLER", FETCH_JS_LIBRARIES_INIT: "FETCH_JS_LIBRARIES_INIT", FETCH_JS_LIBRARIES_SUCCESS: "FETCH_JS_LIBRARIES_SUCCESS", + DEFER_LOADING_JS_LIBRARIES: "DEFER_LOADING_JS_LIBRARIES", CLEAR_PROCESSED_INSTALLS: "CLEAR_PROCESSED_INSTALLS", INSTALL_LIBRARY_INIT: "INSTALL_LIBRARY_INIT", INSTALL_LIBRARY_START: "INSTALL_LIBRARY_START", @@ -1288,7 +1289,15 @@ const PlatformActionErrorTypes = { API_ERROR: "API_ERROR", }; +const DeferRenderingAppViewerActionTypes = { + HAS_DISPATCHED_FIRST_EVALUATION_MESSAGE: + "HAS_DISPATCHED_FIRST_EVALUATION_MESSAGE", + RENDER_PAGE: "RENDER_PAGE", + IS_FIRST_PAGE_LOAD: "IS_FIRST_PAGE_LOAD", +}; + export const ReduxActionTypes = { + ...DeferRenderingAppViewerActionTypes, ...ActionActionTypes, ...AdminSettingsActionTypes, ...AnalyticsActionTypes, diff --git a/app/client/src/ce/reducers/index.tsx b/app/client/src/ce/reducers/index.tsx index 5cb3325151..1ad3dc0918 100644 --- a/app/client/src/ce/reducers/index.tsx +++ b/app/client/src/ce/reducers/index.tsx @@ -70,6 +70,7 @@ import type { layoutConversionReduxState } from "reducers/uiReducers/layoutConve import type { OneClickBindingState } from "reducers/uiReducers/oneClickBindingReducer"; import type { IDEState } from "reducers/uiReducers/ideReducer"; import type { PluginActionEditorState } from "PluginActionEditor"; +import type { FirstEvaluationState } from "reducers/evaluationReducers/firstEvaluationReducer"; /* Reducers which are integrated into the core system when registering a pluggable module or done so by a module that is designed to be eventually pluggable */ @@ -171,6 +172,7 @@ export interface AppState { loadingEntities: LoadingEntitiesState; formEvaluation: FormEvaluationState; triggers: TriggerValuesEvaluationState; + firstEvaluation: FirstEvaluationState; }; linting: { errors: LintErrorsStore; diff --git a/app/client/src/ce/sagas/PageSagas.tsx b/app/client/src/ce/sagas/PageSagas.tsx index 28555bcaa9..53902eb7f6 100644 --- a/app/client/src/ce/sagas/PageSagas.tsx +++ b/app/client/src/ce/sagas/PageSagas.tsx @@ -157,6 +157,7 @@ import { apiFailureResponseInterceptor } from "api/interceptors/response"; import type { AxiosError } from "axios"; import { handleFetchApplicationError } from "./ApplicationSagas"; import { getCurrentUser } from "actions/authActions"; +import { getIsFirstPageLoad } from "selectors/evaluationSelectors"; export interface HandleWidgetNameUpdatePayload { newName: string; @@ -370,8 +371,14 @@ export function* postFetchedPublishedPage( response.data.userPermissions, ), ); - // Clear any existing caches - yield call(clearEvalCache); + const isFirstLoad: boolean = yield select(getIsFirstPageLoad); + + // Only the first page load we defer the clearing of caches + if (!isFirstLoad) { + // Clear any existing caches + yield call(clearEvalCache); + } + // Set url params yield call(setDataUrl); diff --git a/app/client/src/ce/selectors/moduleInstanceSelectors.ts b/app/client/src/ce/selectors/moduleInstanceSelectors.ts index f26879d774..ff39a4f317 100644 --- a/app/client/src/ce/selectors/moduleInstanceSelectors.ts +++ b/app/client/src/ce/selectors/moduleInstanceSelectors.ts @@ -14,3 +14,6 @@ export const getModuleInstanceJSCollectionById = ( ): JSCollection | undefined => { return undefined; }; +export const getAllUniqueWidgetTypesInUiModules = (state: DefaultRootState) => { + return []; +}; diff --git a/app/client/src/components/editorComponents/form/ToggleComponentToJson.tsx b/app/client/src/components/editorComponents/form/ToggleComponentToJson.tsx index 6faf7c4025..4daf523d00 100644 --- a/app/client/src/components/editorComponents/form/ToggleComponentToJson.tsx +++ b/app/client/src/components/editorComponents/form/ToggleComponentToJson.tsx @@ -92,7 +92,7 @@ function ToggleComponentToJsonHandler(props: HandlerProps) { } function ToggleComponentToJson(props: Props) { - return props.viewType === ViewTypes.JSON + return props.viewType === ViewTypes.JSON && props.renderCompFunction ? props.renderCompFunction({ ...alternateViewTypeInputConfig(), configProperty: props.configProperty, diff --git a/app/client/src/components/propertyControls/index.ts b/app/client/src/components/propertyControls/index.ts index ef44f1f97c..0b84a4bf9b 100644 --- a/app/client/src/components/propertyControls/index.ts +++ b/app/client/src/components/propertyControls/index.ts @@ -6,8 +6,8 @@ import type { SwitchControlProps } from "components/propertyControls/SwitchContr import SwitchControl from "components/propertyControls/SwitchControl"; import OptionControl from "components/propertyControls/OptionControl"; import type { ControlProps } from "components/propertyControls/BaseControl"; -import type BaseControl from "components/propertyControls/BaseControl"; import CodeEditorControl from "components/propertyControls/CodeEditorControl"; +import type BaseControl from "components/propertyControls/BaseControl"; import type { DatePickerControlProps } from "components/propertyControls/DatePickerControl"; import DatePickerControl from "components/propertyControls/DatePickerControl"; import ChartDataControl from "components/propertyControls/ChartDataControl"; diff --git a/app/client/src/entities/Engine/AppViewerEngine.ts b/app/client/src/entities/Engine/AppViewerEngine.ts index 9fd5485026..36d07dc0ef 100644 --- a/app/client/src/entities/Engine/AppViewerEngine.ts +++ b/app/client/src/entities/Engine/AppViewerEngine.ts @@ -11,7 +11,7 @@ import { ReduxActionTypes, } from "ee/constants/ReduxActionConstants"; import type { APP_MODE } from "entities/App"; -import { call, put, spawn } from "redux-saga/effects"; +import { call, put, select, spawn } from "redux-saga/effects"; import type { DeployConsolidatedApi } from "sagas/InitSagas"; import { failFastApiCalls, @@ -20,7 +20,10 @@ import { } from "sagas/InitSagas"; import type { AppEnginePayload } from "."; import AppEngine, { ActionsNotFoundError } from "."; -import { fetchJSLibraries } from "actions/JSLibraryActions"; +import { + fetchJSLibraries, + deferLoadingJSLibraries, +} from "actions/JSLibraryActions"; import { waitForFetchUserSuccess } from "ee/sagas/userSagas"; import { fetchJSCollectionsForView } from "actions/jsActionActions"; import { @@ -29,6 +32,7 @@ import { } from "actions/appThemingActions"; import type { Span } from "instrumentation/types"; import { endSpan, startNestedSpan } from "instrumentation/generateTraces"; +import { getIsFirstPageLoad } from "selectors/evaluationSelectors"; export default class AppViewerEngine extends AppEngine { constructor(mode: APP_MODE) { @@ -120,9 +124,18 @@ export default class AppViewerEngine extends AppEngine { ReduxActionErrorTypes.SETUP_PUBLISHED_PAGE_ERROR, ]; - initActionsCalls.push(fetchJSLibraries(applicationId, customJSLibraries)); - successActionEffects.push(ReduxActionTypes.FETCH_JS_LIBRARIES_SUCCESS); - failureActionEffects.push(ReduxActionErrorTypes.FETCH_JS_LIBRARIES_FAILED); + const isFirstPageLoad = yield select(getIsFirstPageLoad); + + if (isFirstPageLoad) { + // we are deferring the loading of JS libraries + yield put(deferLoadingJSLibraries(applicationId, customJSLibraries)); + } else { + initActionsCalls.push(fetchJSLibraries(applicationId, customJSLibraries)); + successActionEffects.push(ReduxActionTypes.FETCH_JS_LIBRARIES_SUCCESS); + failureActionEffects.push( + ReduxActionErrorTypes.FETCH_JS_LIBRARIES_FAILED, + ); + } const resultOfPrimaryCalls: boolean = yield failFastApiCalls( initActionsCalls, diff --git a/app/client/src/layoutSystems/anvil/editor/canvasArenas/hooks/useAnvilDnDListenerStates.ts b/app/client/src/layoutSystems/anvil/editor/canvasArenas/hooks/useAnvilDnDListenerStates.ts index 287bfc5b7d..a7b090d8b7 100644 --- a/app/client/src/layoutSystems/anvil/editor/canvasArenas/hooks/useAnvilDnDListenerStates.ts +++ b/app/client/src/layoutSystems/anvil/editor/canvasArenas/hooks/useAnvilDnDListenerStates.ts @@ -15,8 +15,8 @@ import { getWidgetHierarchy } from "layoutSystems/anvil/utils/paste/utils"; import type { AnvilGlobalDnDStates } from "../../canvas/hooks/useAnvilGlobalDnDStates"; import { getWidgets } from "sagas/selectors"; import { useMemo } from "react"; -import { WDSZoneWidget } from "widgets/wds/WDSZoneWidget"; import { useAnvilWidgetElevation } from "../../canvas/providers/AnvilWidgetElevationProvider"; +import { anvilWidgets } from "widgets/wds/constants"; interface AnvilDnDListenerStatesProps { anvilGlobalDragStates: AnvilGlobalDnDStates; @@ -148,7 +148,7 @@ export const useAnvilDnDListenerStates = ({ }, [widgetProps, allWidgets]); const isElevatedWidget = useMemo(() => { - if (widgetProps.type === WDSZoneWidget.type) { + if (widgetProps.type === anvilWidgets.ZONE_WIDGET) { const isAnyZoneElevated = allSiblingsWidgetIds.some( (each) => !!elevatedWidgets[each], ); diff --git a/app/client/src/layoutSystems/fixedlayout/canvas/FixedLayoutViewerCanvas.tsx b/app/client/src/layoutSystems/fixedlayout/canvas/FixedLayoutViewerCanvas.tsx index 9e1dd6f580..8bcda421de 100644 --- a/app/client/src/layoutSystems/fixedlayout/canvas/FixedLayoutViewerCanvas.tsx +++ b/app/client/src/layoutSystems/fixedlayout/canvas/FixedLayoutViewerCanvas.tsx @@ -56,14 +56,13 @@ export const FixedLayoutViewerCanvas = (props: BaseWidgetProps) => { !!props.noPad, ); }, [ - props.children, + props?.children, + props?.metaWidgetChildrenStructure, props.positioning, - props.shouldScrollContents, props.widgetId, - props.componentHeight, - props.componentWidth, - snapColumnSpace, - props.metaWidgetChildrenStructure, + props.noPad, + defaultWidgetProps, + layoutSystemProps, ]); const snapRows = getCanvasSnapRows(props.bottomRow); diff --git a/app/client/src/pages/AppViewer/Navigation/index.tsx b/app/client/src/pages/AppViewer/Navigation/index.tsx index b9728ba94f..117782f2e0 100644 --- a/app/client/src/pages/AppViewer/Navigation/index.tsx +++ b/app/client/src/pages/AppViewer/Navigation/index.tsx @@ -31,6 +31,7 @@ import { useIsMobileDevice } from "utils/hooks/useDeviceDetect"; import HtmlTitle from "../AppViewerHtmlTitle"; import Sidebar from "./Sidebar"; import TopHeader from "./components/TopHeader"; +import { getRenderPage } from "selectors/evaluationSelectors"; export function Navigation() { const dispatch = useDispatch(); @@ -50,7 +51,7 @@ export function Navigation() { getCurrentApplication, ); const pages = useSelector(getViewModePageList); - + const shouldShowHeader = useSelector(getRenderPage); const queryParams = new URLSearchParams(search); const isEmbed = queryParams.get("embed") === "true"; const forceShowNavBar = queryParams.get("navbar") === "true"; @@ -69,15 +70,17 @@ export function Navigation() { // TODO: refactor this to not directly reference a DOM element by class defined elsewhere useEffect( function adjustHeaderHeightEffect() { - const header = document.querySelector(".js-appviewer-header"); + if (shouldShowHeader) { + const header = document.querySelector(".js-appviewer-header"); - dispatch(setAppViewHeaderHeight(header?.clientHeight || 0)); + dispatch(setAppViewHeaderHeight(header?.clientHeight || 0)); + } return () => { dispatch(setAppViewHeaderHeight(0)); }; }, - [navStyle, orientation, dispatch], + [navStyle, orientation, dispatch, shouldShowHeader], ); useEffect( @@ -122,6 +125,8 @@ export function Navigation() { pages, ]); + if (!shouldShowHeader) return null; + if (hideHeader) return ; return ( diff --git a/app/client/src/pages/AppViewer/index.tsx b/app/client/src/pages/AppViewer/index.tsx index 3f03b317d8..df515323ef 100644 --- a/app/client/src/pages/AppViewer/index.tsx +++ b/app/client/src/pages/AppViewer/index.tsx @@ -40,8 +40,6 @@ import { getAppThemeSettings, getCurrentApplication, } from "ee/selectors/applicationSelectors"; -import { editorInitializer } from "../../utils/editor/EditorUtils"; -import { widgetInitialisationSuccess } from "../../actions/widgetActions"; import { ThemeProvider as WDSThemeProvider, useTheme, @@ -49,6 +47,10 @@ import { import urlBuilder from "ee/entities/URLRedirect/URLAssembly"; import { getHideWatermark } from "ee/selectors/organizationSelectors"; import { getIsAnvilLayout } from "layoutSystems/anvil/integrations/selectors"; +import { getRenderPage } from "selectors/evaluationSelectors"; +import type { ReactNode } from "react"; +import { registerLayoutComponents } from "layoutSystems/anvil/utils/layouts/layoutUtils"; +import { widgetInitialisationSuccess } from "actions/widgetActions"; const AppViewerBody = styled.section<{ hasPages: boolean; @@ -80,6 +82,21 @@ type Props = AppViewerProps & RouteComponentProps; const DEFAULT_FONT_NAME = "System Default"; +function WDSThemeProviderWithTheme({ children }: { children: ReactNode }) { + const isAnvilLayout = useSelector(getIsAnvilLayout); + const themeSetting = useSelector(getAppThemeSettings); + const wdsThemeProps = { + borderRadius: themeSetting.borderRadius, + seedColor: themeSetting.accentColor, + colorMode: themeSetting.colorMode.toLowerCase(), + userSizing: themeSetting.sizing, + userDensity: themeSetting.density, + } as Parameters[0]; + const { theme } = useTheme(isAnvilLayout ? wdsThemeProps : {}); + + return {children}; +} + function AppViewer(props: Props) { const dispatch = useDispatch(); const { pathname, search } = props.location; @@ -103,15 +120,7 @@ function AppViewer(props: Props) { getCurrentApplication, ); const isAnvilLayout = useSelector(getIsAnvilLayout); - const themeSetting = useSelector(getAppThemeSettings); - const wdsThemeProps = { - borderRadius: themeSetting.borderRadius, - seedColor: themeSetting.accentColor, - colorMode: themeSetting.colorMode.toLowerCase(), - userSizing: themeSetting.sizing, - userDensity: themeSetting.density, - } as Parameters[0]; - const { theme } = useTheme(isAnvilLayout ? wdsThemeProps : {}); + const renderPage = useSelector(getRenderPage); const focusRef = useWidgetFocus(); const isAutoLayout = useSelector(getIsAutoLayout); @@ -120,9 +129,9 @@ function AppViewer(props: Props) { * initializes the widgets factory and registers all widgets */ useEffect(() => { - editorInitializer().then(() => { - dispatch(widgetInitialisationSuccess()); - }); + registerLayoutComponents(); + // we want to intialise only the widgets relevant to the tab within the appViewer page first so that first evaluation is faster + dispatch(widgetInitialisationSuccess()); }, []); /** * initialize the app if branch, pageId or application is changed @@ -205,6 +214,8 @@ function AppViewer(props: Props) { }; }, [selectedTheme.properties.fontFamily.appFont]); + if (!renderPage) return null; + const renderChildren = () => { return ( @@ -251,7 +262,7 @@ function AppViewer(props: Props) { if (isAnvilLayout) { return ( - {renderChildren()} + {renderChildren()} ); } diff --git a/app/client/src/pages/Templates/TemplateView.tsx b/app/client/src/pages/Templates/TemplateView.tsx index 10add2e2f1..d1e9736e1b 100644 --- a/app/client/src/pages/Templates/TemplateView.tsx +++ b/app/client/src/pages/Templates/TemplateView.tsx @@ -24,7 +24,7 @@ import TemplateDescription from "./Template/TemplateDescription"; import SimilarTemplates from "./Template/SimilarTemplates"; import { templateIdUrl } from "ee/RouteBuilder"; import TemplateViewHeader from "./TemplateViewHeader"; -import { registerEditorWidgets } from "utils/editor/EditorUtils"; +import { registerAllWidgets } from "utils/editor/EditorUtils"; const Wrapper = styled.div` overflow: auto; @@ -154,7 +154,7 @@ export function TemplateView({ }; useEffect(() => { - registerEditorWidgets(); + registerAllWidgets(); }, []); useEffect(() => { dispatch(getTemplateInformation(templateId)); diff --git a/app/client/src/reducers/evaluationReducers/firstEvaluationReducer.ts b/app/client/src/reducers/evaluationReducers/firstEvaluationReducer.ts new file mode 100644 index 0000000000..ff43e257e9 --- /dev/null +++ b/app/client/src/reducers/evaluationReducers/firstEvaluationReducer.ts @@ -0,0 +1,26 @@ +import type { ReduxAction } from "actions/ReduxActionTypes"; +import { ReduxActionTypes } from "ee/constants/ReduxActionConstants"; + +export interface FirstEvaluationState { + renderPage: boolean; + isFirstPageLoad: boolean; +} + +const initialState: FirstEvaluationState = { + renderPage: false, + isFirstPageLoad: true, +}; + +export default function firstEvaluationReducer( + state = initialState, + action: ReduxAction, +): FirstEvaluationState { + switch (action.type) { + case ReduxActionTypes.RENDER_PAGE: + return { ...state, renderPage: true }; + case ReduxActionTypes.IS_FIRST_PAGE_LOAD: + return { ...state, isFirstPageLoad: false }; + default: + return state; + } +} diff --git a/app/client/src/reducers/evaluationReducers/index.ts b/app/client/src/reducers/evaluationReducers/index.ts index 4e4540abbf..81aa5ff013 100644 --- a/app/client/src/reducers/evaluationReducers/index.ts +++ b/app/client/src/reducers/evaluationReducers/index.ts @@ -4,6 +4,7 @@ import evaluationDependencyReducer from "./dependencyReducer"; import loadingEntitiesReducer from "./loadingEntitiesReducer"; import formEvaluationReducer from "./formEvaluationReducer"; import triggerReducer from "./triggerReducer"; +import firstEvaluationReducer from "./firstEvaluationReducer"; export default combineReducers({ tree: evaluatedTreeReducer, @@ -11,4 +12,5 @@ export default combineReducers({ loadingEntities: loadingEntitiesReducer, formEvaluation: formEvaluationReducer, triggers: triggerReducer, + firstEvaluation: firstEvaluationReducer, }); diff --git a/app/client/src/sagas/EvalWorkerActionSagas.ts b/app/client/src/sagas/EvalWorkerActionSagas.ts index 88b281f38c..75c268d1aa 100644 --- a/app/client/src/sagas/EvalWorkerActionSagas.ts +++ b/app/client/src/sagas/EvalWorkerActionSagas.ts @@ -1,4 +1,4 @@ -import { all, call, put, select, spawn, take } from "redux-saga/effects"; +import { all, call, put, spawn, take } from "redux-saga/effects"; import { ReduxActionTypes } from "ee/constants/ReduxActionConstants"; import { MAIN_THREAD_ACTION } from "ee/workers/Evaluation/evalWorkerActions"; import log from "loglevel"; @@ -13,6 +13,7 @@ import { MessageType } from "utils/MessageUtil"; import type { ResponsePayload } from "../sagas/EvaluationsSaga"; import { executeTriggerRequestSaga, + getUnevalTreeWithWidgetsRegistered, updateDataTreeHandler, } from "../sagas/EvaluationsSaga"; import { evalWorker } from "utils/workerInstances"; @@ -22,7 +23,7 @@ import isEmpty from "lodash/isEmpty"; import { sortJSExecutionDataByCollectionId } from "workers/Evaluation/JSObject/utils"; import type { LintTreeSagaRequestData } from "plugins/Linting/types"; import { evalErrorHandler } from "./EvalErrorHandler"; -import { getUnevaluatedDataTree } from "selectors/dataTreeSelectors"; +import type { getUnevaluatedDataTree } from "selectors/dataTreeSelectors"; import { endSpan, startRootSpan } from "instrumentation/generateTraces"; import type { UpdateDataTreeMessageData } from "./types"; @@ -165,9 +166,8 @@ export function* handleEvalWorkerMessage(message: TMessage) { case MAIN_THREAD_ACTION.UPDATE_DATATREE: { const { workerResponse } = data as UpdateDataTreeMessageData; const rootSpan = startRootSpan("DataTreeFactory.create"); - const unEvalAndConfigTree: ReturnType = - yield select(getUnevaluatedDataTree); + yield call(getUnevalTreeWithWidgetsRegistered); endSpan(rootSpan); diff --git a/app/client/src/sagas/EvaluationsSaga.test.ts b/app/client/src/sagas/EvaluationsSaga.test.ts index f37629c0f3..0cb1ea8d1b 100644 --- a/app/client/src/sagas/EvaluationsSaga.test.ts +++ b/app/client/src/sagas/EvaluationsSaga.test.ts @@ -34,8 +34,17 @@ import { getCurrentPageId, } from "selectors/editorSelectors"; import { updateActionData } from "actions/pluginActionActions"; +import watchInitSagas from "./InitSagas"; + +import { clearAllWidgetFactoryCache } from "WidgetProvider/factory/decorators"; jest.mock("loglevel"); +jest.mock("utils/editor/EditorUtils", () => ({ + registerAllWidgets: jest.fn(), +})); +jest.mock("WidgetProvider/factory/decorators", () => ({ + clearAllWidgetFactoryCache: jest.fn(), +})); describe("evaluateTreeSaga", () => { afterAll(() => { @@ -64,29 +73,34 @@ describe("evaluateTreeSaga", () => { ], [select(getCurrentPageDSLVersion), 1], ]) - .call(evalWorker.request, EVAL_WORKER_ACTIONS.EVAL_TREE, { - cacheProps: { - instanceId: "instanceId", - appId: "applicationId", - pageId: "pageId", + .call( + evalWorker.request, + EVAL_WORKER_ACTIONS.EVAL_TREE, + { + cacheProps: { + instanceId: "instanceId", + appId: "applicationId", + pageId: "pageId", + appMode: false, + timestamp: new Date("11 September 2024").toISOString(), + dslVersion: 1, + }, + unevalTree: unEvalAndConfigTree, + widgetTypeConfigMap: undefined, + widgets: {}, + theme: {}, + shouldReplay: true, + allActionValidationConfig: {}, + forceEvaluation: false, + metaWidgets: {}, appMode: false, - timestamp: new Date("11 September 2024").toISOString(), - dslVersion: 1, + widgetsMeta: {}, + shouldRespondWithLogs: true, + affectedJSObjects: { ids: [], isAllAffected: false }, + actionDataPayloadConsolidated: undefined, }, - unevalTree: unEvalAndConfigTree, - widgetTypeConfigMap: undefined, - widgets: {}, - theme: {}, - shouldReplay: true, - allActionValidationConfig: {}, - forceEvaluation: false, - metaWidgets: {}, - appMode: false, - widgetsMeta: {}, - shouldRespondWithLogs: true, - affectedJSObjects: { ids: [], isAllAffected: false }, - actionDataPayloadConsolidated: undefined, - }) + false, + ) .run(); }); test("should set 'shouldRespondWithLogs' to false when the log level is not debug", async () => { @@ -112,29 +126,34 @@ describe("evaluateTreeSaga", () => { ], [select(getCurrentPageDSLVersion), 1], ]) - .call(evalWorker.request, EVAL_WORKER_ACTIONS.EVAL_TREE, { - cacheProps: { - instanceId: "instanceId", - appId: "applicationId", - pageId: "pageId", + .call( + evalWorker.request, + EVAL_WORKER_ACTIONS.EVAL_TREE, + { + cacheProps: { + instanceId: "instanceId", + appId: "applicationId", + pageId: "pageId", + appMode: false, + timestamp: new Date("11 September 2024").toISOString(), + dslVersion: 1, + }, + unevalTree: unEvalAndConfigTree, + widgetTypeConfigMap: undefined, + widgets: {}, + theme: {}, + shouldReplay: true, + allActionValidationConfig: {}, + forceEvaluation: false, + metaWidgets: {}, appMode: false, - timestamp: new Date("11 September 2024").toISOString(), - dslVersion: 1, + widgetsMeta: {}, + shouldRespondWithLogs: false, + affectedJSObjects: { ids: [], isAllAffected: false }, + actionDataPayloadConsolidated: undefined, }, - unevalTree: unEvalAndConfigTree, - widgetTypeConfigMap: undefined, - widgets: {}, - theme: {}, - shouldReplay: true, - allActionValidationConfig: {}, - forceEvaluation: false, - metaWidgets: {}, - appMode: false, - widgetsMeta: {}, - shouldRespondWithLogs: false, - affectedJSObjects: { ids: [], isAllAffected: false }, - actionDataPayloadConsolidated: undefined, - }) + false, + ) .run(); }); test("should propagate affectedJSObjects property to evaluation action", async () => { @@ -169,29 +188,95 @@ describe("evaluateTreeSaga", () => { ], [select(getCurrentPageDSLVersion), 1], ]) - .call(evalWorker.request, EVAL_WORKER_ACTIONS.EVAL_TREE, { - cacheProps: { - instanceId: "instanceId", - appId: "applicationId", - pageId: "pageId", + .call( + evalWorker.request, + EVAL_WORKER_ACTIONS.EVAL_TREE, + { + cacheProps: { + instanceId: "instanceId", + appId: "applicationId", + pageId: "pageId", + appMode: false, + timestamp: new Date("11 September 2024").toISOString(), + dslVersion: 1, + }, + unevalTree: unEvalAndConfigTree, + widgetTypeConfigMap: undefined, + widgets: {}, + theme: {}, + shouldReplay: true, + allActionValidationConfig: {}, + forceEvaluation: false, + metaWidgets: {}, appMode: false, - timestamp: new Date("11 September 2024").toISOString(), - dslVersion: 1, + widgetsMeta: {}, + shouldRespondWithLogs: false, + affectedJSObjects, + actionDataPayloadConsolidated: undefined, }, - unevalTree: unEvalAndConfigTree, - widgetTypeConfigMap: undefined, - widgets: {}, - theme: {}, - shouldReplay: true, - allActionValidationConfig: {}, - forceEvaluation: false, - metaWidgets: {}, - appMode: false, - widgetsMeta: {}, - shouldRespondWithLogs: false, - affectedJSObjects, - actionDataPayloadConsolidated: undefined, - }) + false, + ) + .run(); + }); + test("should call evalWorker.request with isFirstEvaluation as true when isFirstEvaluation is set as true in evaluateTreeSaga", async () => { + const unEvalAndConfigTree = { unEvalTree: {}, configTree: {} }; + const isFirstEvaluation = true; + + return expectSaga( + evaluateTreeSaga, + unEvalAndConfigTree, + [], + undefined, + undefined, + undefined, + undefined, + undefined, + isFirstEvaluation, + ) + .provide([ + [select(getAllActionValidationConfig), {}], + [select(getWidgets), {}], + [select(getMetaWidgets), {}], + [select(getSelectedAppTheme), {}], + [select(getAppMode), false], + [select(getWidgetsMeta), {}], + [select(getInstanceId), "instanceId"], + [select(getCurrentApplicationId), "applicationId"], + [select(getCurrentPageId), "pageId"], + [ + select(getApplicationLastDeployedAt), + new Date("11 September 2024").toISOString(), + ], + [select(getCurrentPageDSLVersion), 1], + ]) + .call( + evalWorker.request, + EVAL_WORKER_ACTIONS.EVAL_TREE, + { + cacheProps: { + instanceId: "instanceId", + appId: "applicationId", + pageId: "pageId", + appMode: false, + timestamp: new Date("11 September 2024").toISOString(), + dslVersion: 1, + }, + unevalTree: unEvalAndConfigTree, + widgetTypeConfigMap: undefined, + widgets: {}, + theme: {}, + shouldReplay: true, + allActionValidationConfig: {}, + forceEvaluation: false, + metaWidgets: {}, + appMode: false, + widgetsMeta: {}, + shouldRespondWithLogs: false, + affectedJSObjects: { ids: [], isAllAffected: false }, + actionDataPayloadConsolidated: undefined, + }, + true, + ) .run(); }); }); @@ -534,3 +619,15 @@ describe("evaluationLoopWithDebounce", () => { }); }); }); + +describe("first evaluation integration", () => { + it("should call clearAllWidgetFactoryCache when WIDGET_INIT_SUCCESS is dispatched", async () => { + await expectSaga(watchInitSagas) + .dispatch({ + type: ReduxActionTypes.WIDGET_INIT_SUCCESS, + }) + .silentRun(); + + expect(clearAllWidgetFactoryCache).toHaveBeenCalled(); + }); +}); diff --git a/app/client/src/sagas/EvaluationsSaga.ts b/app/client/src/sagas/EvaluationsSaga.ts index 49dae0c200..dcfab0a378 100644 --- a/app/client/src/sagas/EvaluationsSaga.ts +++ b/app/client/src/sagas/EvaluationsSaga.ts @@ -1,4 +1,9 @@ -import type { ActionPattern, CallEffect, ForkEffect } from "redux-saga/effects"; +import type { + ActionPattern, + CallEffect, + Effect, + ForkEffect, +} from "redux-saga/effects"; import { actionChannel, all, @@ -9,6 +14,7 @@ import { select, spawn, take, + join, } from "redux-saga/effects"; import type { @@ -16,7 +22,10 @@ import type { ReduxActionType, AnyReduxAction, } from "actions/ReduxActionTypes"; -import { ReduxActionTypes } from "ee/constants/ReduxActionConstants"; +import { + ReduxActionTypes, + ReduxActionErrorTypes, +} from "ee/constants/ReduxActionConstants"; import { getDataTree, getUnevaluatedDataTree, @@ -39,6 +48,7 @@ import { import { setDependencyMap, setEvaluatedTree, + setIsFirstPageLoad, shouldForceEval, shouldLog, shouldProcessAction, @@ -99,7 +109,7 @@ import { } from "actions/pluginActionActions"; import { executeJSUpdates } from "actions/jsPaneActions"; import { setEvaluatedActionSelectorField } from "actions/actionSelectorActions"; -import { waitForWidgetConfigBuild } from "./InitSagas"; + import { logDynamicTriggerExecution } from "ee/sagas/analyticsSaga"; import { selectFeatureFlags } from "ee/selectors/featureFlagsSelectors"; import { fetchFeatureFlagsInit } from "actions/userActions"; @@ -108,7 +118,6 @@ import { parseUpdatesAndDeleteUndefinedUpdates, } from "./EvaluationsSagaUtils"; import { getFeatureFlagsFetched } from "selectors/usersSelectors"; -import { getIsCurrentEditorWorkflowType } from "ee/selectors/workflowSelectors"; import { evalErrorHandler } from "./EvalErrorHandler"; import AnalyticsUtil from "ee/utils/AnalyticsUtil"; import { endSpan, startRootSpan } from "instrumentation/generateTraces"; @@ -124,11 +133,89 @@ import type { EvaluationReduxAction, } from "actions/EvaluationReduxActionTypes"; import { appsmithTelemetry } from "instrumentation"; +import { getUsedWidgetTypes } from "selectors/widgetSelectors"; +import type BaseWidget from "widgets/BaseWidget"; +import { loadWidget } from "widgets"; +import { registerWidgets } from "WidgetProvider/factory/registrationHelper"; +import { failFastApiCalls } from "./InitSagas"; +import { fetchJSLibraries } from "actions/JSLibraryActions"; +import type { Task } from "redux-saga"; +import { getAllUniqueWidgetTypesInUiModules } from "ee/selectors/moduleInstanceSelectors"; +import { clearAllWidgetFactoryCache } from "WidgetProvider/factory/decorators"; const APPSMITH_CONFIGS = getAppsmithConfigs(); let widgetTypeConfigMap: WidgetTypeConfigMap; +// Common worker setup logic +// TODO: Fix this the next time the file is edited +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function* setupWorkers(clearCache = false): any { + // Explicitly shutdown old worker if present + yield all([call(evalWorker.shutdown), call(lintWorker.shutdown)]); + const [evalWorkerListenerChannel] = yield all([ + call(evalWorker.start), + call(lintWorker.start), + ]); + + if (clearCache) { + yield call(evalWorker.request, EVAL_WORKER_ACTIONS.CLEAR_CACHE); + } + + const isFFFetched = yield select(getFeatureFlagsFetched); + + if (!isFFFetched) { + yield call(fetchFeatureFlagsInit); + yield take(ReduxActionTypes.FETCH_FEATURE_FLAGS_SUCCESS); + } + + const featureFlags: Record = + yield select(selectFeatureFlags); + + yield call(evalWorker.request, EVAL_WORKER_ACTIONS.SETUP, { + cloudHosting: !!APPSMITH_CONFIGS.cloudHosting, + featureFlags: featureFlags, + }); + + return evalWorkerListenerChannel; +} + +// TODO: Fix this the next time the file is edited +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function* webWorkerSetupSaga(): any { + const evalWorkerListenerChannel = yield call(setupWorkers); + + yield spawn(handleEvalWorkerRequestSaga, evalWorkerListenerChannel); +} + +function* webWorkerSetupSagaWithJSLibraries( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + initializeJSLibrariesChannel: any, + // eslint-disable-next-line @typescript-eslint/no-explicit-any +): any { + const evalWorkerListenerChannel = yield call(setupWorkers, true); + + // Take the action from the appVi + const jsLibrariesAction = yield take(initializeJSLibrariesChannel); + const { applicationId, customJSLibraries } = jsLibrariesAction.payload; + + yield put(setIsFirstPageLoad()); + + // Use failFastApiCalls to execute fetchJSLibraries + const resultOfJSLibrariesCall: boolean = yield call( + failFastApiCalls, + [fetchJSLibraries(applicationId, customJSLibraries)], + [ReduxActionTypes.FETCH_JS_LIBRARIES_SUCCESS], + [ReduxActionErrorTypes.FETCH_JS_LIBRARIES_FAILED], + ); + + if (!resultOfJSLibrariesCall) { + throw new Error("Failed to load JS libraries"); + } + + yield spawn(handleEvalWorkerRequestSaga, evalWorkerListenerChannel); +} + export function* updateDataTreeHandler( data: { evalTreeResponse: EvalTreeResponseData; @@ -271,6 +358,7 @@ export function* evaluateTreeSaga( requiresLogging = false, affectedJSObjects: AffectedJSObjects = defaultAffectedJSObjects, actionDataPayloadConsolidated?: actionDataPayload, + isFirstEvaluation = false, ) { const allActionValidationConfig: ReturnType< typeof getAllActionValidationConfig @@ -322,6 +410,7 @@ export function* evaluateTreeSaga( evalWorker.request, EVAL_WORKER_ACTIONS.EVAL_TREE, evalTreeRequestData, + isFirstEvaluation, ); yield call( @@ -369,8 +458,8 @@ export function* evaluateAndExecuteDynamicTrigger( ) { const rootSpan = startRootSpan("DataTreeFactory.create"); - const unEvalTree: ReturnType = yield select( - getUnevaluatedDataTree, + const unEvalTree: ReturnType = yield call( + getUnevalTreeWithWidgetsRegistered, ); endSpan(rootSpan); @@ -521,7 +610,7 @@ function* validateProperty(property: string, value: any, props: WidgetProps) { const rootSpan = startRootSpan("DataTreeFactory.create"); const unEvalAndConfigTree: ReturnType = - yield select(getUnevaluatedDataTree); + yield call(getUnevalTreeWithWidgetsRegistered); endSpan(rootSpan); const configTree = unEvalAndConfigTree.configTree; @@ -541,6 +630,15 @@ function* validateProperty(property: string, value: any, props: WidgetProps) { return response; } +export function* getUnevalTreeWithWidgetsRegistered() { + yield call(loadAndRegisterOnlyCanvasWidgets); + + const unEvalAndConfigTree: ReturnType = + yield select(getUnevaluatedDataTree); + + return unEvalAndConfigTree; +} + // We are clubbing all pending action's affected JS objects into the buffered action // So that during that evaluation cycle all affected JS objects are correctly diffed function mergeJSBufferedActions( @@ -706,6 +804,8 @@ export function* evalAndLintingHandler( requiresLogging: boolean; affectedJSObjects: AffectedJSObjects; actionDataPayloadConsolidated: actionDataPayload[]; + isFirstEvaluation?: boolean; + jsLibrariesTask?: Task; }>, ) { const span = startRootSpan("evalAndLintingHandler"); @@ -713,6 +813,9 @@ export function* evalAndLintingHandler( actionDataPayloadConsolidated, affectedJSObjects, forceEvaluation, + + isFirstEvaluation = false, + jsLibrariesTask, requiresLogging, shouldReplay, } = options; @@ -737,10 +840,17 @@ export function* evalAndLintingHandler( // Generate all the data needed for both eval and linting const unEvalAndConfigTree: ReturnType = - yield select(getUnevaluatedDataTree); + yield call(getUnevalTreeWithWidgetsRegistered); + + widgetTypeConfigMap = WidgetFactory.getWidgetTypeConfigMap(); endSpan(rootSpan); + // wait for the webworker to complete its setup before starting the evaluation + if (jsLibrariesTask) { + yield join(jsLibrariesTask); + } + const postEvalActions = getPostEvalActions(action); const fn: (...args: unknown[]) => CallEffect | ForkEffect = isBlockingCall ? call : fork; @@ -758,6 +868,7 @@ export function* evalAndLintingHandler( requiresLogging, affectedJSObjects, actionDataPayloadConsolidated, + isFirstEvaluation, ), ); } @@ -769,51 +880,80 @@ export function* evalAndLintingHandler( yield all(effects); endSpan(span); } +export function* loadAndRegisterOnlyCanvasWidgets(): Generator< + Effect, + (typeof BaseWidget)[], + unknown +> { + try { + const widgetTypes = (yield select(getUsedWidgetTypes)) as string[]; + + const uiModuleTypes = (yield select( + getAllUniqueWidgetTypesInUiModules, + )) as string[]; + + const uniqueWidgetTypes = Array.from( + new Set([...uiModuleTypes, ...widgetTypes, "SKELETON_WIDGET"]), + ); + + // Filter out already registered widget types + const unregisteredWidgetTypes = uniqueWidgetTypes.filter( + (type: string) => !WidgetFactory.widgetsMap.has(type), + ); + + if (!unregisteredWidgetTypes.length) { + return []; + } + + // Load only unregistered widgets in parallel + const loadedWidgets = (yield all( + unregisteredWidgetTypes.map((type: string) => call(loadWidget, type)), + )) as (typeof BaseWidget)[]; + + // Register only the newly loaded widgets + registerWidgets(loadedWidgets); + + clearAllWidgetFactoryCache(); + + return loadedWidgets; + } catch (error) { + log.error("Error loading and registering widgets:", error); + throw error; + } +} // TODO: Fix this the next time the file is edited // eslint-disable-next-line @typescript-eslint/no-explicit-any function* evaluationChangeListenerSaga(): any { const firstEvalActionChannel = yield actionChannel(FIRST_EVAL_REDUX_ACTIONS); - // Explicitly shutdown old worker if present - yield all([call(evalWorker.shutdown), call(lintWorker.shutdown)]); - const [evalWorkerListenerChannel] = yield all([ - call(evalWorker.start), - call(lintWorker.start), - ]); + const initializeJSLibrariesChannel = yield actionChannel( + ReduxActionTypes.DEFER_LOADING_JS_LIBRARIES, + ); + const appMode = yield select(getAppMode); - const isFFFetched = yield select(getFeatureFlagsFetched); + let jsLibrariesTask: Task | undefined; - if (!isFFFetched) { - yield call(fetchFeatureFlagsInit); - yield take(ReduxActionTypes.FETCH_FEATURE_FLAGS_SUCCESS); + // for all published apps, we need to reset the data tree and setup the worker as an independent process + // after the process is forked we can allow the main thread to continue its execution since the main thread's tasks would be independent + // we just need to ensure that the webworker setup is completed before the first evaluation is triggered + if (appMode === APP_MODE.PUBLISHED) { + yield put({ type: ReduxActionTypes.RESET_DATA_TREE }); + jsLibrariesTask = yield fork( + webWorkerSetupSagaWithJSLibraries, + initializeJSLibrariesChannel, + ); + } else { + // for all other modes, just call the webworker + yield call(webWorkerSetupSaga); } - const featureFlags: Record = - yield select(selectFeatureFlags); - - yield call(evalWorker.request, EVAL_WORKER_ACTIONS.SETUP, { - cloudHosting: !!APPSMITH_CONFIGS.cloudHosting, - featureFlags: featureFlags, - }); - yield spawn(handleEvalWorkerRequestSaga, evalWorkerListenerChannel); - const initAction: EvaluationReduxAction = yield take( firstEvalActionChannel, ); firstEvalActionChannel.close(); - // Wait for widget config build to complete before starting evaluation only if the current editor is not a workflow - const isCurrentEditorWorkflowType = yield select( - getIsCurrentEditorWorkflowType, - ); - - if (!isCurrentEditorWorkflowType) { - yield call(waitForWidgetConfigBuild); - } - - widgetTypeConfigMap = WidgetFactory.getWidgetTypeConfigMap(); yield fork(evalAndLintingHandler, false, initAction, { shouldReplay: false, forceEvaluation: false, @@ -822,6 +962,8 @@ function* evaluationChangeListenerSaga(): any { ids: [], isAllAffected: true, }, + isFirstEvaluation: true, + jsLibrariesTask: jsLibrariesTask, }); // TODO: Fix this the next time the file is edited // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/app/client/src/sagas/InitSagas.ts b/app/client/src/sagas/InitSagas.ts index 4ada867312..83699129de 100644 --- a/app/client/src/sagas/InitSagas.ts +++ b/app/client/src/sagas/InitSagas.ts @@ -93,6 +93,7 @@ import type { Page } from "entities/Page"; import type { PACKAGE_PULL_STATUS } from "ee/constants/ModuleConstants"; import { validateSessionToken } from "utils/SessionUtils"; import { appsmithTelemetry } from "instrumentation"; +import { clearAllWidgetFactoryCache } from "WidgetProvider/factory/decorators"; export const URL_CHANGE_ACTIONS = [ ReduxActionTypes.CURRENT_APPLICATION_NAME_UPDATE, @@ -535,6 +536,11 @@ function* eagerPageInitSaga() { } catch (e) {} } +function handleWidgetInitSuccess() { + //every time a widget is initialized, we clear the cache so that all widgetFactory values are recomputed + clearAllWidgetFactoryCache(); +} + export default function* watchInitSagas() { yield all([ takeLeading( @@ -547,5 +553,7 @@ export default function* watchInitSagas() { takeLatest(ReduxActionTypes.RESET_EDITOR_REQUEST, resetEditorSaga), takeEvery(URL_CHANGE_ACTIONS, updateURLSaga), takeEvery(ReduxActionTypes.INITIALIZE_CURRENT_PAGE, eagerPageInitSaga), + + takeLeading(ReduxActionTypes.WIDGET_INIT_SUCCESS, handleWidgetInitSuccess), ]); } diff --git a/app/client/src/selectors/actionSelectors.tsx b/app/client/src/selectors/actionSelectors.tsx index c368f4301e..c66e4c0d2d 100644 --- a/app/client/src/selectors/actionSelectors.tsx +++ b/app/client/src/selectors/actionSelectors.tsx @@ -1,6 +1,7 @@ import type { DataTree } from "entities/DataTree/dataTreeTypes"; import { createSelector } from "reselect"; import WidgetFactory from "WidgetProvider/factory"; +import { getWidgetConfigsVersion } from "WidgetProvider/factory/widgetConfigVersion"; import type { FlattenedWidgetProps } from "WidgetProvider/types"; import type { JSLibrary } from "workers/common/JSLibrary"; import { getDataTree } from "./dataTreeSelectors"; @@ -24,6 +25,7 @@ export const getUsedActionNames = createSelector( getDataTree, getParentWidget, selectInstalledLibraries, + getWidgetConfigsVersion, // Add dependency on widget configs version ( // TODO: Fix this the next time the file is edited // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/app/client/src/selectors/editorSelectors.tsx b/app/client/src/selectors/editorSelectors.tsx index 2861488720..5dc52c29b4 100644 --- a/app/client/src/selectors/editorSelectors.tsx +++ b/app/client/src/selectors/editorSelectors.tsx @@ -52,6 +52,7 @@ import type { Page } from "entities/Page"; import { objectKeys } from "@appsmith/utils"; import type { MetaWidgetsReduxState } from "reducers/entityReducers/metaWidgetsReducer"; import { ActionRunBehaviour } from "PluginActionEditor/types/PluginActionTypes"; +import { getWidgetConfigsVersion } from "WidgetProvider/factory/widgetConfigVersion"; const getIsDraggingOrResizing = (state: DefaultRootState) => state.ui.widgetDragResize.isResizing || state.ui.widgetDragResize.isDragging; @@ -398,6 +399,7 @@ const isModuleWidget = ( export const getWidgetCards = createSelector( getIsAutoLayout, getIsAnvilLayout, + getWidgetConfigsVersion, // Add dependency on widget configs version (isAutoLayout, isAnvilLayout) => { const widgetConfigs = WidgetFactory.getConfigs(); const widgetConfigsArray = Object.values(widgetConfigs); diff --git a/app/client/src/selectors/evaluationSelectors.ts b/app/client/src/selectors/evaluationSelectors.ts new file mode 100644 index 0000000000..8c4a165fbe --- /dev/null +++ b/app/client/src/selectors/evaluationSelectors.ts @@ -0,0 +1,7 @@ +import type { DefaultRootState } from "react-redux"; + +export const getRenderPage = (state: DefaultRootState): boolean => + state.evaluations?.firstEvaluation?.renderPage ?? false; + +export const getIsFirstPageLoad = (state: DefaultRootState): boolean => + state.evaluations?.firstEvaluation?.isFirstPageLoad ?? false; diff --git a/app/client/src/selectors/widgetSelectors.ts b/app/client/src/selectors/widgetSelectors.ts index 8b196c71b1..a6c59e57c9 100644 --- a/app/client/src/selectors/widgetSelectors.ts +++ b/app/client/src/selectors/widgetSelectors.ts @@ -8,6 +8,7 @@ import { getExistingWidgetNames } from "sagas/selectors"; import { getNextEntityName } from "utils/AppsmithUtils"; import WidgetFactory from "WidgetProvider/factory"; +import { getWidgetConfigsVersion } from "WidgetProvider/factory/widgetConfigVersion"; import { getAltBlockWidgetSelection, getFocusedWidget, @@ -78,6 +79,7 @@ export const getModalDropdownList = createSelector( export const getNextModalName = createSelector( getExistingWidgetNames, getModalWidgetType, + getWidgetConfigsVersion, // Add dependency on widget configs version (names, modalWidgetType) => { const prefix = WidgetFactory.widgetConfigMap.get(modalWidgetType)?.widgetName || ""; @@ -267,3 +269,19 @@ export const isResizingOrDragging = createSelector( (state: DefaultRootState) => state.ui.widgetDragResize.isDragging, (isResizing, isDragging) => !!isResizing || !!isDragging, ); +// get widgets types associated to a tab +export const getUsedWidgetTypes = createSelector( + getCanvasWidgets, + (canvasWidgets) => { + const widgetTypes = new Set(); + + // Iterate through all widgets in the state + Object.values(canvasWidgets).forEach((widget) => { + if (widget.type && !widget.type.startsWith("MODULE_WIDGET_")) { + widgetTypes.add(widget.type); + } + }); + + return Array.from(widgetTypes); + }, +); diff --git a/app/client/src/utils/WidgetSizeUtils.ts b/app/client/src/utils/WidgetSizeUtils.ts index 7941fc1154..a6f072f349 100644 --- a/app/client/src/utils/WidgetSizeUtils.ts +++ b/app/client/src/utils/WidgetSizeUtils.ts @@ -21,6 +21,7 @@ export const getCanvasHeightOffset = ( props: WidgetProps, ) => { const { getCanvasHeightOffset } = WidgetFactory.getWidgetMethods(widgetType); + let offset = 0; if (getCanvasHeightOffset) { diff --git a/app/client/src/utils/WorkerUtil.ts b/app/client/src/utils/WorkerUtil.ts index d6d37b8e8a..cabf791ba2 100644 --- a/app/client/src/utils/WorkerUtil.ts +++ b/app/client/src/utils/WorkerUtil.ts @@ -20,6 +20,7 @@ import { filterSpanData, newWebWorkerSpanData, } from "instrumentation/generateWebWorkerTraces"; +import { ReduxActionTypes } from "ee/constants/ReduxActionConstants"; /** * Wrap a webworker to provide a synchronous request-response semantic. @@ -241,12 +242,13 @@ export class GracefulWorkerService { * * @param method identifier for a rpc method * @param requestData data that we want to send over to the worker + * @param isFirstEvaluation whether this is the first evaluation of the request * * @returns response from the worker */ // TODO: Fix this the next time the file is edited // eslint-disable-next-line @typescript-eslint/no-explicit-any - *request(method: string, data = {}): any { + *request(method: string, data = {}, isFirstEvaluation = false): any { yield this.ready(true); // Impossible case, but helps avoid `?` later in code and makes it clearer. @@ -292,6 +294,12 @@ export class GracefulWorkerService { messageId, }); + // Use delay to ensure RENDER_PAGE is dispatched after the sendMessage macro task + if (isFirstEvaluation) { + yield delay(0); // This ensures the macro task completes + yield put({ type: ReduxActionTypes.RENDER_PAGE }); + } + // The `this._broker` method is listening to events and will pass response to us over this channel. const response = yield take(ch); const { data, endTime, startTime } = response; diff --git a/app/client/src/utils/editor/EditorUtils.ts b/app/client/src/utils/editor/EditorUtils.ts index 138da365f2..a44f0950d3 100644 --- a/app/client/src/utils/editor/EditorUtils.ts +++ b/app/client/src/utils/editor/EditorUtils.ts @@ -2,14 +2,20 @@ // import Widgets from "widgets"; import { registerWidgets } from "WidgetProvider/factory/registrationHelper"; import { registerLayoutComponents } from "layoutSystems/anvil/utils/layouts/layoutUtils"; -import widgets from "widgets"; +import { loadAllWidgets } from "widgets"; +export const registerAllWidgets = async () => { + try { + const loadedWidgets = await loadAllWidgets(); -export const registerEditorWidgets = () => { - registerWidgets(widgets); + registerWidgets(Array.from(loadedWidgets.values())); + } catch (error) { + // eslint-disable-next-line no-console + console.error("Error loading widgets", error); + } }; export const editorInitializer = async () => { - registerEditorWidgets(); + await registerAllWidgets(); // TODO: do this only for anvil. registerLayoutComponents(); }; diff --git a/app/client/src/utils/testPropertyPaneConfig.test.ts b/app/client/src/utils/testPropertyPaneConfig.test.ts index 21cfb9b462..8b54b5fb90 100644 --- a/app/client/src/utils/testPropertyPaneConfig.test.ts +++ b/app/client/src/utils/testPropertyPaneConfig.test.ts @@ -6,9 +6,10 @@ import type { } from "constants/PropertyControlConstants"; import { ValidationTypes } from "constants/WidgetValidation"; import { isFunction } from "lodash"; -import widgets from "widgets"; +import { loadAllWidgets } from "widgets"; import WidgetFactory from "WidgetProvider/factory"; import { registerWidgets } from "WidgetProvider/factory/registrationHelper"; +import type BaseWidget from "widgets/BaseWidget"; function validatePropertyPaneConfig( config: PropertyPaneConfig[], @@ -143,96 +144,112 @@ const isNotFloat = (n: any) => { }; describe("Tests all widget's propertyPane config", () => { - beforeAll(() => { - registerWidgets(widgets); + let widgetsArray: (typeof BaseWidget)[] = []; + + beforeAll(async () => { + // Load all widgets and convert Map to array + const widgetsMap = await loadAllWidgets(); + + widgetsArray = Array.from(widgetsMap.values()); + + // Register all widgets + registerWidgets(widgetsArray); }); - widgets - // Exclude WDS widgets from the tests, since they work differently - .filter((widget) => !widget.type.includes("WDS")) - .forEach((widget) => { - const config = widget.getConfig(); + it("should have loaded widgets", () => { + expect(widgetsArray.length).toBeGreaterThan(0); + }); - it(`Checks ${widget.type}'s propertyPaneConfig`, () => { - const propertyPaneConfig = widget.getPropertyPaneConfig(); + describe("Property Pane Config Tests", () => { + //widgets are loaded in the beforeAll and ready now + widgetsArray + // Exclude WDS widgets from the tests, since they work differently + .filter((widget) => !widget.type.includes("WDS")) + .forEach((widget) => { + const config = widget.getConfig(); - expect( - validatePropertyPaneConfig(propertyPaneConfig, !!config.hideCard), - ).toStrictEqual(true); - const propertyPaneContentConfig = widget.getPropertyPaneContentConfig(); + it(`Checks ${widget.type}'s propertyPaneConfig`, () => { + const propertyPaneConfig = widget.getPropertyPaneConfig(); - expect( - validatePropertyPaneConfig( - propertyPaneContentConfig, - !!config.isDeprecated, - ), - ).toStrictEqual(true); - const propertyPaneStyleConfig = widget.getPropertyPaneStyleConfig(); + expect( + validatePropertyPaneConfig(propertyPaneConfig, !!config.hideCard), + ).toStrictEqual(true); + const propertyPaneContentConfig = + widget.getPropertyPaneContentConfig(); - expect( - validatePropertyPaneConfig( - propertyPaneStyleConfig, - !!config.isDeprecated, - ), - ).toStrictEqual(true); - }); - it(`Check if ${widget.type}'s dimensions are always integers`, () => { - const defaults = widget.getDefaults(); + expect( + validatePropertyPaneConfig( + propertyPaneContentConfig, + !!config.isDeprecated, + ), + ).toStrictEqual(true); + const propertyPaneStyleConfig = widget.getPropertyPaneStyleConfig(); - expect(isNotFloat(defaults.rows)).toBe(true); - expect(isNotFloat(defaults.columns)).toBe(true); - }); + expect( + validatePropertyPaneConfig( + propertyPaneStyleConfig, + !!config.isDeprecated, + ), + ).toStrictEqual(true); + }); + it(`Check if ${widget.type}'s dimensions are always integers`, () => { + const defaults = widget.getDefaults(); - if (config.isDeprecated) { - it(`Check if ${widget.type}'s deprecation config has a proper replacement Widget`, () => { - const widgetType = widget.type; + expect(isNotFloat(defaults.rows)).toBe(true); + expect(isNotFloat(defaults.columns)).toBe(true); + }); - if (config.replacement === undefined) { - fail(`${widgetType}'s replacement widget is not defined`); - } + if (config.isDeprecated) { + it(`Check if ${widget.type}'s deprecation config has a proper replacement Widget`, () => { + const widgetType = widget.type; - const replacementWidgetType = config.replacement; - const replacementWidget = WidgetFactory.get(replacementWidgetType); - const replacementWidgetConfig = replacementWidget?.getConfig(); + if (config.replacement === undefined) { + fail(`${widgetType}'s replacement widget is not defined`); + } - if (replacementWidgetConfig === undefined) { - fail( - `${widgetType}'s replacement widget ${replacementWidgetType} does not resolve to an actual widget Config`, - ); - } + const replacementWidgetType = config.replacement; + const replacementWidget = WidgetFactory.get(replacementWidgetType); + const replacementWidgetConfig = replacementWidget?.getConfig(); - if (replacementWidgetConfig?.isDeprecated) { - fail( - `${widgetType}'s replacement widget ${replacementWidgetType} itself is deprecated. Cannot have a deprecated widget as a replacement for another deprecated widget`, - ); - } + if (replacementWidgetConfig === undefined) { + fail( + `${widgetType}'s replacement widget ${replacementWidgetType} does not resolve to an actual widget Config`, + ); + } - if (replacementWidgetConfig?.hideCard) { - fail( - `${widgetType}'s replacement widget ${replacementWidgetType} should be available in the entity Explorer`, - ); + if (replacementWidgetConfig?.isDeprecated) { + fail( + `${widgetType}'s replacement widget ${replacementWidgetType} itself is deprecated. Cannot have a deprecated widget as a replacement for another deprecated widget`, + ); + } + + if (replacementWidgetConfig?.hideCard) { + fail( + `${widgetType}'s replacement widget ${replacementWidgetType} should be available in the entity Explorer`, + ); + } + }); + } + + it(`Check if ${widget.type}'s setter method are configured correctly`, () => { + const setterConfig = widget.getSetterConfig(); + + if (setterConfig) { + expect(setterConfig).toHaveProperty("__setters"); + const setters = setterConfig.__setters; + + for (const [setterName, config] of Object.entries(setters)) { + expect(config).toHaveProperty("type"); + expect(config).toHaveProperty("path"); + expect(setterName).toContain("set"); + const type = config.type; + const path = config.path; + + expect(typeof type).toBe("string"); + expect(typeof path).toBe("string"); + } } }); - } - - it(`Check if ${widget.type}'s setter method are configured correctly`, () => { - const setterConfig = widget.getSetterConfig(); - - if (setterConfig) { - expect(setterConfig).toHaveProperty("__setters"); - const setters = setterConfig.__setters; - - for (const [setterName, config] of Object.entries(setters)) { - expect(config).toHaveProperty("type"); - expect(config).toHaveProperty("path"); - expect(setterName).toContain("set"); - const type = config.type; - const path = config.path; - - expect(typeof type).toBe("string"); - expect(typeof path).toBe("string"); - } - } }); - }); + }); }); diff --git a/app/client/src/widgets/index.ts b/app/client/src/widgets/index.ts index 945b306269..fc83e4e27a 100644 --- a/app/client/src/widgets/index.ts +++ b/app/client/src/widgets/index.ts @@ -1,205 +1,463 @@ -import AudioRecorderWidget from "./AudioRecorderWidget"; -import AudioWidget from "./AudioWidget"; -import ButtonGroupWidget from "./ButtonGroupWidget"; -import ButtonWidget from "./ButtonWidget"; -import SelectWidget from "./SelectWidget"; -import CameraWidget from "./CameraWidget"; -import CanvasWidget from "./CanvasWidget"; -import ChartWidget from "./ChartWidget"; -import CheckboxGroupWidget from "./CheckboxGroupWidget"; -import CheckboxWidget from "./CheckboxWidget"; -import CircularProgressWidget from "./CircularProgressWidget"; -import ContainerWidget from "./ContainerWidget"; -import CurrencyInputWidget from "./CurrencyInputWidget"; -import DatePickerWidget from "./DatePickerWidget"; -import DatePickerWidget2 from "./DatePickerWidget2"; -import DividerWidget from "./DividerWidget"; -import MultiSelectWidgetV2 from "./MultiSelectWidgetV2"; -import DocumentViewerWidget from "./DocumentViewerWidget"; -import DropdownWidget from "./DropdownWidget"; -import FilePickerWidget from "./FilepickerWidget"; -import FilePickerWidgetV2 from "./FilePickerWidgetV2"; -import FormButtonWidget from "./FormButtonWidget"; -import FormWidget from "./FormWidget"; -import IconButtonWidget from "./IconButtonWidget"; -import IconWidget from "./IconWidget"; -import IframeWidget from "./IframeWidget"; -import ImageWidget from "./ImageWidget"; -import InputWidget from "./InputWidget"; -import InputWidgetV2 from "./InputWidgetV2"; -import ListWidget from "./ListWidget"; -import MapChartWidget from "./MapChartWidget"; -import MapWidget from "./MapWidget"; -import MenuButtonWidget from "./MenuButtonWidget"; -import ModalWidget from "./ModalWidget"; -import MultiSelectTreeWidget from "./MultiSelectTreeWidget"; -import MultiSelectWidget from "./MultiSelectWidget"; -import PhoneInputWidget from "./PhoneInputWidget"; -import ProgressBarWidget from "./ProgressBarWidget"; -import RadioGroupWidget from "./RadioGroupWidget"; -import RateWidget from "./RateWidget"; -import RichTextEditorWidget from "./RichTextEditorWidget"; -import SingleSelectTreeWidget from "./SingleSelectTreeWidget"; -import SkeletonWidget from "./SkeletonWidget"; -import StatboxWidget from "./StatboxWidget"; -import JSONFormWidget from "./JSONFormWidget"; -import SwitchGroupWidget from "./SwitchGroupWidget"; -import SwitchWidget from "./SwitchWidget"; -import TableWidget from "./TableWidget"; -import TabsMigratorWidget from "./TabsMigrator"; -import TabsWidget from "./TabsWidget"; -import TextWidget from "./TextWidget"; -import VideoWidget from "./VideoWidget"; -import ProgressWidget from "./ProgressWidget"; -import TableWidgetV2 from "./TableWidgetV2"; -import NumberSliderWidget from "./NumberSliderWidget"; -import RangeSliderWidget from "./RangeSliderWidget"; -import CategorySliderWidget from "./CategorySliderWidget"; -import CodeScannerWidget from "./CodeScannerWidget"; -import ListWidgetV2 from "./ListWidgetV2"; -import { WDSButtonWidget } from "widgets/wds/WDSButtonWidget"; -import { WDSInputWidget } from "widgets/wds/WDSInputWidget"; -import { WDSCheckboxWidget } from "widgets/wds/WDSCheckboxWidget"; -import { WDSIconButtonWidget } from "widgets/wds/WDSIconButtonWidget"; import type BaseWidget from "./BaseWidget"; -import ExternalWidget from "./ExternalWidget"; -import { WDSTableWidget } from "widgets/wds/WDSTableWidget"; -import { WDSCurrencyInputWidget } from "widgets/wds/WDSCurrencyInputWidget"; -import { WDSToolbarButtonsWidget } from "widgets/wds/WDSToolbarButtonsWidget"; -import { WDSPhoneInputWidget } from "widgets/wds/WDSPhoneInputWidget"; -import { WDSCheckboxGroupWidget } from "widgets/wds/WDSCheckboxGroupWidget"; -import { WDSComboBoxWidget } from "widgets/wds/WDSComboBoxWidget"; -import { WDSSwitchWidget } from "widgets/wds/WDSSwitchWidget"; -import { WDSSwitchGroupWidget } from "widgets/wds/WDSSwitchGroupWidget"; -import { WDSRadioGroupWidget } from "widgets/wds/WDSRadioGroupWidget"; -import { WDSMenuButtonWidget } from "widgets/wds/WDSMenuButtonWidget"; -import CustomWidget from "./CustomWidget"; -import { WDSSectionWidget } from "widgets/wds/WDSSectionWidget"; -import { WDSZoneWidget } from "widgets/wds/WDSZoneWidget"; -import { WDSHeadingWidget } from "widgets/wds/WDSHeadingWidget"; -import { WDSParagraphWidget } from "widgets/wds/WDSParagraphWidget"; -import { WDSModalWidget } from "widgets/wds/WDSModalWidget"; -import { WDSStatsWidget } from "widgets/wds/WDSStatsWidget"; -import { WDSKeyValueWidget } from "widgets/wds/WDSKeyValueWidget"; -import { WDSInlineButtonsWidget } from "widgets/wds/WDSInlineButtonsWidget"; -import { WDSEmailInputWidget } from "widgets/wds/WDSEmailInputWidget"; -import { WDSPasswordInputWidget } from "widgets/wds/WDSPasswordInputWidget"; -import { WDSNumberInputWidget } from "widgets/wds/WDSNumberInputWidget"; -import { WDSMultilineInputWidget } from "widgets/wds/WDSMultilineInputWidget"; -import { WDSSelectWidget } from "widgets/wds/WDSSelectWidget"; -import { WDSCustomWidget } from "widgets/wds/WDSCustomWidget"; +import { retryPromise } from "utils/AppsmithUtils"; +import { anvilWidgets } from "./wds/constants"; import { EEWDSWidgets } from "ee/widgets/wds"; -import { WDSDatePickerWidget } from "widgets/wds/WDSDatePickerWidget"; -import { WDSMultiSelectWidget } from "widgets/wds/WDSMultiSelectWidget"; import { EEWidgets } from "ee/widgets"; -const LegacyWidgets = [ - CanvasWidget, - SkeletonWidget, - ContainerWidget, - TextWidget, - TableWidget, - CheckboxWidget, - RadioGroupWidget, - ButtonWidget, - ImageWidget, - VideoWidget, - TabsWidget, - ModalWidget, - ChartWidget, - MapWidget, - RichTextEditorWidget, - DatePickerWidget2, - SwitchWidget, - FormWidget, - RateWidget, - IframeWidget, - TabsMigratorWidget, - DividerWidget, - MenuButtonWidget, - IconButtonWidget, - CheckboxGroupWidget, - FilePickerWidgetV2, - StatboxWidget, - AudioRecorderWidget, - DocumentViewerWidget, - ButtonGroupWidget, - MultiSelectTreeWidget, - SingleSelectTreeWidget, - SwitchGroupWidget, - AudioWidget, - ProgressBarWidget, - CameraWidget, - MapChartWidget, - SelectWidget, - MultiSelectWidgetV2, - InputWidgetV2, - PhoneInputWidget, - CurrencyInputWidget, - JSONFormWidget, - TableWidgetV2, - NumberSliderWidget, - RangeSliderWidget, - CategorySliderWidget, - CodeScannerWidget, - ListWidgetV2, - ExternalWidget, -]; - -const DeprecatedWidgets = [ - //Deprecated Widgets - InputWidget, - DropdownWidget, - DatePickerWidget, - IconWidget, - FilePickerWidget, - MultiSelectWidget, - FormButtonWidget, - ProgressWidget, - CircularProgressWidget, - ListWidget, -]; - -const WDSWidgets = [ - WDSButtonWidget, - WDSInputWidget, - WDSCheckboxWidget, - WDSIconButtonWidget, - WDSTableWidget, - WDSCurrencyInputWidget, - WDSToolbarButtonsWidget, - WDSPhoneInputWidget, - WDSCheckboxGroupWidget, - WDSComboBoxWidget, - WDSSwitchWidget, - WDSSwitchGroupWidget, - WDSRadioGroupWidget, - WDSMenuButtonWidget, - CustomWidget, - WDSSectionWidget, - WDSZoneWidget, - WDSParagraphWidget, - WDSHeadingWidget, - WDSModalWidget, - WDSStatsWidget, - WDSKeyValueWidget, - WDSInlineButtonsWidget, - WDSEmailInputWidget, - WDSPasswordInputWidget, - WDSNumberInputWidget, - WDSMultilineInputWidget, - WDSSelectWidget, - WDSDatePickerWidget, - WDSCustomWidget, - WDSMultiSelectWidget, -]; - -const Widgets = [ - ...WDSWidgets, - ...DeprecatedWidgets, - ...LegacyWidgets, +// Create widget loader map +const WidgetLoaders = new Map Promise>([ ...EEWDSWidgets, ...EEWidgets, -] as (typeof BaseWidget)[]; + // WDS Widgets + [ + "WDS_BUTTON_WIDGET", + async () => + import("widgets/wds/WDSButtonWidget").then((m) => m.WDSButtonWidget), + ], + [ + "WDS_INPUT_WIDGET", + async () => + import("widgets/wds/WDSInputWidget").then((m) => m.WDSInputWidget), + ], + [ + "WDS_CHECKBOX_WIDGET", + async () => + import("widgets/wds/WDSCheckboxWidget").then((m) => m.WDSCheckboxWidget), + ], + [ + "WDS_ICON_BUTTON_WIDGET", + async () => + import("widgets/wds/WDSIconButtonWidget").then( + (m) => m.WDSIconButtonWidget, + ), + ], + [ + "WDS_TABLE_WIDGET", + async () => + import("widgets/wds/WDSTableWidget").then((m) => m.WDSTableWidget), + ], + [ + "WDS_CURRENCY_INPUT_WIDGET", + async () => + import("widgets/wds/WDSCurrencyInputWidget").then( + (m) => m.WDSCurrencyInputWidget, + ), + ], + [ + "WDS_TOOLBAR_BUTTONS_WIDGET", + async () => + import("widgets/wds/WDSToolbarButtonsWidget").then( + (m) => m.WDSToolbarButtonsWidget, + ), + ], + [ + "WDS_PHONE_INPUT_WIDGET", + async () => + import("widgets/wds/WDSPhoneInputWidget").then( + (m) => m.WDSPhoneInputWidget, + ), + ], + [ + "WDS_CHECKBOX_GROUP_WIDGET", + async () => + import("widgets/wds/WDSCheckboxGroupWidget").then( + (m) => m.WDSCheckboxGroupWidget, + ), + ], + [ + "WDS_COMBO_BOX_WIDGET", + async () => + import("widgets/wds/WDSComboBoxWidget").then((m) => m.WDSComboBoxWidget), + ], + [ + "WDS_SWITCH_WIDGET", + async () => + import("widgets/wds/WDSSwitchWidget").then((m) => m.WDSSwitchWidget), + ], + [ + "WDS_SWITCH_GROUP_WIDGET", + async () => + import("widgets/wds/WDSSwitchGroupWidget").then( + (m) => m.WDSSwitchGroupWidget, + ), + ], + [ + "WDS_RADIO_GROUP_WIDGET", + async () => + import("widgets/wds/WDSRadioGroupWidget").then( + (m) => m.WDSRadioGroupWidget, + ), + ], + [ + "WDS_MENU_BUTTON_WIDGET", + async () => + import("widgets/wds/WDSMenuButtonWidget").then( + (m) => m.WDSMenuButtonWidget, + ), + ], + [ + "CUSTOM_WIDGET", + async () => import("./CustomWidget").then((m) => m.default), + ], + [ + anvilWidgets.SECTION_WIDGET, + async () => + import("widgets/wds/WDSSectionWidget").then((m) => m.WDSSectionWidget), + ], + [ + anvilWidgets.ZONE_WIDGET, + async () => + import("widgets/wds/WDSZoneWidget").then((m) => m.WDSZoneWidget), + ], + [ + "WDS_PARAGRAPH_WIDGET", + async () => + import("widgets/wds/WDSParagraphWidget").then( + (m) => m.WDSParagraphWidget, + ), + ], + [ + "WDS_HEADING_WIDGET", + async () => + import("widgets/wds/WDSHeadingWidget").then((m) => m.WDSHeadingWidget), + ], + [ + "WDS_MODAL_WIDGET", + async () => + import("widgets/wds/WDSModalWidget").then((m) => m.WDSModalWidget), + ], + [ + "WDS_STATS_WIDGET", + async () => + import("widgets/wds/WDSStatsWidget").then((m) => m.WDSStatsWidget), + ], + [ + "WDS_KEY_VALUE_WIDGET", + async () => + import("widgets/wds/WDSKeyValueWidget").then((m) => m.WDSKeyValueWidget), + ], + [ + "WDS_INLINE_BUTTONS_WIDGET", + async () => + import("widgets/wds/WDSInlineButtonsWidget").then( + (m) => m.WDSInlineButtonsWidget, + ), + ], + [ + "WDS_EMAIL_INPUT_WIDGET", + async () => + import("widgets/wds/WDSEmailInputWidget").then( + (m) => m.WDSEmailInputWidget, + ), + ], + [ + "WDS_PASSWORD_INPUT_WIDGET", + async () => + import("widgets/wds/WDSPasswordInputWidget").then( + (m) => m.WDSPasswordInputWidget, + ), + ], + [ + "WDS_NUMBER_INPUT_WIDGET", + async () => + import("widgets/wds/WDSNumberInputWidget").then( + (m) => m.WDSNumberInputWidget, + ), + ], + [ + "WDS_MULTILINE_INPUT_WIDGET", + async () => + import("widgets/wds/WDSMultilineInputWidget").then( + (m) => m.WDSMultilineInputWidget, + ), + ], + [ + "WDS_SELECT_WIDGET", + async () => + import("widgets/wds/WDSSelectWidget").then((m) => m.WDSSelectWidget), + ], + [ + "WDS_DATEPICKER_WIDGET", + async () => + import("widgets/wds/WDSDatePickerWidget").then( + (m) => m.WDSDatePickerWidget, + ), + ], + [ + "WDS_MULTI_SELECT_WIDGET", + async () => + import("widgets/wds/WDSMultiSelectWidget").then( + (m) => m.WDSMultiSelectWidget, + ), + ], -export default Widgets; + // Legacy Widgets + [ + "CANVAS_WIDGET", + async () => import("./CanvasWidget").then((m) => m.default), + ], + [ + "SKELETON_WIDGET", + async () => import("./SkeletonWidget").then((m) => m.default), + ], + [ + "CONTAINER_WIDGET", + async () => import("./ContainerWidget").then((m) => m.default), + ], + ["TEXT_WIDGET", async () => import("./TextWidget").then((m) => m.default)], + ["TABLE_WIDGET", async () => import("./TableWidget").then((m) => m.default)], + [ + "CHECKBOX_WIDGET", + async () => import("./CheckboxWidget").then((m) => m.default), + ], + [ + "RADIO_GROUP_WIDGET", + async () => import("./RadioGroupWidget").then((m) => m.default), + ], + [ + "BUTTON_WIDGET", + async () => import("./ButtonWidget").then((m) => m.default), + ], + ["IMAGE_WIDGET", async () => import("./ImageWidget").then((m) => m.default)], + ["VIDEO_WIDGET", async () => import("./VideoWidget").then((m) => m.default)], + ["TABS_WIDGET", async () => import("./TabsWidget").then((m) => m.default)], + ["MODAL_WIDGET", async () => import("./ModalWidget").then((m) => m.default)], + ["CHART_WIDGET", async () => import("./ChartWidget").then((m) => m.default)], + ["MAP_WIDGET", async () => import("./MapWidget").then((m) => m.default)], + [ + "RICH_TEXT_EDITOR_WIDGET", + async () => import("./RichTextEditorWidget").then((m) => m.default), + ], + [ + "DATE_PICKER_WIDGET2", + async () => import("./DatePickerWidget2").then((m) => m.default), + ], + [ + "SWITCH_WIDGET", + async () => import("./SwitchWidget").then((m) => m.default), + ], + ["FORM_WIDGET", async () => import("./FormWidget").then((m) => m.default)], + ["RATE_WIDGET", async () => import("./RateWidget").then((m) => m.default)], + [ + "IFRAME_WIDGET", + async () => import("./IframeWidget").then((m) => m.default), + ], + [ + "TABS_MIGRATOR_WIDGET", + async () => import("./TabsMigrator").then((m) => m.default), + ], + [ + "DIVIDER_WIDGET", + async () => import("./DividerWidget").then((m) => m.default), + ], + [ + "MENU_BUTTON_WIDGET", + async () => import("./MenuButtonWidget").then((m) => m.default), + ], + [ + "ICON_BUTTON_WIDGET", + async () => import("./IconButtonWidget").then((m) => m.default), + ], + [ + "CHECKBOX_GROUP_WIDGET", + async () => import("./CheckboxGroupWidget").then((m) => m.default), + ], + [ + "FILE_PICKER_WIDGET_V2", + async () => import("./FilePickerWidgetV2").then((m) => m.default), + ], + [ + "STATBOX_WIDGET", + async () => import("./StatboxWidget").then((m) => m.default), + ], + [ + "AUDIO_RECORDER_WIDGET", + async () => import("./AudioRecorderWidget").then((m) => m.default), + ], + [ + "DOCUMENT_VIEWER_WIDGET", + async () => import("./DocumentViewerWidget").then((m) => m.default), + ], + [ + "BUTTON_GROUP_WIDGET", + async () => import("./ButtonGroupWidget").then((m) => m.default), + ], + [ + "WDS_CUSTOM_WIDGET", + async () => + import("widgets/wds/WDSCustomWidget").then((m) => m.WDSCustomWidget), + ], + [ + "MULTI_SELECT_TREE_WIDGET", + async () => import("./MultiSelectTreeWidget").then((m) => m.default), + ], + [ + "SINGLE_SELECT_TREE_WIDGET", + async () => import("./SingleSelectTreeWidget").then((m) => m.default), + ], + [ + "SWITCH_GROUP_WIDGET", + async () => import("./SwitchGroupWidget").then((m) => m.default), + ], + ["AUDIO_WIDGET", async () => import("./AudioWidget").then((m) => m.default)], + [ + "PROGRESSBAR_WIDGET", + async () => import("./ProgressBarWidget").then((m) => m.default), + ], + [ + "CAMERA_WIDGET", + async () => import("./CameraWidget").then((m) => m.default), + ], + [ + "MAP_CHART_WIDGET", + async () => import("./MapChartWidget").then((m) => m.default), + ], + [ + "SELECT_WIDGET", + async () => import("./SelectWidget").then((m) => m.default), + ], + [ + "MULTI_SELECT_WIDGET_V2", + async () => import("./MultiSelectWidgetV2").then((m) => m.default), + ], + [ + "MULTI_SELECT_WIDGET", + async () => import("./MultiSelectWidget").then((m) => m.default), + ], + [ + "INPUT_WIDGET_V2", + async () => import("./InputWidgetV2").then((m) => m.default), + ], + [ + "PHONE_INPUT_WIDGET", + async () => import("./PhoneInputWidget").then((m) => m.default), + ], + [ + "CURRENCY_INPUT_WIDGET", + async () => import("./CurrencyInputWidget").then((m) => m.default), + ], + [ + "JSON_FORM_WIDGET", + async () => import("./JSONFormWidget").then((m) => m.default), + ], + [ + "TABLE_WIDGET_V2", + async () => import("./TableWidgetV2").then((m) => m.default), + ], + [ + "NUMBER_SLIDER_WIDGET", + async () => import("./NumberSliderWidget").then((m) => m.default), + ], + [ + "RANGE_SLIDER_WIDGET", + async () => import("./RangeSliderWidget").then((m) => m.default), + ], + [ + "CATEGORY_SLIDER_WIDGET", + async () => import("./CategorySliderWidget").then((m) => m.default), + ], + [ + "CODE_SCANNER_WIDGET", + async () => import("./CodeScannerWidget").then((m) => m.default), + ], + [ + "LIST_WIDGET_V2", + async () => import("./ListWidgetV2").then((m) => m.default), + ], + [ + "EXTERNAL_WIDGET", + async () => import("./ExternalWidget").then((m) => m.default), + ], + + // Deprecated Widgets + [ + "DROP_DOWN_WIDGET", + async () => import("./DropdownWidget").then((m) => m.default), + ], + ["ICON_WIDGET", async () => import("./IconWidget").then((m) => m.default)], + [ + "FILE_PICKER_WIDGET", + async () => import("./FilepickerWidget").then((m) => m.default), + ], + [ + "FORM_BUTTON_WIDGET", + async () => import("./FormButtonWidget").then((m) => m.default), + ], + [ + "PROGRESS_WIDGET", + async () => import("./ProgressWidget").then((m) => m.default), + ], + [ + "CIRCULAR_PROGRESS_WIDGET", + async () => import("./CircularProgressWidget").then((m) => m.default), + ], + ["LIST_WIDGET", async () => import("./ListWidget").then((m) => m.default)], + [ + "DATE_PICKER_WIDGET", + async () => import("./DatePickerWidget").then((m) => m.default), + ], + ["INPUT_WIDGET", async () => import("./InputWidget").then((m) => m.default)], +]); + +// Cache for loaded widgets +const loadedWidgets = new Map(); + +// Function to load a specific widget by type +export const loadWidget = async (type: string): Promise => { + if (loadedWidgets.has(type)) { + return loadedWidgets.get(type)!; + } + + const loader = WidgetLoaders.get(type); + + if (!loader) { + throw new Error(`Widget type ${type} not found`); + } + + try { + const widget = await retryPromise(async () => loader()); + + loadedWidgets.set(type, widget); + + return widget; + } catch (error) { + throw new Error(`Error loading widget ${type}:` + error); + } +}; + +// Function to load all widgets +// Function to load all widgets +export const loadAllWidgets = async (): Promise< + Map +> => { + const allWidgets = new Map(); + + const widgetPromises = Array.from(WidgetLoaders.entries()).map( + async ([type, loader]) => { + if (loadedWidgets.has(type)) { + return [type, loadedWidgets.get(type)!] as [string, typeof BaseWidget]; + } + + try { + const widget = await retryPromise(async () => loader()); + + loadedWidgets.set(type, widget); + + return [type, widget] as [string, typeof BaseWidget]; + } catch (error) { + throw new Error( + `Failed to load widget type ${type}: ${error instanceof Error ? error.message : error}`, + ); + } + }, + ); + + const loadedWidgetEntries = await Promise.all(widgetPromises); + + for (const [type, widget] of loadedWidgetEntries) { + allWidgets.set(type, widget); + } + + return allWidgets; +}; +export default WidgetLoaders; diff --git a/app/client/src/workers/Evaluation/asyncWorkerActions.ts b/app/client/src/workers/Evaluation/asyncWorkerActions.ts index 20329ba23b..214f08b26a 100644 --- a/app/client/src/workers/Evaluation/asyncWorkerActions.ts +++ b/app/client/src/workers/Evaluation/asyncWorkerActions.ts @@ -4,9 +4,13 @@ import { evalWorker } from "utils/workerInstances"; import { EVAL_WORKER_ACTIONS } from "ee/workers/Evaluation/evalWorkerActions"; import { runSaga } from "redux-saga"; import { TriggerKind } from "constants/AppsmithActionConstants/ActionConstants"; +import { registerAllWidgets } from "utils/editor/EditorUtils"; export async function UNSTABLE_executeDynamicTrigger(dynamicTrigger: string) { const state = store.getState(); + + await registerAllWidgets(); + const unEvalTree = getUnevaluatedDataTree(state); const result = runSaga( diff --git a/app/client/src/workers/Evaluation/handlers/jsLibrary.ts b/app/client/src/workers/Evaluation/handlers/jsLibrary.ts index 56821485f1..9a100e7376 100644 --- a/app/client/src/workers/Evaluation/handlers/jsLibrary.ts +++ b/app/client/src/workers/Evaluation/handlers/jsLibrary.ts @@ -290,75 +290,64 @@ export async function loadLibraries( const libStore: Record = {}; try { - for (const lib of libs) { - const url = lib.url as string; - const accessors = lib.accessor; - const keysBefore = Object.keys(self); - let module = null; + await Promise.all( + libs.map(async (lib) => { + const url = lib.url as string; + const accessors = lib.accessor; + const keysBefore = Object.keys(self); + let module = null; - try { - self.importScripts(url); - const keysAfter = Object.keys(self); - let defaultAccessors = difference(keysAfter, keysBefore); + try { + self.importScripts(url); + const keysAfter = Object.keys(self); + let defaultAccessors = difference(keysAfter, keysBefore); - // Changing default export to library accessors name which was saved when it was installed, if default export present - movetheDefaultExportedLibraryToAccessorKey( - defaultAccessors, - accessors[0], - ); + movetheDefaultExportedLibraryToAccessorKey( + defaultAccessors, + accessors[0], + ); - // Following the same process which was happening earlier - const keysAfterDefaultOperation = Object.keys(self); + const keysAfterDefaultOperation = Object.keys(self); - defaultAccessors = difference(keysAfterDefaultOperation, keysBefore); + defaultAccessors = difference(keysAfterDefaultOperation, keysBefore); - /** - * Installing 2 different version of lodash tries to add the same accessor on the self object. Let take version a & b for example. - * Installation of version a, will add _ to the self object and can be detected by looking at the differences in the previous step. - * Now when version b is installed, differences will be [], since _ already exists in the self object. - * We add all the installations to the libStore and see if the reference it points to in the self object changes. - * If the references changes it means that it a valid accessor. - */ - defaultAccessors.push( - ...Object.keys(libStore).filter((k) => libStore[k] !== self[k]), - ); + defaultAccessors.push( + ...Object.keys(libStore).filter((k) => libStore[k] !== self[k]), + ); - /** - * Sort the accessor list from backend and installed accessor list using the same rule to apply all modifications. - * This is required only for UMD builds, since we always generate unique names for ESM. - */ - accessors.sort(); - defaultAccessors.sort(); + accessors.sort(); + defaultAccessors.sort(); - for (let i = 0; i < defaultAccessors.length; i++) { - self[accessors[i]] = self[defaultAccessors[i]]; - libStore[defaultAccessors[i]] = self[defaultAccessors[i]]; - libraryReservedIdentifiers[accessors[i]] = true; - invalidEntityIdentifiers[accessors[i]] = true; + for (let i = 0; i < defaultAccessors.length; i++) { + self[accessors[i]] = self[defaultAccessors[i]]; + libStore[defaultAccessors[i]] = self[defaultAccessors[i]]; + libraryReservedIdentifiers[accessors[i]] = true; + invalidEntityIdentifiers[accessors[i]] = true; + } + + return; + } catch (e) { + log.debug(e); } - continue; - } catch (e) { - log.debug(e); - } + try { + module = await import(/* webpackIgnore: true */ url); - try { - module = await import(/* webpackIgnore: true */ url); + if (!module || typeof module !== "object") throw "Not an ESM module"; - if (!module || typeof module !== "object") throw "Not an ESM module"; + const key = accessors[0]; + const flattenedModule = flattenModule(module); - const key = accessors[0]; - const flattenedModule = flattenModule(module); - - libStore[key] = flattenedModule; - self[key] = flattenedModule; - libraryReservedIdentifiers[key] = true; - invalidEntityIdentifiers[key] = true; - } catch (e) { - log.debug(e); - throw new ImportError(url); - } - } + libStore[key] = flattenedModule; + self[key] = flattenedModule; + libraryReservedIdentifiers[key] = true; + invalidEntityIdentifiers[key] = true; + } catch (e) { + log.debug(e); + throw new ImportError(url); + } + }), + ); JSLibraries.push(...libs); JSLibraryAccessor.regenerateSet(); diff --git a/app/client/src/workers/common/DataTreeEvaluator/dataTreeEvaluator.test.ts b/app/client/src/workers/common/DataTreeEvaluator/dataTreeEvaluator.test.ts index f370792e0d..f08cb4373a 100644 --- a/app/client/src/workers/common/DataTreeEvaluator/dataTreeEvaluator.test.ts +++ b/app/client/src/workers/common/DataTreeEvaluator/dataTreeEvaluator.test.ts @@ -14,7 +14,7 @@ import { import { updateDependencyMap } from "workers/common/DependencyMap"; import { replaceThisDotParams } from "./utils"; import { isDataField } from "./utils"; -import widgets from "widgets"; +import { loadAllWidgets } from "widgets"; import type { WidgetConfiguration } from "WidgetProvider/types"; import { type WidgetEntity } from "ee/entities/DataTree/types"; import { @@ -35,14 +35,18 @@ const widgetConfigMap: Record< } > = {}; -widgets.map((widget) => { - if (widget.type) { - widgetConfigMap[widget.type] = { - defaultProperties: widget.getDefaultPropertiesMap(), - derivedProperties: widget.getDerivedPropertiesMap(), - metaProperties: widget.getMetaPropertiesMap(), - }; - } +beforeAll(async () => { + const loadedWidgets = await loadAllWidgets(); + + loadedWidgets.forEach((widget) => { + if (widget.type) { + widgetConfigMap[widget.type] = { + defaultProperties: widget.getDefaultPropertiesMap(), + derivedProperties: widget.getDerivedPropertiesMap(), + metaProperties: widget.getMetaPropertiesMap(), + }; + } + }); }); jest.mock("ee/workers/Evaluation/generateOverrideContext"); // mock the generateOverrideContext function