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 000000000000..62bc519f2a63 --- /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 1dbc737333f5..2e8ff09946dd 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 19fec2ef568b..41c31ce3bfee 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(); + widgets.forEach((widget) => { + registerWidget(widget); + }); + // Increment version to trigger selectors that depend on widget configs + incrementWidgetConfigsVersion(); +}; - // 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, - ); +export const registerWidget = (widget: typeof BaseWidget) => { + const { eagerRender = false, needsMeta = false } = widget.getConfig(); - return [ - widget, - (widgetProps: CanvasWidgetStructure) => ( - - ), - ] as [ - typeof BaseWidget, - (widgetProps: CanvasWidgetStructure) => React.ReactNode, - ]; - }); + // 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(widgetAndBuilders); + 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 000000000000..733ef017587a --- /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 c96972e54b3c..fa032a119651 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 4857b05ea447..d2a34b9ff72b 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 028a3dc491fe..36f44caa2a15 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 5cb332515145..1ad3dc091804 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 28555bcaa9f9..53902eb7f691 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 f26879d7744b..ff39a4f31706 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 6faf7c4025b7..4daf523d00f0 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 ef44f1f97cf9..0b84a4bf9bda 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 9fd5485026a0..36d07dc0ef2c 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 287bfc5b7dea..a7b090d8b7b2 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 9e1dd6f5809e..8bcda421dee4 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 b9728ba94fcd..117782f2e06e 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 3f03b317d892..df515323ef2a 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 10add2e2f1e9..d1e9736e1b99 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 000000000000..ff43e257e929 --- /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 4e4540abbf13..81aa5ff01302 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 88b281f38cea..75c268d1aa09 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 f37629c0f316..0cb1ea8d1be7 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 49dae0c20078..dcfab0a378ad 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,34 +880,73 @@ export function* evalAndLintingHandler( yield all(effects); endSpan(span); } +export function* loadAndRegisterOnlyCanvasWidgets(): Generator< + Effect, + (typeof BaseWidget)[], + unknown +> { + try { + const widgetTypes = (yield select(getUsedWidgetTypes)) as string[]; -// 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); + const uiModuleTypes = (yield select( + getAllUniqueWidgetTypesInUiModules, + )) as string[]; - // 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 uniqueWidgetTypes = Array.from( + new Set([...uiModuleTypes, ...widgetTypes, "SKELETON_WIDGET"]), + ); - const isFFFetched = yield select(getFeatureFlagsFetched); + // Filter out already registered widget types + const unregisteredWidgetTypes = uniqueWidgetTypes.filter( + (type: string) => !WidgetFactory.widgetsMap.has(type), + ); - if (!isFFFetched) { - yield call(fetchFeatureFlagsInit); - yield take(ReduxActionTypes.FETCH_FEATURE_FLAGS_SUCCESS); + 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; } +} - const featureFlags: Record = - yield select(selectFeatureFlags); +// 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); - yield call(evalWorker.request, EVAL_WORKER_ACTIONS.SETUP, { - cloudHosting: !!APPSMITH_CONFIGS.cloudHosting, - featureFlags: featureFlags, - }); - yield spawn(handleEvalWorkerRequestSaga, evalWorkerListenerChannel); + const initializeJSLibrariesChannel = yield actionChannel( + ReduxActionTypes.DEFER_LOADING_JS_LIBRARIES, + ); + const appMode = yield select(getAppMode); + + let jsLibrariesTask: Task | undefined; + + // 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 initAction: EvaluationReduxAction = yield take( firstEvalActionChannel, @@ -804,16 +954,6 @@ function* evaluationChangeListenerSaga(): any { 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 4ada86731263..83699129de47 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 c368f4301ee1..c66e4c0d2d4c 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 286148872082..5dc52c29b4e9 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 000000000000..8c4a165fbe6b --- /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 8b196c71b1e2..a6c59e57c9cf 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 7941fc1154bb..a6f072f34940 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 d6d37b8e8a7e..cabf791ba2eb 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 138da365f228..a44f0950d398 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 21cfb9b46278..8b54b5fb908d 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)[] = []; - widgets - // Exclude WDS widgets from the tests, since they work differently - .filter((widget) => !widget.type.includes("WDS")) - .forEach((widget) => { - const config = widget.getConfig(); - - it(`Checks ${widget.type}'s propertyPaneConfig`, () => { - const propertyPaneConfig = widget.getPropertyPaneConfig(); - - expect( - validatePropertyPaneConfig(propertyPaneConfig, !!config.hideCard), - ).toStrictEqual(true); - const propertyPaneContentConfig = widget.getPropertyPaneContentConfig(); - - expect( - validatePropertyPaneConfig( - propertyPaneContentConfig, - !!config.isDeprecated, - ), - ).toStrictEqual(true); - const propertyPaneStyleConfig = widget.getPropertyPaneStyleConfig(); - - expect( - validatePropertyPaneConfig( - propertyPaneStyleConfig, - !!config.isDeprecated, - ), - ).toStrictEqual(true); - }); - it(`Check if ${widget.type}'s dimensions are always integers`, () => { - const defaults = widget.getDefaults(); + beforeAll(async () => { + // Load all widgets and convert Map to array + const widgetsMap = await loadAllWidgets(); - expect(isNotFloat(defaults.rows)).toBe(true); - expect(isNotFloat(defaults.columns)).toBe(true); - }); + widgetsArray = Array.from(widgetsMap.values()); - if (config.isDeprecated) { - it(`Check if ${widget.type}'s deprecation config has a proper replacement Widget`, () => { - const widgetType = widget.type; - - if (config.replacement === undefined) { - fail(`${widgetType}'s replacement widget is not defined`); - } - - const replacementWidgetType = config.replacement; - const replacementWidget = WidgetFactory.get(replacementWidgetType); - const replacementWidgetConfig = replacementWidget?.getConfig(); + // Register all widgets + registerWidgets(widgetsArray); + }); - if (replacementWidgetConfig === undefined) { - fail( - `${widgetType}'s replacement widget ${replacementWidgetType} does not resolve to an actual widget Config`, - ); - } + it("should have loaded widgets", () => { + expect(widgetsArray.length).toBeGreaterThan(0); + }); - if (replacementWidgetConfig?.isDeprecated) { - fail( - `${widgetType}'s replacement widget ${replacementWidgetType} itself is deprecated. Cannot have a deprecated widget as a replacement for another deprecated widget`, - ); - } + 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(); + + it(`Checks ${widget.type}'s propertyPaneConfig`, () => { + const propertyPaneConfig = widget.getPropertyPaneConfig(); + + expect( + validatePropertyPaneConfig(propertyPaneConfig, !!config.hideCard), + ).toStrictEqual(true); + const propertyPaneContentConfig = + widget.getPropertyPaneContentConfig(); + + expect( + validatePropertyPaneConfig( + propertyPaneContentConfig, + !!config.isDeprecated, + ), + ).toStrictEqual(true); + const propertyPaneStyleConfig = widget.getPropertyPaneStyleConfig(); + + expect( + validatePropertyPaneConfig( + propertyPaneStyleConfig, + !!config.isDeprecated, + ), + ).toStrictEqual(true); + }); + it(`Check if ${widget.type}'s dimensions are always integers`, () => { + const defaults = widget.getDefaults(); - if (replacementWidgetConfig?.hideCard) { - fail( - `${widgetType}'s replacement widget ${replacementWidgetType} should be available in the entity Explorer`, - ); - } + expect(isNotFloat(defaults.rows)).toBe(true); + expect(isNotFloat(defaults.columns)).toBe(true); }); - } - it(`Check if ${widget.type}'s setter method are configured correctly`, () => { - const setterConfig = widget.getSetterConfig(); + if (config.isDeprecated) { + it(`Check if ${widget.type}'s deprecation config has a proper replacement Widget`, () => { + const widgetType = widget.type; + + if (config.replacement === undefined) { + fail(`${widgetType}'s replacement widget is not defined`); + } + + const replacementWidgetType = config.replacement; + const replacementWidget = WidgetFactory.get(replacementWidgetType); + const replacementWidgetConfig = replacementWidget?.getConfig(); + + if (replacementWidgetConfig === undefined) { + fail( + `${widgetType}'s replacement widget ${replacementWidgetType} does not resolve to an actual widget Config`, + ); + } + + 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`, + ); + } + }); + } - if (setterConfig) { - expect(setterConfig).toHaveProperty("__setters"); - const setters = setterConfig.__setters; + it(`Check if ${widget.type}'s setter method are configured correctly`, () => { + const setterConfig = widget.getSetterConfig(); - 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; + if (setterConfig) { + expect(setterConfig).toHaveProperty("__setters"); + const setters = setterConfig.__setters; - expect(typeof type).toBe("string"); - expect(typeof path).toBe("string"); + 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 945b3062695a..fc83e4e27a71 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 20329ba23ba8..214f08b26ad0 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 56821485f172..9a100e7376e4 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); + + movetheDefaultExportedLibraryToAccessorKey( + defaultAccessors, + accessors[0], + ); - 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], - ); - - // Following the same process which was happening earlier - const keysAfterDefaultOperation = Object.keys(self); - - 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]), - ); - - /** - * 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(); - - 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; - } + const keysAfterDefaultOperation = Object.keys(self); - continue; - } catch (e) { - log.debug(e); - } + defaultAccessors = difference(keysAfterDefaultOperation, keysBefore); - try { - module = await import(/* webpackIgnore: true */ url); + defaultAccessors.push( + ...Object.keys(libStore).filter((k) => libStore[k] !== self[k]), + ); - if (!module || typeof module !== "object") throw "Not an ESM module"; + accessors.sort(); + defaultAccessors.sort(); - const key = accessors[0]; - const flattenedModule = flattenModule(module); + 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; + } - libStore[key] = flattenedModule; - self[key] = flattenedModule; - libraryReservedIdentifiers[key] = true; - invalidEntityIdentifiers[key] = true; - } catch (e) { - log.debug(e); - throw new ImportError(url); - } - } + return; + } catch (e) { + log.debug(e); + } + + try { + module = await import(/* webpackIgnore: true */ url); + + if (!module || typeof module !== "object") throw "Not an ESM 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); + } + }), + ); 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 f370792e0d15..f08cb4373a6a 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