chore: ce changes related to decoupling webworker (#41033)

## 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"

### 🔍 Cypress test results
<!-- This is an auto-generated comment: Cypress test results  -->
> [!TIP]
> 🟢 🟢 🟢 All cypress tests have passed! 🎉 🎉 🎉
> Workflow run:
<https://github.com/appsmithorg/appsmith/actions/runs/16202622510>
> Commit: b648036bd7b74ae742f5c5d7f6cfd770867a2828
> <a
href="https://internal.appsmith.com/app/cypress-dashboard/rundetails-65890b3c81d7400d08fa9ee5?branch=master&workflowId=16202622510&attempt=1"
target="_blank">Cypress dashboard</a>.
> Tags: `@tag.All`
> Spec:
> <hr>Thu, 10 Jul 2025 19:22:25 UTC
<!-- end of auto-generated comment: Cypress test results  -->


## Communication
Should the DevRel and Marketing teams inform users about this change?
- [ ] Yes
- [ ] No


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## 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.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Vemparala Surya Vamsi 2025-07-11 12:24:44 +05:30 committed by GitHub
parent 7282f64dcf
commit e5b2a26c65
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
36 changed files with 1262 additions and 513 deletions

View File

@ -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);
});
});

View File

@ -1,13 +1,47 @@
import memo from "micro-memoize"; 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<MemoizedWithClear>();
// 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( export function memoize(
target: unknown, target: unknown,
methodName: unknown, methodName: unknown,
descriptor: PropertyDescriptor, descriptor: PropertyDescriptor,
) { ) {
descriptor.value = memo(descriptor.value, { descriptor.value = memoizeWithClear(descriptor.value);
maxSize: 100,
});
} }
export function freeze( export function freeze(
@ -25,3 +59,8 @@ export function freeze(
return Object.freeze(result); return Object.freeze(result);
}; };
} }
// Function to clear all memoized caches
export function clearAllWidgetFactoryCache() {
memoizedFunctions.forEach((fn) => fn.clearCache());
}

View File

@ -3,6 +3,7 @@ import type { CanvasWidgetStructure } from "WidgetProvider/types";
import type BaseWidget from "widgets/BaseWidget"; import type BaseWidget from "widgets/BaseWidget";
import WidgetFactory from "."; import WidgetFactory from ".";
import { withBaseWidgetHOC } from "widgets/BaseWidgetHOC/withBaseWidgetHOC"; import { withBaseWidgetHOC } from "widgets/BaseWidgetHOC/withBaseWidgetHOC";
import { incrementWidgetConfigsVersion } from "./widgetConfigVersion";
/* /*
* Function to create builder for the widgets and register them in widget factory * 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. * extracted this into a seperate file to break the circular reference.
* *
*/ */
export const registerWidgets = (widgets: (typeof BaseWidget)[]) => { export const registerWidgets = (widgets: (typeof BaseWidget)[]) => {
const widgetAndBuilders = widgets.map((widget) => { widgets.forEach((widget) => {
const { eagerRender = false, needsMeta = false } = widget.getConfig(); registerWidget(widget);
// 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) => (
<ProfiledWidget {...widgetProps} key={widgetProps.widgetId} />
),
] as [
typeof BaseWidget,
(widgetProps: CanvasWidgetStructure) => React.ReactNode,
];
}); });
// Increment version to trigger selectors that depend on widget configs
WidgetFactory.initialize(widgetAndBuilders); 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) => (
<ProfiledWidget {...widgetProps} key={widgetProps.widgetId} />
),
];
WidgetFactory.initialize([widgetAndBuilder]);
}; };

View File

@ -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++;
};

View File

@ -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<JSLibrary>) { export function installLibraryInit(payload: Partial<JSLibrary>) {
return { return {
type: ReduxActionTypes.INSTALL_LIBRARY_INIT, type: ReduxActionTypes.INSTALL_LIBRARY_INIT,

View File

@ -13,6 +13,7 @@ import type {
ConditionalOutput, ConditionalOutput,
DynamicValues, DynamicValues,
} from "reducers/evaluationReducers/formEvaluationReducer"; } from "reducers/evaluationReducers/formEvaluationReducer";
import type { ReduxActionWithoutPayload } from "./ReduxActionTypes";
export const shouldTriggerEvaluation = (action: ReduxAction<unknown>) => { export const shouldTriggerEvaluation = (action: ReduxAction<unknown>) => {
return ( 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 // These actions require the entire tree to be re-evaluated
const FORCE_EVAL_ACTIONS = { const FORCE_EVAL_ACTIONS = {
[ReduxActionTypes.INSTALL_LIBRARY_SUCCESS]: true, [ReduxActionTypes.INSTALL_LIBRARY_SUCCESS]: true,

View File

@ -14,6 +14,7 @@ const JSLibraryActionTypes = {
TOGGLE_INSTALLER: "TOGGLE_INSTALLER", TOGGLE_INSTALLER: "TOGGLE_INSTALLER",
FETCH_JS_LIBRARIES_INIT: "FETCH_JS_LIBRARIES_INIT", FETCH_JS_LIBRARIES_INIT: "FETCH_JS_LIBRARIES_INIT",
FETCH_JS_LIBRARIES_SUCCESS: "FETCH_JS_LIBRARIES_SUCCESS", FETCH_JS_LIBRARIES_SUCCESS: "FETCH_JS_LIBRARIES_SUCCESS",
DEFER_LOADING_JS_LIBRARIES: "DEFER_LOADING_JS_LIBRARIES",
CLEAR_PROCESSED_INSTALLS: "CLEAR_PROCESSED_INSTALLS", CLEAR_PROCESSED_INSTALLS: "CLEAR_PROCESSED_INSTALLS",
INSTALL_LIBRARY_INIT: "INSTALL_LIBRARY_INIT", INSTALL_LIBRARY_INIT: "INSTALL_LIBRARY_INIT",
INSTALL_LIBRARY_START: "INSTALL_LIBRARY_START", INSTALL_LIBRARY_START: "INSTALL_LIBRARY_START",
@ -1288,7 +1289,15 @@ const PlatformActionErrorTypes = {
API_ERROR: "API_ERROR", 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 = { export const ReduxActionTypes = {
...DeferRenderingAppViewerActionTypes,
...ActionActionTypes, ...ActionActionTypes,
...AdminSettingsActionTypes, ...AdminSettingsActionTypes,
...AnalyticsActionTypes, ...AnalyticsActionTypes,

View File

@ -70,6 +70,7 @@ import type { layoutConversionReduxState } from "reducers/uiReducers/layoutConve
import type { OneClickBindingState } from "reducers/uiReducers/oneClickBindingReducer"; import type { OneClickBindingState } from "reducers/uiReducers/oneClickBindingReducer";
import type { IDEState } from "reducers/uiReducers/ideReducer"; import type { IDEState } from "reducers/uiReducers/ideReducer";
import type { PluginActionEditorState } from "PluginActionEditor"; 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 /* 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 */ or done so by a module that is designed to be eventually pluggable */
@ -171,6 +172,7 @@ export interface AppState {
loadingEntities: LoadingEntitiesState; loadingEntities: LoadingEntitiesState;
formEvaluation: FormEvaluationState; formEvaluation: FormEvaluationState;
triggers: TriggerValuesEvaluationState; triggers: TriggerValuesEvaluationState;
firstEvaluation: FirstEvaluationState;
}; };
linting: { linting: {
errors: LintErrorsStore; errors: LintErrorsStore;

View File

@ -157,6 +157,7 @@ import { apiFailureResponseInterceptor } from "api/interceptors/response";
import type { AxiosError } from "axios"; import type { AxiosError } from "axios";
import { handleFetchApplicationError } from "./ApplicationSagas"; import { handleFetchApplicationError } from "./ApplicationSagas";
import { getCurrentUser } from "actions/authActions"; import { getCurrentUser } from "actions/authActions";
import { getIsFirstPageLoad } from "selectors/evaluationSelectors";
export interface HandleWidgetNameUpdatePayload { export interface HandleWidgetNameUpdatePayload {
newName: string; newName: string;
@ -370,8 +371,14 @@ export function* postFetchedPublishedPage(
response.data.userPermissions, response.data.userPermissions,
), ),
); );
// Clear any existing caches const isFirstLoad: boolean = yield select(getIsFirstPageLoad);
yield call(clearEvalCache);
// Only the first page load we defer the clearing of caches
if (!isFirstLoad) {
// Clear any existing caches
yield call(clearEvalCache);
}
// Set url params // Set url params
yield call(setDataUrl); yield call(setDataUrl);

View File

@ -14,3 +14,6 @@ export const getModuleInstanceJSCollectionById = (
): JSCollection | undefined => { ): JSCollection | undefined => {
return undefined; return undefined;
}; };
export const getAllUniqueWidgetTypesInUiModules = (state: DefaultRootState) => {
return [];
};

View File

@ -92,7 +92,7 @@ function ToggleComponentToJsonHandler(props: HandlerProps) {
} }
function ToggleComponentToJson(props: Props) { function ToggleComponentToJson(props: Props) {
return props.viewType === ViewTypes.JSON return props.viewType === ViewTypes.JSON && props.renderCompFunction
? props.renderCompFunction({ ? props.renderCompFunction({
...alternateViewTypeInputConfig(), ...alternateViewTypeInputConfig(),
configProperty: props.configProperty, configProperty: props.configProperty,

View File

@ -6,8 +6,8 @@ import type { SwitchControlProps } from "components/propertyControls/SwitchContr
import SwitchControl from "components/propertyControls/SwitchControl"; import SwitchControl from "components/propertyControls/SwitchControl";
import OptionControl from "components/propertyControls/OptionControl"; import OptionControl from "components/propertyControls/OptionControl";
import type { ControlProps } from "components/propertyControls/BaseControl"; import type { ControlProps } from "components/propertyControls/BaseControl";
import type BaseControl from "components/propertyControls/BaseControl";
import CodeEditorControl from "components/propertyControls/CodeEditorControl"; import CodeEditorControl from "components/propertyControls/CodeEditorControl";
import type BaseControl from "components/propertyControls/BaseControl";
import type { DatePickerControlProps } from "components/propertyControls/DatePickerControl"; import type { DatePickerControlProps } from "components/propertyControls/DatePickerControl";
import DatePickerControl from "components/propertyControls/DatePickerControl"; import DatePickerControl from "components/propertyControls/DatePickerControl";
import ChartDataControl from "components/propertyControls/ChartDataControl"; import ChartDataControl from "components/propertyControls/ChartDataControl";

View File

@ -11,7 +11,7 @@ import {
ReduxActionTypes, ReduxActionTypes,
} from "ee/constants/ReduxActionConstants"; } from "ee/constants/ReduxActionConstants";
import type { APP_MODE } from "entities/App"; 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 type { DeployConsolidatedApi } from "sagas/InitSagas";
import { import {
failFastApiCalls, failFastApiCalls,
@ -20,7 +20,10 @@ import {
} from "sagas/InitSagas"; } from "sagas/InitSagas";
import type { AppEnginePayload } from "."; import type { AppEnginePayload } from ".";
import AppEngine, { ActionsNotFoundError } from "."; import AppEngine, { ActionsNotFoundError } from ".";
import { fetchJSLibraries } from "actions/JSLibraryActions"; import {
fetchJSLibraries,
deferLoadingJSLibraries,
} from "actions/JSLibraryActions";
import { waitForFetchUserSuccess } from "ee/sagas/userSagas"; import { waitForFetchUserSuccess } from "ee/sagas/userSagas";
import { fetchJSCollectionsForView } from "actions/jsActionActions"; import { fetchJSCollectionsForView } from "actions/jsActionActions";
import { import {
@ -29,6 +32,7 @@ import {
} from "actions/appThemingActions"; } from "actions/appThemingActions";
import type { Span } from "instrumentation/types"; import type { Span } from "instrumentation/types";
import { endSpan, startNestedSpan } from "instrumentation/generateTraces"; import { endSpan, startNestedSpan } from "instrumentation/generateTraces";
import { getIsFirstPageLoad } from "selectors/evaluationSelectors";
export default class AppViewerEngine extends AppEngine { export default class AppViewerEngine extends AppEngine {
constructor(mode: APP_MODE) { constructor(mode: APP_MODE) {
@ -120,9 +124,18 @@ export default class AppViewerEngine extends AppEngine {
ReduxActionErrorTypes.SETUP_PUBLISHED_PAGE_ERROR, ReduxActionErrorTypes.SETUP_PUBLISHED_PAGE_ERROR,
]; ];
initActionsCalls.push(fetchJSLibraries(applicationId, customJSLibraries)); const isFirstPageLoad = yield select(getIsFirstPageLoad);
successActionEffects.push(ReduxActionTypes.FETCH_JS_LIBRARIES_SUCCESS);
failureActionEffects.push(ReduxActionErrorTypes.FETCH_JS_LIBRARIES_FAILED); 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( const resultOfPrimaryCalls: boolean = yield failFastApiCalls(
initActionsCalls, initActionsCalls,

View File

@ -15,8 +15,8 @@ import { getWidgetHierarchy } from "layoutSystems/anvil/utils/paste/utils";
import type { AnvilGlobalDnDStates } from "../../canvas/hooks/useAnvilGlobalDnDStates"; import type { AnvilGlobalDnDStates } from "../../canvas/hooks/useAnvilGlobalDnDStates";
import { getWidgets } from "sagas/selectors"; import { getWidgets } from "sagas/selectors";
import { useMemo } from "react"; import { useMemo } from "react";
import { WDSZoneWidget } from "widgets/wds/WDSZoneWidget";
import { useAnvilWidgetElevation } from "../../canvas/providers/AnvilWidgetElevationProvider"; import { useAnvilWidgetElevation } from "../../canvas/providers/AnvilWidgetElevationProvider";
import { anvilWidgets } from "widgets/wds/constants";
interface AnvilDnDListenerStatesProps { interface AnvilDnDListenerStatesProps {
anvilGlobalDragStates: AnvilGlobalDnDStates; anvilGlobalDragStates: AnvilGlobalDnDStates;
@ -148,7 +148,7 @@ export const useAnvilDnDListenerStates = ({
}, [widgetProps, allWidgets]); }, [widgetProps, allWidgets]);
const isElevatedWidget = useMemo(() => { const isElevatedWidget = useMemo(() => {
if (widgetProps.type === WDSZoneWidget.type) { if (widgetProps.type === anvilWidgets.ZONE_WIDGET) {
const isAnyZoneElevated = allSiblingsWidgetIds.some( const isAnyZoneElevated = allSiblingsWidgetIds.some(
(each) => !!elevatedWidgets[each], (each) => !!elevatedWidgets[each],
); );

View File

@ -56,14 +56,13 @@ export const FixedLayoutViewerCanvas = (props: BaseWidgetProps) => {
!!props.noPad, !!props.noPad,
); );
}, [ }, [
props.children, props?.children,
props?.metaWidgetChildrenStructure,
props.positioning, props.positioning,
props.shouldScrollContents,
props.widgetId, props.widgetId,
props.componentHeight, props.noPad,
props.componentWidth, defaultWidgetProps,
snapColumnSpace, layoutSystemProps,
props.metaWidgetChildrenStructure,
]); ]);
const snapRows = getCanvasSnapRows(props.bottomRow); const snapRows = getCanvasSnapRows(props.bottomRow);

View File

@ -31,6 +31,7 @@ import { useIsMobileDevice } from "utils/hooks/useDeviceDetect";
import HtmlTitle from "../AppViewerHtmlTitle"; import HtmlTitle from "../AppViewerHtmlTitle";
import Sidebar from "./Sidebar"; import Sidebar from "./Sidebar";
import TopHeader from "./components/TopHeader"; import TopHeader from "./components/TopHeader";
import { getRenderPage } from "selectors/evaluationSelectors";
export function Navigation() { export function Navigation() {
const dispatch = useDispatch(); const dispatch = useDispatch();
@ -50,7 +51,7 @@ export function Navigation() {
getCurrentApplication, getCurrentApplication,
); );
const pages = useSelector(getViewModePageList); const pages = useSelector(getViewModePageList);
const shouldShowHeader = useSelector(getRenderPage);
const queryParams = new URLSearchParams(search); const queryParams = new URLSearchParams(search);
const isEmbed = queryParams.get("embed") === "true"; const isEmbed = queryParams.get("embed") === "true";
const forceShowNavBar = queryParams.get("navbar") === "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 // TODO: refactor this to not directly reference a DOM element by class defined elsewhere
useEffect( useEffect(
function adjustHeaderHeightEffect() { 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 () => { return () => {
dispatch(setAppViewHeaderHeight(0)); dispatch(setAppViewHeaderHeight(0));
}; };
}, },
[navStyle, orientation, dispatch], [navStyle, orientation, dispatch, shouldShowHeader],
); );
useEffect( useEffect(
@ -122,6 +125,8 @@ export function Navigation() {
pages, pages,
]); ]);
if (!shouldShowHeader) return null;
if (hideHeader) return <HtmlTitle />; if (hideHeader) return <HtmlTitle />;
return ( return (

View File

@ -40,8 +40,6 @@ import {
getAppThemeSettings, getAppThemeSettings,
getCurrentApplication, getCurrentApplication,
} from "ee/selectors/applicationSelectors"; } from "ee/selectors/applicationSelectors";
import { editorInitializer } from "../../utils/editor/EditorUtils";
import { widgetInitialisationSuccess } from "../../actions/widgetActions";
import { import {
ThemeProvider as WDSThemeProvider, ThemeProvider as WDSThemeProvider,
useTheme, useTheme,
@ -49,6 +47,10 @@ import {
import urlBuilder from "ee/entities/URLRedirect/URLAssembly"; import urlBuilder from "ee/entities/URLRedirect/URLAssembly";
import { getHideWatermark } from "ee/selectors/organizationSelectors"; import { getHideWatermark } from "ee/selectors/organizationSelectors";
import { getIsAnvilLayout } from "layoutSystems/anvil/integrations/selectors"; 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<{ const AppViewerBody = styled.section<{
hasPages: boolean; hasPages: boolean;
@ -80,6 +82,21 @@ type Props = AppViewerProps & RouteComponentProps<AppViewerRouteParams>;
const DEFAULT_FONT_NAME = "System Default"; 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<typeof useTheme>[0];
const { theme } = useTheme(isAnvilLayout ? wdsThemeProps : {});
return <WDSThemeProvider theme={theme}>{children}</WDSThemeProvider>;
}
function AppViewer(props: Props) { function AppViewer(props: Props) {
const dispatch = useDispatch(); const dispatch = useDispatch();
const { pathname, search } = props.location; const { pathname, search } = props.location;
@ -103,15 +120,7 @@ function AppViewer(props: Props) {
getCurrentApplication, getCurrentApplication,
); );
const isAnvilLayout = useSelector(getIsAnvilLayout); const isAnvilLayout = useSelector(getIsAnvilLayout);
const themeSetting = useSelector(getAppThemeSettings); const renderPage = useSelector(getRenderPage);
const wdsThemeProps = {
borderRadius: themeSetting.borderRadius,
seedColor: themeSetting.accentColor,
colorMode: themeSetting.colorMode.toLowerCase(),
userSizing: themeSetting.sizing,
userDensity: themeSetting.density,
} as Parameters<typeof useTheme>[0];
const { theme } = useTheme(isAnvilLayout ? wdsThemeProps : {});
const focusRef = useWidgetFocus(); const focusRef = useWidgetFocus();
const isAutoLayout = useSelector(getIsAutoLayout); const isAutoLayout = useSelector(getIsAutoLayout);
@ -120,9 +129,9 @@ function AppViewer(props: Props) {
* initializes the widgets factory and registers all widgets * initializes the widgets factory and registers all widgets
*/ */
useEffect(() => { useEffect(() => {
editorInitializer().then(() => { registerLayoutComponents();
dispatch(widgetInitialisationSuccess()); // 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 * initialize the app if branch, pageId or application is changed
@ -205,6 +214,8 @@ function AppViewer(props: Props) {
}; };
}, [selectedTheme.properties.fontFamily.appFont]); }, [selectedTheme.properties.fontFamily.appFont]);
if (!renderPage) return null;
const renderChildren = () => { const renderChildren = () => {
return ( return (
<EditorContextProvider renderMode="PAGE"> <EditorContextProvider renderMode="PAGE">
@ -251,7 +262,7 @@ function AppViewer(props: Props) {
if (isAnvilLayout) { if (isAnvilLayout) {
return ( return (
<WDSThemeProvider theme={theme}>{renderChildren()}</WDSThemeProvider> <WDSThemeProviderWithTheme>{renderChildren()}</WDSThemeProviderWithTheme>
); );
} }

View File

@ -24,7 +24,7 @@ import TemplateDescription from "./Template/TemplateDescription";
import SimilarTemplates from "./Template/SimilarTemplates"; import SimilarTemplates from "./Template/SimilarTemplates";
import { templateIdUrl } from "ee/RouteBuilder"; import { templateIdUrl } from "ee/RouteBuilder";
import TemplateViewHeader from "./TemplateViewHeader"; import TemplateViewHeader from "./TemplateViewHeader";
import { registerEditorWidgets } from "utils/editor/EditorUtils"; import { registerAllWidgets } from "utils/editor/EditorUtils";
const Wrapper = styled.div` const Wrapper = styled.div`
overflow: auto; overflow: auto;
@ -154,7 +154,7 @@ export function TemplateView({
}; };
useEffect(() => { useEffect(() => {
registerEditorWidgets(); registerAllWidgets();
}, []); }, []);
useEffect(() => { useEffect(() => {
dispatch(getTemplateInformation(templateId)); dispatch(getTemplateInformation(templateId));

View File

@ -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<unknown>,
): 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;
}
}

View File

@ -4,6 +4,7 @@ import evaluationDependencyReducer from "./dependencyReducer";
import loadingEntitiesReducer from "./loadingEntitiesReducer"; import loadingEntitiesReducer from "./loadingEntitiesReducer";
import formEvaluationReducer from "./formEvaluationReducer"; import formEvaluationReducer from "./formEvaluationReducer";
import triggerReducer from "./triggerReducer"; import triggerReducer from "./triggerReducer";
import firstEvaluationReducer from "./firstEvaluationReducer";
export default combineReducers({ export default combineReducers({
tree: evaluatedTreeReducer, tree: evaluatedTreeReducer,
@ -11,4 +12,5 @@ export default combineReducers({
loadingEntities: loadingEntitiesReducer, loadingEntities: loadingEntitiesReducer,
formEvaluation: formEvaluationReducer, formEvaluation: formEvaluationReducer,
triggers: triggerReducer, triggers: triggerReducer,
firstEvaluation: firstEvaluationReducer,
}); });

View File

@ -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 { ReduxActionTypes } from "ee/constants/ReduxActionConstants";
import { MAIN_THREAD_ACTION } from "ee/workers/Evaluation/evalWorkerActions"; import { MAIN_THREAD_ACTION } from "ee/workers/Evaluation/evalWorkerActions";
import log from "loglevel"; import log from "loglevel";
@ -13,6 +13,7 @@ import { MessageType } from "utils/MessageUtil";
import type { ResponsePayload } from "../sagas/EvaluationsSaga"; import type { ResponsePayload } from "../sagas/EvaluationsSaga";
import { import {
executeTriggerRequestSaga, executeTriggerRequestSaga,
getUnevalTreeWithWidgetsRegistered,
updateDataTreeHandler, updateDataTreeHandler,
} from "../sagas/EvaluationsSaga"; } from "../sagas/EvaluationsSaga";
import { evalWorker } from "utils/workerInstances"; import { evalWorker } from "utils/workerInstances";
@ -22,7 +23,7 @@ import isEmpty from "lodash/isEmpty";
import { sortJSExecutionDataByCollectionId } from "workers/Evaluation/JSObject/utils"; import { sortJSExecutionDataByCollectionId } from "workers/Evaluation/JSObject/utils";
import type { LintTreeSagaRequestData } from "plugins/Linting/types"; import type { LintTreeSagaRequestData } from "plugins/Linting/types";
import { evalErrorHandler } from "./EvalErrorHandler"; import { evalErrorHandler } from "./EvalErrorHandler";
import { getUnevaluatedDataTree } from "selectors/dataTreeSelectors"; import type { getUnevaluatedDataTree } from "selectors/dataTreeSelectors";
import { endSpan, startRootSpan } from "instrumentation/generateTraces"; import { endSpan, startRootSpan } from "instrumentation/generateTraces";
import type { UpdateDataTreeMessageData } from "./types"; import type { UpdateDataTreeMessageData } from "./types";
@ -165,9 +166,8 @@ export function* handleEvalWorkerMessage(message: TMessage<any>) {
case MAIN_THREAD_ACTION.UPDATE_DATATREE: { case MAIN_THREAD_ACTION.UPDATE_DATATREE: {
const { workerResponse } = data as UpdateDataTreeMessageData; const { workerResponse } = data as UpdateDataTreeMessageData;
const rootSpan = startRootSpan("DataTreeFactory.create"); const rootSpan = startRootSpan("DataTreeFactory.create");
const unEvalAndConfigTree: ReturnType<typeof getUnevaluatedDataTree> = const unEvalAndConfigTree: ReturnType<typeof getUnevaluatedDataTree> =
yield select(getUnevaluatedDataTree); yield call(getUnevalTreeWithWidgetsRegistered);
endSpan(rootSpan); endSpan(rootSpan);

View File

@ -34,8 +34,17 @@ import {
getCurrentPageId, getCurrentPageId,
} from "selectors/editorSelectors"; } from "selectors/editorSelectors";
import { updateActionData } from "actions/pluginActionActions"; import { updateActionData } from "actions/pluginActionActions";
import watchInitSagas from "./InitSagas";
import { clearAllWidgetFactoryCache } from "WidgetProvider/factory/decorators";
jest.mock("loglevel"); jest.mock("loglevel");
jest.mock("utils/editor/EditorUtils", () => ({
registerAllWidgets: jest.fn(),
}));
jest.mock("WidgetProvider/factory/decorators", () => ({
clearAllWidgetFactoryCache: jest.fn(),
}));
describe("evaluateTreeSaga", () => { describe("evaluateTreeSaga", () => {
afterAll(() => { afterAll(() => {
@ -64,29 +73,34 @@ describe("evaluateTreeSaga", () => {
], ],
[select(getCurrentPageDSLVersion), 1], [select(getCurrentPageDSLVersion), 1],
]) ])
.call(evalWorker.request, EVAL_WORKER_ACTIONS.EVAL_TREE, { .call(
cacheProps: { evalWorker.request,
instanceId: "instanceId", EVAL_WORKER_ACTIONS.EVAL_TREE,
appId: "applicationId", {
pageId: "pageId", 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, appMode: false,
timestamp: new Date("11 September 2024").toISOString(), widgetsMeta: {},
dslVersion: 1, shouldRespondWithLogs: true,
affectedJSObjects: { ids: [], isAllAffected: false },
actionDataPayloadConsolidated: undefined,
}, },
unevalTree: unEvalAndConfigTree, false,
widgetTypeConfigMap: undefined, )
widgets: {},
theme: {},
shouldReplay: true,
allActionValidationConfig: {},
forceEvaluation: false,
metaWidgets: {},
appMode: false,
widgetsMeta: {},
shouldRespondWithLogs: true,
affectedJSObjects: { ids: [], isAllAffected: false },
actionDataPayloadConsolidated: undefined,
})
.run(); .run();
}); });
test("should set 'shouldRespondWithLogs' to false when the log level is not debug", async () => { test("should set 'shouldRespondWithLogs' to false when the log level is not debug", async () => {
@ -112,29 +126,34 @@ describe("evaluateTreeSaga", () => {
], ],
[select(getCurrentPageDSLVersion), 1], [select(getCurrentPageDSLVersion), 1],
]) ])
.call(evalWorker.request, EVAL_WORKER_ACTIONS.EVAL_TREE, { .call(
cacheProps: { evalWorker.request,
instanceId: "instanceId", EVAL_WORKER_ACTIONS.EVAL_TREE,
appId: "applicationId", {
pageId: "pageId", 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, appMode: false,
timestamp: new Date("11 September 2024").toISOString(), widgetsMeta: {},
dslVersion: 1, shouldRespondWithLogs: false,
affectedJSObjects: { ids: [], isAllAffected: false },
actionDataPayloadConsolidated: undefined,
}, },
unevalTree: unEvalAndConfigTree, false,
widgetTypeConfigMap: undefined, )
widgets: {},
theme: {},
shouldReplay: true,
allActionValidationConfig: {},
forceEvaluation: false,
metaWidgets: {},
appMode: false,
widgetsMeta: {},
shouldRespondWithLogs: false,
affectedJSObjects: { ids: [], isAllAffected: false },
actionDataPayloadConsolidated: undefined,
})
.run(); .run();
}); });
test("should propagate affectedJSObjects property to evaluation action", async () => { test("should propagate affectedJSObjects property to evaluation action", async () => {
@ -169,29 +188,95 @@ describe("evaluateTreeSaga", () => {
], ],
[select(getCurrentPageDSLVersion), 1], [select(getCurrentPageDSLVersion), 1],
]) ])
.call(evalWorker.request, EVAL_WORKER_ACTIONS.EVAL_TREE, { .call(
cacheProps: { evalWorker.request,
instanceId: "instanceId", EVAL_WORKER_ACTIONS.EVAL_TREE,
appId: "applicationId", {
pageId: "pageId", 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, appMode: false,
timestamp: new Date("11 September 2024").toISOString(), widgetsMeta: {},
dslVersion: 1, shouldRespondWithLogs: false,
affectedJSObjects,
actionDataPayloadConsolidated: undefined,
}, },
unevalTree: unEvalAndConfigTree, false,
widgetTypeConfigMap: undefined, )
widgets: {}, .run();
theme: {}, });
shouldReplay: true, test("should call evalWorker.request with isFirstEvaluation as true when isFirstEvaluation is set as true in evaluateTreeSaga", async () => {
allActionValidationConfig: {}, const unEvalAndConfigTree = { unEvalTree: {}, configTree: {} };
forceEvaluation: false, const isFirstEvaluation = true;
metaWidgets: {},
appMode: false, return expectSaga(
widgetsMeta: {}, evaluateTreeSaga,
shouldRespondWithLogs: false, unEvalAndConfigTree,
affectedJSObjects, [],
actionDataPayloadConsolidated: undefined, 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(); .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();
});
});

View File

@ -1,4 +1,9 @@
import type { ActionPattern, CallEffect, ForkEffect } from "redux-saga/effects"; import type {
ActionPattern,
CallEffect,
Effect,
ForkEffect,
} from "redux-saga/effects";
import { import {
actionChannel, actionChannel,
all, all,
@ -9,6 +14,7 @@ import {
select, select,
spawn, spawn,
take, take,
join,
} from "redux-saga/effects"; } from "redux-saga/effects";
import type { import type {
@ -16,7 +22,10 @@ import type {
ReduxActionType, ReduxActionType,
AnyReduxAction, AnyReduxAction,
} from "actions/ReduxActionTypes"; } from "actions/ReduxActionTypes";
import { ReduxActionTypes } from "ee/constants/ReduxActionConstants"; import {
ReduxActionTypes,
ReduxActionErrorTypes,
} from "ee/constants/ReduxActionConstants";
import { import {
getDataTree, getDataTree,
getUnevaluatedDataTree, getUnevaluatedDataTree,
@ -39,6 +48,7 @@ import {
import { import {
setDependencyMap, setDependencyMap,
setEvaluatedTree, setEvaluatedTree,
setIsFirstPageLoad,
shouldForceEval, shouldForceEval,
shouldLog, shouldLog,
shouldProcessAction, shouldProcessAction,
@ -99,7 +109,7 @@ import {
} from "actions/pluginActionActions"; } from "actions/pluginActionActions";
import { executeJSUpdates } from "actions/jsPaneActions"; import { executeJSUpdates } from "actions/jsPaneActions";
import { setEvaluatedActionSelectorField } from "actions/actionSelectorActions"; import { setEvaluatedActionSelectorField } from "actions/actionSelectorActions";
import { waitForWidgetConfigBuild } from "./InitSagas";
import { logDynamicTriggerExecution } from "ee/sagas/analyticsSaga"; import { logDynamicTriggerExecution } from "ee/sagas/analyticsSaga";
import { selectFeatureFlags } from "ee/selectors/featureFlagsSelectors"; import { selectFeatureFlags } from "ee/selectors/featureFlagsSelectors";
import { fetchFeatureFlagsInit } from "actions/userActions"; import { fetchFeatureFlagsInit } from "actions/userActions";
@ -108,7 +118,6 @@ import {
parseUpdatesAndDeleteUndefinedUpdates, parseUpdatesAndDeleteUndefinedUpdates,
} from "./EvaluationsSagaUtils"; } from "./EvaluationsSagaUtils";
import { getFeatureFlagsFetched } from "selectors/usersSelectors"; import { getFeatureFlagsFetched } from "selectors/usersSelectors";
import { getIsCurrentEditorWorkflowType } from "ee/selectors/workflowSelectors";
import { evalErrorHandler } from "./EvalErrorHandler"; import { evalErrorHandler } from "./EvalErrorHandler";
import AnalyticsUtil from "ee/utils/AnalyticsUtil"; import AnalyticsUtil from "ee/utils/AnalyticsUtil";
import { endSpan, startRootSpan } from "instrumentation/generateTraces"; import { endSpan, startRootSpan } from "instrumentation/generateTraces";
@ -124,11 +133,89 @@ import type {
EvaluationReduxAction, EvaluationReduxAction,
} from "actions/EvaluationReduxActionTypes"; } from "actions/EvaluationReduxActionTypes";
import { appsmithTelemetry } from "instrumentation"; 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(); const APPSMITH_CONFIGS = getAppsmithConfigs();
let widgetTypeConfigMap: WidgetTypeConfigMap; 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<string, boolean> =
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( export function* updateDataTreeHandler(
data: { data: {
evalTreeResponse: EvalTreeResponseData; evalTreeResponse: EvalTreeResponseData;
@ -271,6 +358,7 @@ export function* evaluateTreeSaga(
requiresLogging = false, requiresLogging = false,
affectedJSObjects: AffectedJSObjects = defaultAffectedJSObjects, affectedJSObjects: AffectedJSObjects = defaultAffectedJSObjects,
actionDataPayloadConsolidated?: actionDataPayload, actionDataPayloadConsolidated?: actionDataPayload,
isFirstEvaluation = false,
) { ) {
const allActionValidationConfig: ReturnType< const allActionValidationConfig: ReturnType<
typeof getAllActionValidationConfig typeof getAllActionValidationConfig
@ -322,6 +410,7 @@ export function* evaluateTreeSaga(
evalWorker.request, evalWorker.request,
EVAL_WORKER_ACTIONS.EVAL_TREE, EVAL_WORKER_ACTIONS.EVAL_TREE,
evalTreeRequestData, evalTreeRequestData,
isFirstEvaluation,
); );
yield call( yield call(
@ -369,8 +458,8 @@ export function* evaluateAndExecuteDynamicTrigger(
) { ) {
const rootSpan = startRootSpan("DataTreeFactory.create"); const rootSpan = startRootSpan("DataTreeFactory.create");
const unEvalTree: ReturnType<typeof getUnevaluatedDataTree> = yield select( const unEvalTree: ReturnType<typeof getUnevaluatedDataTree> = yield call(
getUnevaluatedDataTree, getUnevalTreeWithWidgetsRegistered,
); );
endSpan(rootSpan); endSpan(rootSpan);
@ -521,7 +610,7 @@ function* validateProperty(property: string, value: any, props: WidgetProps) {
const rootSpan = startRootSpan("DataTreeFactory.create"); const rootSpan = startRootSpan("DataTreeFactory.create");
const unEvalAndConfigTree: ReturnType<typeof getUnevaluatedDataTree> = const unEvalAndConfigTree: ReturnType<typeof getUnevaluatedDataTree> =
yield select(getUnevaluatedDataTree); yield call(getUnevalTreeWithWidgetsRegistered);
endSpan(rootSpan); endSpan(rootSpan);
const configTree = unEvalAndConfigTree.configTree; const configTree = unEvalAndConfigTree.configTree;
@ -541,6 +630,15 @@ function* validateProperty(property: string, value: any, props: WidgetProps) {
return response; return response;
} }
export function* getUnevalTreeWithWidgetsRegistered() {
yield call(loadAndRegisterOnlyCanvasWidgets);
const unEvalAndConfigTree: ReturnType<typeof getUnevaluatedDataTree> =
yield select(getUnevaluatedDataTree);
return unEvalAndConfigTree;
}
// We are clubbing all pending action's affected JS objects into the buffered action // 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 // So that during that evaluation cycle all affected JS objects are correctly diffed
function mergeJSBufferedActions( function mergeJSBufferedActions(
@ -706,6 +804,8 @@ export function* evalAndLintingHandler(
requiresLogging: boolean; requiresLogging: boolean;
affectedJSObjects: AffectedJSObjects; affectedJSObjects: AffectedJSObjects;
actionDataPayloadConsolidated: actionDataPayload[]; actionDataPayloadConsolidated: actionDataPayload[];
isFirstEvaluation?: boolean;
jsLibrariesTask?: Task;
}>, }>,
) { ) {
const span = startRootSpan("evalAndLintingHandler"); const span = startRootSpan("evalAndLintingHandler");
@ -713,6 +813,9 @@ export function* evalAndLintingHandler(
actionDataPayloadConsolidated, actionDataPayloadConsolidated,
affectedJSObjects, affectedJSObjects,
forceEvaluation, forceEvaluation,
isFirstEvaluation = false,
jsLibrariesTask,
requiresLogging, requiresLogging,
shouldReplay, shouldReplay,
} = options; } = options;
@ -737,10 +840,17 @@ export function* evalAndLintingHandler(
// Generate all the data needed for both eval and linting // Generate all the data needed for both eval and linting
const unEvalAndConfigTree: ReturnType<typeof getUnevaluatedDataTree> = const unEvalAndConfigTree: ReturnType<typeof getUnevaluatedDataTree> =
yield select(getUnevaluatedDataTree); yield call(getUnevalTreeWithWidgetsRegistered);
widgetTypeConfigMap = WidgetFactory.getWidgetTypeConfigMap();
endSpan(rootSpan); endSpan(rootSpan);
// wait for the webworker to complete its setup before starting the evaluation
if (jsLibrariesTask) {
yield join(jsLibrariesTask);
}
const postEvalActions = getPostEvalActions(action); const postEvalActions = getPostEvalActions(action);
const fn: (...args: unknown[]) => CallEffect<unknown> | ForkEffect<unknown> = const fn: (...args: unknown[]) => CallEffect<unknown> | ForkEffect<unknown> =
isBlockingCall ? call : fork; isBlockingCall ? call : fork;
@ -758,6 +868,7 @@ export function* evalAndLintingHandler(
requiresLogging, requiresLogging,
affectedJSObjects, affectedJSObjects,
actionDataPayloadConsolidated, actionDataPayloadConsolidated,
isFirstEvaluation,
), ),
); );
} }
@ -769,51 +880,80 @@ export function* evalAndLintingHandler(
yield all(effects); yield all(effects);
endSpan(span); 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 // TODO: Fix this the next time the file is edited
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
function* evaluationChangeListenerSaga(): any { function* evaluationChangeListenerSaga(): any {
const firstEvalActionChannel = yield actionChannel(FIRST_EVAL_REDUX_ACTIONS); const firstEvalActionChannel = yield actionChannel(FIRST_EVAL_REDUX_ACTIONS);
// Explicitly shutdown old worker if present const initializeJSLibrariesChannel = yield actionChannel(
yield all([call(evalWorker.shutdown), call(lintWorker.shutdown)]); ReduxActionTypes.DEFER_LOADING_JS_LIBRARIES,
const [evalWorkerListenerChannel] = yield all([ );
call(evalWorker.start), const appMode = yield select(getAppMode);
call(lintWorker.start),
]);
const isFFFetched = yield select(getFeatureFlagsFetched); let jsLibrariesTask: Task | undefined;
if (!isFFFetched) { // for all published apps, we need to reset the data tree and setup the worker as an independent process
yield call(fetchFeatureFlagsInit); // after the process is forked we can allow the main thread to continue its execution since the main thread's tasks would be independent
yield take(ReduxActionTypes.FETCH_FEATURE_FLAGS_SUCCESS); // 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<string, boolean> =
yield select(selectFeatureFlags);
yield call(evalWorker.request, EVAL_WORKER_ACTIONS.SETUP, {
cloudHosting: !!APPSMITH_CONFIGS.cloudHosting,
featureFlags: featureFlags,
});
yield spawn(handleEvalWorkerRequestSaga, evalWorkerListenerChannel);
const initAction: EvaluationReduxAction<unknown> = yield take( const initAction: EvaluationReduxAction<unknown> = yield take(
firstEvalActionChannel, firstEvalActionChannel,
); );
firstEvalActionChannel.close(); 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, { yield fork(evalAndLintingHandler, false, initAction, {
shouldReplay: false, shouldReplay: false,
forceEvaluation: false, forceEvaluation: false,
@ -822,6 +962,8 @@ function* evaluationChangeListenerSaga(): any {
ids: [], ids: [],
isAllAffected: true, isAllAffected: true,
}, },
isFirstEvaluation: true,
jsLibrariesTask: jsLibrariesTask,
}); });
// TODO: Fix this the next time the file is edited // TODO: Fix this the next time the file is edited
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any

View File

@ -93,6 +93,7 @@ import type { Page } from "entities/Page";
import type { PACKAGE_PULL_STATUS } from "ee/constants/ModuleConstants"; import type { PACKAGE_PULL_STATUS } from "ee/constants/ModuleConstants";
import { validateSessionToken } from "utils/SessionUtils"; import { validateSessionToken } from "utils/SessionUtils";
import { appsmithTelemetry } from "instrumentation"; import { appsmithTelemetry } from "instrumentation";
import { clearAllWidgetFactoryCache } from "WidgetProvider/factory/decorators";
export const URL_CHANGE_ACTIONS = [ export const URL_CHANGE_ACTIONS = [
ReduxActionTypes.CURRENT_APPLICATION_NAME_UPDATE, ReduxActionTypes.CURRENT_APPLICATION_NAME_UPDATE,
@ -535,6 +536,11 @@ function* eagerPageInitSaga() {
} catch (e) {} } 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() { export default function* watchInitSagas() {
yield all([ yield all([
takeLeading( takeLeading(
@ -547,5 +553,7 @@ export default function* watchInitSagas() {
takeLatest(ReduxActionTypes.RESET_EDITOR_REQUEST, resetEditorSaga), takeLatest(ReduxActionTypes.RESET_EDITOR_REQUEST, resetEditorSaga),
takeEvery(URL_CHANGE_ACTIONS, updateURLSaga), takeEvery(URL_CHANGE_ACTIONS, updateURLSaga),
takeEvery(ReduxActionTypes.INITIALIZE_CURRENT_PAGE, eagerPageInitSaga), takeEvery(ReduxActionTypes.INITIALIZE_CURRENT_PAGE, eagerPageInitSaga),
takeLeading(ReduxActionTypes.WIDGET_INIT_SUCCESS, handleWidgetInitSuccess),
]); ]);
} }

View File

@ -1,6 +1,7 @@
import type { DataTree } from "entities/DataTree/dataTreeTypes"; import type { DataTree } from "entities/DataTree/dataTreeTypes";
import { createSelector } from "reselect"; import { createSelector } from "reselect";
import WidgetFactory from "WidgetProvider/factory"; import WidgetFactory from "WidgetProvider/factory";
import { getWidgetConfigsVersion } from "WidgetProvider/factory/widgetConfigVersion";
import type { FlattenedWidgetProps } from "WidgetProvider/types"; import type { FlattenedWidgetProps } from "WidgetProvider/types";
import type { JSLibrary } from "workers/common/JSLibrary"; import type { JSLibrary } from "workers/common/JSLibrary";
import { getDataTree } from "./dataTreeSelectors"; import { getDataTree } from "./dataTreeSelectors";
@ -24,6 +25,7 @@ export const getUsedActionNames = createSelector(
getDataTree, getDataTree,
getParentWidget, getParentWidget,
selectInstalledLibraries, selectInstalledLibraries,
getWidgetConfigsVersion, // Add dependency on widget configs version
( (
// TODO: Fix this the next time the file is edited // TODO: Fix this the next time the file is edited
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any

View File

@ -52,6 +52,7 @@ import type { Page } from "entities/Page";
import { objectKeys } from "@appsmith/utils"; import { objectKeys } from "@appsmith/utils";
import type { MetaWidgetsReduxState } from "reducers/entityReducers/metaWidgetsReducer"; import type { MetaWidgetsReduxState } from "reducers/entityReducers/metaWidgetsReducer";
import { ActionRunBehaviour } from "PluginActionEditor/types/PluginActionTypes"; import { ActionRunBehaviour } from "PluginActionEditor/types/PluginActionTypes";
import { getWidgetConfigsVersion } from "WidgetProvider/factory/widgetConfigVersion";
const getIsDraggingOrResizing = (state: DefaultRootState) => const getIsDraggingOrResizing = (state: DefaultRootState) =>
state.ui.widgetDragResize.isResizing || state.ui.widgetDragResize.isDragging; state.ui.widgetDragResize.isResizing || state.ui.widgetDragResize.isDragging;
@ -398,6 +399,7 @@ const isModuleWidget = (
export const getWidgetCards = createSelector( export const getWidgetCards = createSelector(
getIsAutoLayout, getIsAutoLayout,
getIsAnvilLayout, getIsAnvilLayout,
getWidgetConfigsVersion, // Add dependency on widget configs version
(isAutoLayout, isAnvilLayout) => { (isAutoLayout, isAnvilLayout) => {
const widgetConfigs = WidgetFactory.getConfigs(); const widgetConfigs = WidgetFactory.getConfigs();
const widgetConfigsArray = Object.values(widgetConfigs); const widgetConfigsArray = Object.values(widgetConfigs);

View File

@ -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;

View File

@ -8,6 +8,7 @@ import { getExistingWidgetNames } from "sagas/selectors";
import { getNextEntityName } from "utils/AppsmithUtils"; import { getNextEntityName } from "utils/AppsmithUtils";
import WidgetFactory from "WidgetProvider/factory"; import WidgetFactory from "WidgetProvider/factory";
import { getWidgetConfigsVersion } from "WidgetProvider/factory/widgetConfigVersion";
import { import {
getAltBlockWidgetSelection, getAltBlockWidgetSelection,
getFocusedWidget, getFocusedWidget,
@ -78,6 +79,7 @@ export const getModalDropdownList = createSelector(
export const getNextModalName = createSelector( export const getNextModalName = createSelector(
getExistingWidgetNames, getExistingWidgetNames,
getModalWidgetType, getModalWidgetType,
getWidgetConfigsVersion, // Add dependency on widget configs version
(names, modalWidgetType) => { (names, modalWidgetType) => {
const prefix = const prefix =
WidgetFactory.widgetConfigMap.get(modalWidgetType)?.widgetName || ""; WidgetFactory.widgetConfigMap.get(modalWidgetType)?.widgetName || "";
@ -267,3 +269,19 @@ export const isResizingOrDragging = createSelector(
(state: DefaultRootState) => state.ui.widgetDragResize.isDragging, (state: DefaultRootState) => state.ui.widgetDragResize.isDragging,
(isResizing, isDragging) => !!isResizing || !!isDragging, (isResizing, isDragging) => !!isResizing || !!isDragging,
); );
// get widgets types associated to a tab
export const getUsedWidgetTypes = createSelector(
getCanvasWidgets,
(canvasWidgets) => {
const widgetTypes = new Set<string>();
// 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);
},
);

View File

@ -21,6 +21,7 @@ export const getCanvasHeightOffset = (
props: WidgetProps, props: WidgetProps,
) => { ) => {
const { getCanvasHeightOffset } = WidgetFactory.getWidgetMethods(widgetType); const { getCanvasHeightOffset } = WidgetFactory.getWidgetMethods(widgetType);
let offset = 0; let offset = 0;
if (getCanvasHeightOffset) { if (getCanvasHeightOffset) {

View File

@ -20,6 +20,7 @@ import {
filterSpanData, filterSpanData,
newWebWorkerSpanData, newWebWorkerSpanData,
} from "instrumentation/generateWebWorkerTraces"; } from "instrumentation/generateWebWorkerTraces";
import { ReduxActionTypes } from "ee/constants/ReduxActionConstants";
/** /**
* Wrap a webworker to provide a synchronous request-response semantic. * 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 method identifier for a rpc method
* @param requestData data that we want to send over to the worker * @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 * @returns response from the worker
*/ */
// TODO: Fix this the next time the file is edited // TODO: Fix this the next time the file is edited
// eslint-disable-next-line @typescript-eslint/no-explicit-any // 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); yield this.ready(true);
// Impossible case, but helps avoid `?` later in code and makes it clearer. // Impossible case, but helps avoid `?` later in code and makes it clearer.
@ -292,6 +294,12 @@ export class GracefulWorkerService {
messageId, 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. // The `this._broker` method is listening to events and will pass response to us over this channel.
const response = yield take(ch); const response = yield take(ch);
const { data, endTime, startTime } = response; const { data, endTime, startTime } = response;

View File

@ -2,14 +2,20 @@
// import Widgets from "widgets"; // import Widgets from "widgets";
import { registerWidgets } from "WidgetProvider/factory/registrationHelper"; import { registerWidgets } from "WidgetProvider/factory/registrationHelper";
import { registerLayoutComponents } from "layoutSystems/anvil/utils/layouts/layoutUtils"; 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(Array.from(loadedWidgets.values()));
registerWidgets(widgets); } catch (error) {
// eslint-disable-next-line no-console
console.error("Error loading widgets", error);
}
}; };
export const editorInitializer = async () => { export const editorInitializer = async () => {
registerEditorWidgets(); await registerAllWidgets();
// TODO: do this only for anvil. // TODO: do this only for anvil.
registerLayoutComponents(); registerLayoutComponents();
}; };

View File

@ -6,9 +6,10 @@ import type {
} from "constants/PropertyControlConstants"; } from "constants/PropertyControlConstants";
import { ValidationTypes } from "constants/WidgetValidation"; import { ValidationTypes } from "constants/WidgetValidation";
import { isFunction } from "lodash"; import { isFunction } from "lodash";
import widgets from "widgets"; import { loadAllWidgets } from "widgets";
import WidgetFactory from "WidgetProvider/factory"; import WidgetFactory from "WidgetProvider/factory";
import { registerWidgets } from "WidgetProvider/factory/registrationHelper"; import { registerWidgets } from "WidgetProvider/factory/registrationHelper";
import type BaseWidget from "widgets/BaseWidget";
function validatePropertyPaneConfig( function validatePropertyPaneConfig(
config: PropertyPaneConfig[], config: PropertyPaneConfig[],
@ -143,96 +144,112 @@ const isNotFloat = (n: any) => {
}; };
describe("Tests all widget's propertyPane config", () => { describe("Tests all widget's propertyPane config", () => {
beforeAll(() => { let widgetsArray: (typeof BaseWidget)[] = [];
registerWidgets(widgets);
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 it("should have loaded widgets", () => {
// Exclude WDS widgets from the tests, since they work differently expect(widgetsArray.length).toBeGreaterThan(0);
.filter((widget) => !widget.type.includes("WDS")) });
.forEach((widget) => {
const config = widget.getConfig();
it(`Checks ${widget.type}'s propertyPaneConfig`, () => { describe("Property Pane Config Tests", () => {
const propertyPaneConfig = widget.getPropertyPaneConfig(); //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( it(`Checks ${widget.type}'s propertyPaneConfig`, () => {
validatePropertyPaneConfig(propertyPaneConfig, !!config.hideCard), const propertyPaneConfig = widget.getPropertyPaneConfig();
).toStrictEqual(true);
const propertyPaneContentConfig = widget.getPropertyPaneContentConfig();
expect( expect(
validatePropertyPaneConfig( validatePropertyPaneConfig(propertyPaneConfig, !!config.hideCard),
propertyPaneContentConfig, ).toStrictEqual(true);
!!config.isDeprecated, const propertyPaneContentConfig =
), widget.getPropertyPaneContentConfig();
).toStrictEqual(true);
const propertyPaneStyleConfig = widget.getPropertyPaneStyleConfig();
expect( expect(
validatePropertyPaneConfig( validatePropertyPaneConfig(
propertyPaneStyleConfig, propertyPaneContentConfig,
!!config.isDeprecated, !!config.isDeprecated,
), ),
).toStrictEqual(true); ).toStrictEqual(true);
}); const propertyPaneStyleConfig = widget.getPropertyPaneStyleConfig();
it(`Check if ${widget.type}'s dimensions are always integers`, () => {
const defaults = widget.getDefaults();
expect(isNotFloat(defaults.rows)).toBe(true); expect(
expect(isNotFloat(defaults.columns)).toBe(true); validatePropertyPaneConfig(
}); propertyPaneStyleConfig,
!!config.isDeprecated,
),
).toStrictEqual(true);
});
it(`Check if ${widget.type}'s dimensions are always integers`, () => {
const defaults = widget.getDefaults();
if (config.isDeprecated) { expect(isNotFloat(defaults.rows)).toBe(true);
it(`Check if ${widget.type}'s deprecation config has a proper replacement Widget`, () => { expect(isNotFloat(defaults.columns)).toBe(true);
const widgetType = widget.type; });
if (config.replacement === undefined) { if (config.isDeprecated) {
fail(`${widgetType}'s replacement widget is not defined`); it(`Check if ${widget.type}'s deprecation config has a proper replacement Widget`, () => {
} const widgetType = widget.type;
const replacementWidgetType = config.replacement; if (config.replacement === undefined) {
const replacementWidget = WidgetFactory.get(replacementWidgetType); fail(`${widgetType}'s replacement widget is not defined`);
const replacementWidgetConfig = replacementWidget?.getConfig(); }
if (replacementWidgetConfig === undefined) { const replacementWidgetType = config.replacement;
fail( const replacementWidget = WidgetFactory.get(replacementWidgetType);
`${widgetType}'s replacement widget ${replacementWidgetType} does not resolve to an actual widget Config`, const replacementWidgetConfig = replacementWidget?.getConfig();
);
}
if (replacementWidgetConfig?.isDeprecated) { if (replacementWidgetConfig === undefined) {
fail( fail(
`${widgetType}'s replacement widget ${replacementWidgetType} itself is deprecated. Cannot have a deprecated widget as a replacement for another deprecated widget`, `${widgetType}'s replacement widget ${replacementWidgetType} does not resolve to an actual widget Config`,
); );
} }
if (replacementWidgetConfig?.hideCard) { if (replacementWidgetConfig?.isDeprecated) {
fail( fail(
`${widgetType}'s replacement widget ${replacementWidgetType} should be available in the entity Explorer`, `${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");
}
}
}); });
}); });
}); });

View File

@ -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 type BaseWidget from "./BaseWidget";
import ExternalWidget from "./ExternalWidget"; import { retryPromise } from "utils/AppsmithUtils";
import { WDSTableWidget } from "widgets/wds/WDSTableWidget"; import { anvilWidgets } from "./wds/constants";
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 { EEWDSWidgets } from "ee/widgets/wds"; import { EEWDSWidgets } from "ee/widgets/wds";
import { WDSDatePickerWidget } from "widgets/wds/WDSDatePickerWidget";
import { WDSMultiSelectWidget } from "widgets/wds/WDSMultiSelectWidget";
import { EEWidgets } from "ee/widgets"; import { EEWidgets } from "ee/widgets";
const LegacyWidgets = [ // Create widget loader map
CanvasWidget, const WidgetLoaders = new Map<string, () => Promise<typeof BaseWidget>>([
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,
...EEWDSWidgets, ...EEWDSWidgets,
...EEWidgets, ...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<string, typeof BaseWidget>();
// Function to load a specific widget by type
export const loadWidget = async (type: string): Promise<typeof BaseWidget> => {
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<string, typeof BaseWidget>
> => {
const allWidgets = new Map<string, typeof BaseWidget>();
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;

View File

@ -4,9 +4,13 @@ import { evalWorker } from "utils/workerInstances";
import { EVAL_WORKER_ACTIONS } from "ee/workers/Evaluation/evalWorkerActions"; import { EVAL_WORKER_ACTIONS } from "ee/workers/Evaluation/evalWorkerActions";
import { runSaga } from "redux-saga"; import { runSaga } from "redux-saga";
import { TriggerKind } from "constants/AppsmithActionConstants/ActionConstants"; import { TriggerKind } from "constants/AppsmithActionConstants/ActionConstants";
import { registerAllWidgets } from "utils/editor/EditorUtils";
export async function UNSTABLE_executeDynamicTrigger(dynamicTrigger: string) { export async function UNSTABLE_executeDynamicTrigger(dynamicTrigger: string) {
const state = store.getState(); const state = store.getState();
await registerAllWidgets();
const unEvalTree = getUnevaluatedDataTree(state); const unEvalTree = getUnevaluatedDataTree(state);
const result = runSaga( const result = runSaga(

View File

@ -290,75 +290,64 @@ export async function loadLibraries(
const libStore: Record<string, unknown> = {}; const libStore: Record<string, unknown> = {};
try { try {
for (const lib of libs) { await Promise.all(
const url = lib.url as string; libs.map(async (lib) => {
const accessors = lib.accessor; const url = lib.url as string;
const keysBefore = Object.keys(self); const accessors = lib.accessor;
let module = null; const keysBefore = Object.keys(self);
let module = null;
try { try {
self.importScripts(url); self.importScripts(url);
const keysAfter = Object.keys(self); const keysAfter = Object.keys(self);
let defaultAccessors = difference(keysAfter, keysBefore); let defaultAccessors = difference(keysAfter, keysBefore);
// Changing default export to library accessors name which was saved when it was installed, if default export present movetheDefaultExportedLibraryToAccessorKey(
movetheDefaultExportedLibraryToAccessorKey( defaultAccessors,
defaultAccessors, accessors[0],
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);
/** defaultAccessors.push(
* Installing 2 different version of lodash tries to add the same accessor on the self object. Let take version a & b for example. ...Object.keys(libStore).filter((k) => libStore[k] !== self[k]),
* 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]),
);
/** accessors.sort();
* Sort the accessor list from backend and installed accessor list using the same rule to apply all modifications. defaultAccessors.sort();
* 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++) { for (let i = 0; i < defaultAccessors.length; i++) {
self[accessors[i]] = self[defaultAccessors[i]]; self[accessors[i]] = self[defaultAccessors[i]];
libStore[defaultAccessors[i]] = self[defaultAccessors[i]]; libStore[defaultAccessors[i]] = self[defaultAccessors[i]];
libraryReservedIdentifiers[accessors[i]] = true; libraryReservedIdentifiers[accessors[i]] = true;
invalidEntityIdentifiers[accessors[i]] = true; invalidEntityIdentifiers[accessors[i]] = true;
}
return;
} catch (e) {
log.debug(e);
} }
continue; try {
} catch (e) { module = await import(/* webpackIgnore: true */ url);
log.debug(e);
}
try { if (!module || typeof module !== "object") throw "Not an ESM module";
module = await import(/* webpackIgnore: true */ url);
if (!module || typeof module !== "object") throw "Not an ESM module"; const key = accessors[0];
const flattenedModule = flattenModule(module);
const key = accessors[0]; libStore[key] = flattenedModule;
const flattenedModule = flattenModule(module); self[key] = flattenedModule;
libraryReservedIdentifiers[key] = true;
libStore[key] = flattenedModule; invalidEntityIdentifiers[key] = true;
self[key] = flattenedModule; } catch (e) {
libraryReservedIdentifiers[key] = true; log.debug(e);
invalidEntityIdentifiers[key] = true; throw new ImportError(url);
} catch (e) { }
log.debug(e); }),
throw new ImportError(url); );
}
}
JSLibraries.push(...libs); JSLibraries.push(...libs);
JSLibraryAccessor.regenerateSet(); JSLibraryAccessor.regenerateSet();

View File

@ -14,7 +14,7 @@ import {
import { updateDependencyMap } from "workers/common/DependencyMap"; import { updateDependencyMap } from "workers/common/DependencyMap";
import { replaceThisDotParams } from "./utils"; import { replaceThisDotParams } from "./utils";
import { isDataField } from "./utils"; import { isDataField } from "./utils";
import widgets from "widgets"; import { loadAllWidgets } from "widgets";
import type { WidgetConfiguration } from "WidgetProvider/types"; import type { WidgetConfiguration } from "WidgetProvider/types";
import { type WidgetEntity } from "ee/entities/DataTree/types"; import { type WidgetEntity } from "ee/entities/DataTree/types";
import { import {
@ -35,14 +35,18 @@ const widgetConfigMap: Record<
} }
> = {}; > = {};
widgets.map((widget) => { beforeAll(async () => {
if (widget.type) { const loadedWidgets = await loadAllWidgets();
widgetConfigMap[widget.type] = {
defaultProperties: widget.getDefaultPropertiesMap(), loadedWidgets.forEach((widget) => {
derivedProperties: widget.getDerivedPropertiesMap(), if (widget.type) {
metaProperties: widget.getMetaPropertiesMap(), widgetConfigMap[widget.type] = {
}; defaultProperties: widget.getDefaultPropertiesMap(),
} derivedProperties: widget.getDerivedPropertiesMap(),
metaProperties: widget.getMetaPropertiesMap(),
};
}
});
}); });
jest.mock("ee/workers/Evaluation/generateOverrideContext"); // mock the generateOverrideContext function jest.mock("ee/workers/Evaluation/generateOverrideContext"); // mock the generateOverrideContext function