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

View File

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

View File

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

View File

@ -14,6 +14,7 @@ const JSLibraryActionTypes = {
TOGGLE_INSTALLER: "TOGGLE_INSTALLER",
FETCH_JS_LIBRARIES_INIT: "FETCH_JS_LIBRARIES_INIT",
FETCH_JS_LIBRARIES_SUCCESS: "FETCH_JS_LIBRARIES_SUCCESS",
DEFER_LOADING_JS_LIBRARIES: "DEFER_LOADING_JS_LIBRARIES",
CLEAR_PROCESSED_INSTALLS: "CLEAR_PROCESSED_INSTALLS",
INSTALL_LIBRARY_INIT: "INSTALL_LIBRARY_INIT",
INSTALL_LIBRARY_START: "INSTALL_LIBRARY_START",
@ -1288,7 +1289,15 @@ const PlatformActionErrorTypes = {
API_ERROR: "API_ERROR",
};
const DeferRenderingAppViewerActionTypes = {
HAS_DISPATCHED_FIRST_EVALUATION_MESSAGE:
"HAS_DISPATCHED_FIRST_EVALUATION_MESSAGE",
RENDER_PAGE: "RENDER_PAGE",
IS_FIRST_PAGE_LOAD: "IS_FIRST_PAGE_LOAD",
};
export const ReduxActionTypes = {
...DeferRenderingAppViewerActionTypes,
...ActionActionTypes,
...AdminSettingsActionTypes,
...AnalyticsActionTypes,

View File

@ -70,6 +70,7 @@ import type { layoutConversionReduxState } from "reducers/uiReducers/layoutConve
import type { OneClickBindingState } from "reducers/uiReducers/oneClickBindingReducer";
import type { IDEState } from "reducers/uiReducers/ideReducer";
import type { PluginActionEditorState } from "PluginActionEditor";
import type { FirstEvaluationState } from "reducers/evaluationReducers/firstEvaluationReducer";
/* Reducers which are integrated into the core system when registering a pluggable module
or done so by a module that is designed to be eventually pluggable */
@ -171,6 +172,7 @@ export interface AppState {
loadingEntities: LoadingEntitiesState;
formEvaluation: FormEvaluationState;
triggers: TriggerValuesEvaluationState;
firstEvaluation: FirstEvaluationState;
};
linting: {
errors: LintErrorsStore;

View File

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

View File

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

View File

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

View File

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

View File

@ -11,7 +11,7 @@ import {
ReduxActionTypes,
} from "ee/constants/ReduxActionConstants";
import type { APP_MODE } from "entities/App";
import { call, put, spawn } from "redux-saga/effects";
import { call, put, select, spawn } from "redux-saga/effects";
import type { DeployConsolidatedApi } from "sagas/InitSagas";
import {
failFastApiCalls,
@ -20,7 +20,10 @@ import {
} from "sagas/InitSagas";
import type { AppEnginePayload } from ".";
import AppEngine, { ActionsNotFoundError } from ".";
import { fetchJSLibraries } from "actions/JSLibraryActions";
import {
fetchJSLibraries,
deferLoadingJSLibraries,
} from "actions/JSLibraryActions";
import { waitForFetchUserSuccess } from "ee/sagas/userSagas";
import { fetchJSCollectionsForView } from "actions/jsActionActions";
import {
@ -29,6 +32,7 @@ import {
} from "actions/appThemingActions";
import type { Span } from "instrumentation/types";
import { endSpan, startNestedSpan } from "instrumentation/generateTraces";
import { getIsFirstPageLoad } from "selectors/evaluationSelectors";
export default class AppViewerEngine extends AppEngine {
constructor(mode: APP_MODE) {
@ -120,9 +124,18 @@ export default class AppViewerEngine extends AppEngine {
ReduxActionErrorTypes.SETUP_PUBLISHED_PAGE_ERROR,
];
initActionsCalls.push(fetchJSLibraries(applicationId, customJSLibraries));
successActionEffects.push(ReduxActionTypes.FETCH_JS_LIBRARIES_SUCCESS);
failureActionEffects.push(ReduxActionErrorTypes.FETCH_JS_LIBRARIES_FAILED);
const isFirstPageLoad = yield select(getIsFirstPageLoad);
if (isFirstPageLoad) {
// we are deferring the loading of JS libraries
yield put(deferLoadingJSLibraries(applicationId, customJSLibraries));
} else {
initActionsCalls.push(fetchJSLibraries(applicationId, customJSLibraries));
successActionEffects.push(ReduxActionTypes.FETCH_JS_LIBRARIES_SUCCESS);
failureActionEffects.push(
ReduxActionErrorTypes.FETCH_JS_LIBRARIES_FAILED,
);
}
const resultOfPrimaryCalls: boolean = yield failFastApiCalls(
initActionsCalls,

View File

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

View File

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

View File

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

View File

@ -40,8 +40,6 @@ import {
getAppThemeSettings,
getCurrentApplication,
} from "ee/selectors/applicationSelectors";
import { editorInitializer } from "../../utils/editor/EditorUtils";
import { widgetInitialisationSuccess } from "../../actions/widgetActions";
import {
ThemeProvider as WDSThemeProvider,
useTheme,
@ -49,6 +47,10 @@ import {
import urlBuilder from "ee/entities/URLRedirect/URLAssembly";
import { getHideWatermark } from "ee/selectors/organizationSelectors";
import { getIsAnvilLayout } from "layoutSystems/anvil/integrations/selectors";
import { getRenderPage } from "selectors/evaluationSelectors";
import type { ReactNode } from "react";
import { registerLayoutComponents } from "layoutSystems/anvil/utils/layouts/layoutUtils";
import { widgetInitialisationSuccess } from "actions/widgetActions";
const AppViewerBody = styled.section<{
hasPages: boolean;
@ -80,6 +82,21 @@ type Props = AppViewerProps & RouteComponentProps<AppViewerRouteParams>;
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) {
const dispatch = useDispatch();
const { pathname, search } = props.location;
@ -103,15 +120,7 @@ function AppViewer(props: Props) {
getCurrentApplication,
);
const isAnvilLayout = useSelector(getIsAnvilLayout);
const themeSetting = useSelector(getAppThemeSettings);
const wdsThemeProps = {
borderRadius: themeSetting.borderRadius,
seedColor: themeSetting.accentColor,
colorMode: themeSetting.colorMode.toLowerCase(),
userSizing: themeSetting.sizing,
userDensity: themeSetting.density,
} as Parameters<typeof useTheme>[0];
const { theme } = useTheme(isAnvilLayout ? wdsThemeProps : {});
const renderPage = useSelector(getRenderPage);
const focusRef = useWidgetFocus();
const isAutoLayout = useSelector(getIsAutoLayout);
@ -120,9 +129,9 @@ function AppViewer(props: Props) {
* initializes the widgets factory and registers all widgets
*/
useEffect(() => {
editorInitializer().then(() => {
dispatch(widgetInitialisationSuccess());
});
registerLayoutComponents();
// we want to intialise only the widgets relevant to the tab within the appViewer page first so that first evaluation is faster
dispatch(widgetInitialisationSuccess());
}, []);
/**
* initialize the app if branch, pageId or application is changed
@ -205,6 +214,8 @@ function AppViewer(props: Props) {
};
}, [selectedTheme.properties.fontFamily.appFont]);
if (!renderPage) return null;
const renderChildren = () => {
return (
<EditorContextProvider renderMode="PAGE">
@ -251,7 +262,7 @@ function AppViewer(props: Props) {
if (isAnvilLayout) {
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 { templateIdUrl } from "ee/RouteBuilder";
import TemplateViewHeader from "./TemplateViewHeader";
import { registerEditorWidgets } from "utils/editor/EditorUtils";
import { registerAllWidgets } from "utils/editor/EditorUtils";
const Wrapper = styled.div`
overflow: auto;
@ -154,7 +154,7 @@ export function TemplateView({
};
useEffect(() => {
registerEditorWidgets();
registerAllWidgets();
}, []);
useEffect(() => {
dispatch(getTemplateInformation(templateId));

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 formEvaluationReducer from "./formEvaluationReducer";
import triggerReducer from "./triggerReducer";
import firstEvaluationReducer from "./firstEvaluationReducer";
export default combineReducers({
tree: evaluatedTreeReducer,
@ -11,4 +12,5 @@ export default combineReducers({
loadingEntities: loadingEntitiesReducer,
formEvaluation: formEvaluationReducer,
triggers: triggerReducer,
firstEvaluation: firstEvaluationReducer,
});

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

View File

@ -34,8 +34,17 @@ import {
getCurrentPageId,
} from "selectors/editorSelectors";
import { updateActionData } from "actions/pluginActionActions";
import watchInitSagas from "./InitSagas";
import { clearAllWidgetFactoryCache } from "WidgetProvider/factory/decorators";
jest.mock("loglevel");
jest.mock("utils/editor/EditorUtils", () => ({
registerAllWidgets: jest.fn(),
}));
jest.mock("WidgetProvider/factory/decorators", () => ({
clearAllWidgetFactoryCache: jest.fn(),
}));
describe("evaluateTreeSaga", () => {
afterAll(() => {
@ -64,29 +73,34 @@ describe("evaluateTreeSaga", () => {
],
[select(getCurrentPageDSLVersion), 1],
])
.call(evalWorker.request, EVAL_WORKER_ACTIONS.EVAL_TREE, {
cacheProps: {
instanceId: "instanceId",
appId: "applicationId",
pageId: "pageId",
.call(
evalWorker.request,
EVAL_WORKER_ACTIONS.EVAL_TREE,
{
cacheProps: {
instanceId: "instanceId",
appId: "applicationId",
pageId: "pageId",
appMode: false,
timestamp: new Date("11 September 2024").toISOString(),
dslVersion: 1,
},
unevalTree: unEvalAndConfigTree,
widgetTypeConfigMap: undefined,
widgets: {},
theme: {},
shouldReplay: true,
allActionValidationConfig: {},
forceEvaluation: false,
metaWidgets: {},
appMode: false,
timestamp: new Date("11 September 2024").toISOString(),
dslVersion: 1,
widgetsMeta: {},
shouldRespondWithLogs: true,
affectedJSObjects: { ids: [], isAllAffected: false },
actionDataPayloadConsolidated: undefined,
},
unevalTree: unEvalAndConfigTree,
widgetTypeConfigMap: undefined,
widgets: {},
theme: {},
shouldReplay: true,
allActionValidationConfig: {},
forceEvaluation: false,
metaWidgets: {},
appMode: false,
widgetsMeta: {},
shouldRespondWithLogs: true,
affectedJSObjects: { ids: [], isAllAffected: false },
actionDataPayloadConsolidated: undefined,
})
false,
)
.run();
});
test("should set 'shouldRespondWithLogs' to false when the log level is not debug", async () => {
@ -112,29 +126,34 @@ describe("evaluateTreeSaga", () => {
],
[select(getCurrentPageDSLVersion), 1],
])
.call(evalWorker.request, EVAL_WORKER_ACTIONS.EVAL_TREE, {
cacheProps: {
instanceId: "instanceId",
appId: "applicationId",
pageId: "pageId",
.call(
evalWorker.request,
EVAL_WORKER_ACTIONS.EVAL_TREE,
{
cacheProps: {
instanceId: "instanceId",
appId: "applicationId",
pageId: "pageId",
appMode: false,
timestamp: new Date("11 September 2024").toISOString(),
dslVersion: 1,
},
unevalTree: unEvalAndConfigTree,
widgetTypeConfigMap: undefined,
widgets: {},
theme: {},
shouldReplay: true,
allActionValidationConfig: {},
forceEvaluation: false,
metaWidgets: {},
appMode: false,
timestamp: new Date("11 September 2024").toISOString(),
dslVersion: 1,
widgetsMeta: {},
shouldRespondWithLogs: false,
affectedJSObjects: { ids: [], isAllAffected: false },
actionDataPayloadConsolidated: undefined,
},
unevalTree: unEvalAndConfigTree,
widgetTypeConfigMap: undefined,
widgets: {},
theme: {},
shouldReplay: true,
allActionValidationConfig: {},
forceEvaluation: false,
metaWidgets: {},
appMode: false,
widgetsMeta: {},
shouldRespondWithLogs: false,
affectedJSObjects: { ids: [], isAllAffected: false },
actionDataPayloadConsolidated: undefined,
})
false,
)
.run();
});
test("should propagate affectedJSObjects property to evaluation action", async () => {
@ -169,29 +188,95 @@ describe("evaluateTreeSaga", () => {
],
[select(getCurrentPageDSLVersion), 1],
])
.call(evalWorker.request, EVAL_WORKER_ACTIONS.EVAL_TREE, {
cacheProps: {
instanceId: "instanceId",
appId: "applicationId",
pageId: "pageId",
.call(
evalWorker.request,
EVAL_WORKER_ACTIONS.EVAL_TREE,
{
cacheProps: {
instanceId: "instanceId",
appId: "applicationId",
pageId: "pageId",
appMode: false,
timestamp: new Date("11 September 2024").toISOString(),
dslVersion: 1,
},
unevalTree: unEvalAndConfigTree,
widgetTypeConfigMap: undefined,
widgets: {},
theme: {},
shouldReplay: true,
allActionValidationConfig: {},
forceEvaluation: false,
metaWidgets: {},
appMode: false,
timestamp: new Date("11 September 2024").toISOString(),
dslVersion: 1,
widgetsMeta: {},
shouldRespondWithLogs: false,
affectedJSObjects,
actionDataPayloadConsolidated: undefined,
},
unevalTree: unEvalAndConfigTree,
widgetTypeConfigMap: undefined,
widgets: {},
theme: {},
shouldReplay: true,
allActionValidationConfig: {},
forceEvaluation: false,
metaWidgets: {},
appMode: false,
widgetsMeta: {},
shouldRespondWithLogs: false,
affectedJSObjects,
actionDataPayloadConsolidated: undefined,
})
false,
)
.run();
});
test("should call evalWorker.request with isFirstEvaluation as true when isFirstEvaluation is set as true in evaluateTreeSaga", async () => {
const unEvalAndConfigTree = { unEvalTree: {}, configTree: {} };
const isFirstEvaluation = true;
return expectSaga(
evaluateTreeSaga,
unEvalAndConfigTree,
[],
undefined,
undefined,
undefined,
undefined,
undefined,
isFirstEvaluation,
)
.provide([
[select(getAllActionValidationConfig), {}],
[select(getWidgets), {}],
[select(getMetaWidgets), {}],
[select(getSelectedAppTheme), {}],
[select(getAppMode), false],
[select(getWidgetsMeta), {}],
[select(getInstanceId), "instanceId"],
[select(getCurrentApplicationId), "applicationId"],
[select(getCurrentPageId), "pageId"],
[
select(getApplicationLastDeployedAt),
new Date("11 September 2024").toISOString(),
],
[select(getCurrentPageDSLVersion), 1],
])
.call(
evalWorker.request,
EVAL_WORKER_ACTIONS.EVAL_TREE,
{
cacheProps: {
instanceId: "instanceId",
appId: "applicationId",
pageId: "pageId",
appMode: false,
timestamp: new Date("11 September 2024").toISOString(),
dslVersion: 1,
},
unevalTree: unEvalAndConfigTree,
widgetTypeConfigMap: undefined,
widgets: {},
theme: {},
shouldReplay: true,
allActionValidationConfig: {},
forceEvaluation: false,
metaWidgets: {},
appMode: false,
widgetsMeta: {},
shouldRespondWithLogs: false,
affectedJSObjects: { ids: [], isAllAffected: false },
actionDataPayloadConsolidated: undefined,
},
true,
)
.run();
});
});
@ -534,3 +619,15 @@ describe("evaluationLoopWithDebounce", () => {
});
});
});
describe("first evaluation integration", () => {
it("should call clearAllWidgetFactoryCache when WIDGET_INIT_SUCCESS is dispatched", async () => {
await expectSaga(watchInitSagas)
.dispatch({
type: ReduxActionTypes.WIDGET_INIT_SUCCESS,
})
.silentRun();
expect(clearAllWidgetFactoryCache).toHaveBeenCalled();
});
});

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 {
actionChannel,
all,
@ -9,6 +14,7 @@ import {
select,
spawn,
take,
join,
} from "redux-saga/effects";
import type {
@ -16,7 +22,10 @@ import type {
ReduxActionType,
AnyReduxAction,
} from "actions/ReduxActionTypes";
import { ReduxActionTypes } from "ee/constants/ReduxActionConstants";
import {
ReduxActionTypes,
ReduxActionErrorTypes,
} from "ee/constants/ReduxActionConstants";
import {
getDataTree,
getUnevaluatedDataTree,
@ -39,6 +48,7 @@ import {
import {
setDependencyMap,
setEvaluatedTree,
setIsFirstPageLoad,
shouldForceEval,
shouldLog,
shouldProcessAction,
@ -99,7 +109,7 @@ import {
} from "actions/pluginActionActions";
import { executeJSUpdates } from "actions/jsPaneActions";
import { setEvaluatedActionSelectorField } from "actions/actionSelectorActions";
import { waitForWidgetConfigBuild } from "./InitSagas";
import { logDynamicTriggerExecution } from "ee/sagas/analyticsSaga";
import { selectFeatureFlags } from "ee/selectors/featureFlagsSelectors";
import { fetchFeatureFlagsInit } from "actions/userActions";
@ -108,7 +118,6 @@ import {
parseUpdatesAndDeleteUndefinedUpdates,
} from "./EvaluationsSagaUtils";
import { getFeatureFlagsFetched } from "selectors/usersSelectors";
import { getIsCurrentEditorWorkflowType } from "ee/selectors/workflowSelectors";
import { evalErrorHandler } from "./EvalErrorHandler";
import AnalyticsUtil from "ee/utils/AnalyticsUtil";
import { endSpan, startRootSpan } from "instrumentation/generateTraces";
@ -124,11 +133,89 @@ import type {
EvaluationReduxAction,
} from "actions/EvaluationReduxActionTypes";
import { appsmithTelemetry } from "instrumentation";
import { getUsedWidgetTypes } from "selectors/widgetSelectors";
import type BaseWidget from "widgets/BaseWidget";
import { loadWidget } from "widgets";
import { registerWidgets } from "WidgetProvider/factory/registrationHelper";
import { failFastApiCalls } from "./InitSagas";
import { fetchJSLibraries } from "actions/JSLibraryActions";
import type { Task } from "redux-saga";
import { getAllUniqueWidgetTypesInUiModules } from "ee/selectors/moduleInstanceSelectors";
import { clearAllWidgetFactoryCache } from "WidgetProvider/factory/decorators";
const APPSMITH_CONFIGS = getAppsmithConfigs();
let widgetTypeConfigMap: WidgetTypeConfigMap;
// Common worker setup logic
// TODO: Fix this the next time the file is edited
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function* setupWorkers(clearCache = false): any {
// Explicitly shutdown old worker if present
yield all([call(evalWorker.shutdown), call(lintWorker.shutdown)]);
const [evalWorkerListenerChannel] = yield all([
call(evalWorker.start),
call(lintWorker.start),
]);
if (clearCache) {
yield call(evalWorker.request, EVAL_WORKER_ACTIONS.CLEAR_CACHE);
}
const isFFFetched = yield select(getFeatureFlagsFetched);
if (!isFFFetched) {
yield call(fetchFeatureFlagsInit);
yield take(ReduxActionTypes.FETCH_FEATURE_FLAGS_SUCCESS);
}
const featureFlags: Record<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(
data: {
evalTreeResponse: EvalTreeResponseData;
@ -271,6 +358,7 @@ export function* evaluateTreeSaga(
requiresLogging = false,
affectedJSObjects: AffectedJSObjects = defaultAffectedJSObjects,
actionDataPayloadConsolidated?: actionDataPayload,
isFirstEvaluation = false,
) {
const allActionValidationConfig: ReturnType<
typeof getAllActionValidationConfig
@ -322,6 +410,7 @@ export function* evaluateTreeSaga(
evalWorker.request,
EVAL_WORKER_ACTIONS.EVAL_TREE,
evalTreeRequestData,
isFirstEvaluation,
);
yield call(
@ -369,8 +458,8 @@ export function* evaluateAndExecuteDynamicTrigger(
) {
const rootSpan = startRootSpan("DataTreeFactory.create");
const unEvalTree: ReturnType<typeof getUnevaluatedDataTree> = yield select(
getUnevaluatedDataTree,
const unEvalTree: ReturnType<typeof getUnevaluatedDataTree> = yield call(
getUnevalTreeWithWidgetsRegistered,
);
endSpan(rootSpan);
@ -521,7 +610,7 @@ function* validateProperty(property: string, value: any, props: WidgetProps) {
const rootSpan = startRootSpan("DataTreeFactory.create");
const unEvalAndConfigTree: ReturnType<typeof getUnevaluatedDataTree> =
yield select(getUnevaluatedDataTree);
yield call(getUnevalTreeWithWidgetsRegistered);
endSpan(rootSpan);
const configTree = unEvalAndConfigTree.configTree;
@ -541,6 +630,15 @@ function* validateProperty(property: string, value: any, props: WidgetProps) {
return response;
}
export function* getUnevalTreeWithWidgetsRegistered() {
yield call(loadAndRegisterOnlyCanvasWidgets);
const unEvalAndConfigTree: ReturnType<typeof getUnevaluatedDataTree> =
yield select(getUnevaluatedDataTree);
return unEvalAndConfigTree;
}
// We are clubbing all pending action's affected JS objects into the buffered action
// So that during that evaluation cycle all affected JS objects are correctly diffed
function mergeJSBufferedActions(
@ -706,6 +804,8 @@ export function* evalAndLintingHandler(
requiresLogging: boolean;
affectedJSObjects: AffectedJSObjects;
actionDataPayloadConsolidated: actionDataPayload[];
isFirstEvaluation?: boolean;
jsLibrariesTask?: Task;
}>,
) {
const span = startRootSpan("evalAndLintingHandler");
@ -713,6 +813,9 @@ export function* evalAndLintingHandler(
actionDataPayloadConsolidated,
affectedJSObjects,
forceEvaluation,
isFirstEvaluation = false,
jsLibrariesTask,
requiresLogging,
shouldReplay,
} = options;
@ -737,10 +840,17 @@ export function* evalAndLintingHandler(
// Generate all the data needed for both eval and linting
const unEvalAndConfigTree: ReturnType<typeof getUnevaluatedDataTree> =
yield select(getUnevaluatedDataTree);
yield call(getUnevalTreeWithWidgetsRegistered);
widgetTypeConfigMap = WidgetFactory.getWidgetTypeConfigMap();
endSpan(rootSpan);
// wait for the webworker to complete its setup before starting the evaluation
if (jsLibrariesTask) {
yield join(jsLibrariesTask);
}
const postEvalActions = getPostEvalActions(action);
const fn: (...args: unknown[]) => CallEffect<unknown> | ForkEffect<unknown> =
isBlockingCall ? call : fork;
@ -758,6 +868,7 @@ export function* evalAndLintingHandler(
requiresLogging,
affectedJSObjects,
actionDataPayloadConsolidated,
isFirstEvaluation,
),
);
}
@ -769,51 +880,80 @@ export function* evalAndLintingHandler(
yield all(effects);
endSpan(span);
}
export function* loadAndRegisterOnlyCanvasWidgets(): Generator<
Effect,
(typeof BaseWidget)[],
unknown
> {
try {
const widgetTypes = (yield select(getUsedWidgetTypes)) as string[];
const uiModuleTypes = (yield select(
getAllUniqueWidgetTypesInUiModules,
)) as string[];
const uniqueWidgetTypes = Array.from(
new Set([...uiModuleTypes, ...widgetTypes, "SKELETON_WIDGET"]),
);
// Filter out already registered widget types
const unregisteredWidgetTypes = uniqueWidgetTypes.filter(
(type: string) => !WidgetFactory.widgetsMap.has(type),
);
if (!unregisteredWidgetTypes.length) {
return [];
}
// Load only unregistered widgets in parallel
const loadedWidgets = (yield all(
unregisteredWidgetTypes.map((type: string) => call(loadWidget, type)),
)) as (typeof BaseWidget)[];
// Register only the newly loaded widgets
registerWidgets(loadedWidgets);
clearAllWidgetFactoryCache();
return loadedWidgets;
} catch (error) {
log.error("Error loading and registering widgets:", error);
throw error;
}
}
// TODO: Fix this the next time the file is edited
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function* evaluationChangeListenerSaga(): any {
const firstEvalActionChannel = yield actionChannel(FIRST_EVAL_REDUX_ACTIONS);
// Explicitly shutdown old worker if present
yield all([call(evalWorker.shutdown), call(lintWorker.shutdown)]);
const [evalWorkerListenerChannel] = yield all([
call(evalWorker.start),
call(lintWorker.start),
]);
const initializeJSLibrariesChannel = yield actionChannel(
ReduxActionTypes.DEFER_LOADING_JS_LIBRARIES,
);
const appMode = yield select(getAppMode);
const isFFFetched = yield select(getFeatureFlagsFetched);
let jsLibrariesTask: Task | undefined;
if (!isFFFetched) {
yield call(fetchFeatureFlagsInit);
yield take(ReduxActionTypes.FETCH_FEATURE_FLAGS_SUCCESS);
// for all published apps, we need to reset the data tree and setup the worker as an independent process
// after the process is forked we can allow the main thread to continue its execution since the main thread's tasks would be independent
// we just need to ensure that the webworker setup is completed before the first evaluation is triggered
if (appMode === APP_MODE.PUBLISHED) {
yield put({ type: ReduxActionTypes.RESET_DATA_TREE });
jsLibrariesTask = yield fork(
webWorkerSetupSagaWithJSLibraries,
initializeJSLibrariesChannel,
);
} else {
// for all other modes, just call the webworker
yield call(webWorkerSetupSaga);
}
const featureFlags: Record<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(
firstEvalActionChannel,
);
firstEvalActionChannel.close();
// Wait for widget config build to complete before starting evaluation only if the current editor is not a workflow
const isCurrentEditorWorkflowType = yield select(
getIsCurrentEditorWorkflowType,
);
if (!isCurrentEditorWorkflowType) {
yield call(waitForWidgetConfigBuild);
}
widgetTypeConfigMap = WidgetFactory.getWidgetTypeConfigMap();
yield fork(evalAndLintingHandler, false, initAction, {
shouldReplay: false,
forceEvaluation: false,
@ -822,6 +962,8 @@ function* evaluationChangeListenerSaga(): any {
ids: [],
isAllAffected: true,
},
isFirstEvaluation: true,
jsLibrariesTask: jsLibrariesTask,
});
// TODO: Fix this the next time the file is edited
// eslint-disable-next-line @typescript-eslint/no-explicit-any

View File

@ -93,6 +93,7 @@ import type { Page } from "entities/Page";
import type { PACKAGE_PULL_STATUS } from "ee/constants/ModuleConstants";
import { validateSessionToken } from "utils/SessionUtils";
import { appsmithTelemetry } from "instrumentation";
import { clearAllWidgetFactoryCache } from "WidgetProvider/factory/decorators";
export const URL_CHANGE_ACTIONS = [
ReduxActionTypes.CURRENT_APPLICATION_NAME_UPDATE,
@ -535,6 +536,11 @@ function* eagerPageInitSaga() {
} catch (e) {}
}
function handleWidgetInitSuccess() {
//every time a widget is initialized, we clear the cache so that all widgetFactory values are recomputed
clearAllWidgetFactoryCache();
}
export default function* watchInitSagas() {
yield all([
takeLeading(
@ -547,5 +553,7 @@ export default function* watchInitSagas() {
takeLatest(ReduxActionTypes.RESET_EDITOR_REQUEST, resetEditorSaga),
takeEvery(URL_CHANGE_ACTIONS, updateURLSaga),
takeEvery(ReduxActionTypes.INITIALIZE_CURRENT_PAGE, eagerPageInitSaga),
takeLeading(ReduxActionTypes.WIDGET_INIT_SUCCESS, handleWidgetInitSuccess),
]);
}

View File

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

View File

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

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

View File

@ -20,6 +20,7 @@ import {
filterSpanData,
newWebWorkerSpanData,
} from "instrumentation/generateWebWorkerTraces";
import { ReduxActionTypes } from "ee/constants/ReduxActionConstants";
/**
* Wrap a webworker to provide a synchronous request-response semantic.
@ -241,12 +242,13 @@ export class GracefulWorkerService {
*
* @param method identifier for a rpc method
* @param requestData data that we want to send over to the worker
* @param isFirstEvaluation whether this is the first evaluation of the request
*
* @returns response from the worker
*/
// TODO: Fix this the next time the file is edited
// eslint-disable-next-line @typescript-eslint/no-explicit-any
*request(method: string, data = {}): any {
*request(method: string, data = {}, isFirstEvaluation = false): any {
yield this.ready(true);
// Impossible case, but helps avoid `?` later in code and makes it clearer.
@ -292,6 +294,12 @@ export class GracefulWorkerService {
messageId,
});
// Use delay to ensure RENDER_PAGE is dispatched after the sendMessage macro task
if (isFirstEvaluation) {
yield delay(0); // This ensures the macro task completes
yield put({ type: ReduxActionTypes.RENDER_PAGE });
}
// The `this._broker` method is listening to events and will pass response to us over this channel.
const response = yield take(ch);
const { data, endTime, startTime } = response;

View File

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

View File

@ -6,9 +6,10 @@ import type {
} from "constants/PropertyControlConstants";
import { ValidationTypes } from "constants/WidgetValidation";
import { isFunction } from "lodash";
import widgets from "widgets";
import { loadAllWidgets } from "widgets";
import WidgetFactory from "WidgetProvider/factory";
import { registerWidgets } from "WidgetProvider/factory/registrationHelper";
import type BaseWidget from "widgets/BaseWidget";
function validatePropertyPaneConfig(
config: PropertyPaneConfig[],
@ -143,96 +144,112 @@ const isNotFloat = (n: any) => {
};
describe("Tests all widget's propertyPane config", () => {
beforeAll(() => {
registerWidgets(widgets);
let widgetsArray: (typeof BaseWidget)[] = [];
beforeAll(async () => {
// Load all widgets and convert Map to array
const widgetsMap = await loadAllWidgets();
widgetsArray = Array.from(widgetsMap.values());
// Register all widgets
registerWidgets(widgetsArray);
});
widgets
// Exclude WDS widgets from the tests, since they work differently
.filter((widget) => !widget.type.includes("WDS"))
.forEach((widget) => {
const config = widget.getConfig();
it("should have loaded widgets", () => {
expect(widgetsArray.length).toBeGreaterThan(0);
});
it(`Checks ${widget.type}'s propertyPaneConfig`, () => {
const propertyPaneConfig = widget.getPropertyPaneConfig();
describe("Property Pane Config Tests", () => {
//widgets are loaded in the beforeAll and ready now
widgetsArray
// Exclude WDS widgets from the tests, since they work differently
.filter((widget) => !widget.type.includes("WDS"))
.forEach((widget) => {
const config = widget.getConfig();
expect(
validatePropertyPaneConfig(propertyPaneConfig, !!config.hideCard),
).toStrictEqual(true);
const propertyPaneContentConfig = widget.getPropertyPaneContentConfig();
it(`Checks ${widget.type}'s propertyPaneConfig`, () => {
const propertyPaneConfig = widget.getPropertyPaneConfig();
expect(
validatePropertyPaneConfig(
propertyPaneContentConfig,
!!config.isDeprecated,
),
).toStrictEqual(true);
const propertyPaneStyleConfig = widget.getPropertyPaneStyleConfig();
expect(
validatePropertyPaneConfig(propertyPaneConfig, !!config.hideCard),
).toStrictEqual(true);
const propertyPaneContentConfig =
widget.getPropertyPaneContentConfig();
expect(
validatePropertyPaneConfig(
propertyPaneStyleConfig,
!!config.isDeprecated,
),
).toStrictEqual(true);
});
it(`Check if ${widget.type}'s dimensions are always integers`, () => {
const defaults = widget.getDefaults();
expect(
validatePropertyPaneConfig(
propertyPaneContentConfig,
!!config.isDeprecated,
),
).toStrictEqual(true);
const propertyPaneStyleConfig = widget.getPropertyPaneStyleConfig();
expect(isNotFloat(defaults.rows)).toBe(true);
expect(isNotFloat(defaults.columns)).toBe(true);
});
expect(
validatePropertyPaneConfig(
propertyPaneStyleConfig,
!!config.isDeprecated,
),
).toStrictEqual(true);
});
it(`Check if ${widget.type}'s dimensions are always integers`, () => {
const defaults = widget.getDefaults();
if (config.isDeprecated) {
it(`Check if ${widget.type}'s deprecation config has a proper replacement Widget`, () => {
const widgetType = widget.type;
expect(isNotFloat(defaults.rows)).toBe(true);
expect(isNotFloat(defaults.columns)).toBe(true);
});
if (config.replacement === undefined) {
fail(`${widgetType}'s replacement widget is not defined`);
}
if (config.isDeprecated) {
it(`Check if ${widget.type}'s deprecation config has a proper replacement Widget`, () => {
const widgetType = widget.type;
const replacementWidgetType = config.replacement;
const replacementWidget = WidgetFactory.get(replacementWidgetType);
const replacementWidgetConfig = replacementWidget?.getConfig();
if (config.replacement === undefined) {
fail(`${widgetType}'s replacement widget is not defined`);
}
if (replacementWidgetConfig === undefined) {
fail(
`${widgetType}'s replacement widget ${replacementWidgetType} does not resolve to an actual widget Config`,
);
}
const replacementWidgetType = config.replacement;
const replacementWidget = WidgetFactory.get(replacementWidgetType);
const replacementWidgetConfig = replacementWidget?.getConfig();
if (replacementWidgetConfig?.isDeprecated) {
fail(
`${widgetType}'s replacement widget ${replacementWidgetType} itself is deprecated. Cannot have a deprecated widget as a replacement for another deprecated widget`,
);
}
if (replacementWidgetConfig === undefined) {
fail(
`${widgetType}'s replacement widget ${replacementWidgetType} does not resolve to an actual widget Config`,
);
}
if (replacementWidgetConfig?.hideCard) {
fail(
`${widgetType}'s replacement widget ${replacementWidgetType} should be available in the entity Explorer`,
);
if (replacementWidgetConfig?.isDeprecated) {
fail(
`${widgetType}'s replacement widget ${replacementWidgetType} itself is deprecated. Cannot have a deprecated widget as a replacement for another deprecated widget`,
);
}
if (replacementWidgetConfig?.hideCard) {
fail(
`${widgetType}'s replacement widget ${replacementWidgetType} should be available in the entity Explorer`,
);
}
});
}
it(`Check if ${widget.type}'s setter method are configured correctly`, () => {
const setterConfig = widget.getSetterConfig();
if (setterConfig) {
expect(setterConfig).toHaveProperty("__setters");
const setters = setterConfig.__setters;
for (const [setterName, config] of Object.entries(setters)) {
expect(config).toHaveProperty("type");
expect(config).toHaveProperty("path");
expect(setterName).toContain("set");
const type = config.type;
const path = config.path;
expect(typeof type).toBe("string");
expect(typeof path).toBe("string");
}
}
});
}
it(`Check if ${widget.type}'s setter method are configured correctly`, () => {
const setterConfig = widget.getSetterConfig();
if (setterConfig) {
expect(setterConfig).toHaveProperty("__setters");
const setters = setterConfig.__setters;
for (const [setterName, config] of Object.entries(setters)) {
expect(config).toHaveProperty("type");
expect(config).toHaveProperty("path");
expect(setterName).toContain("set");
const type = config.type;
const path = config.path;
expect(typeof type).toBe("string");
expect(typeof path).toBe("string");
}
}
});
});
});
});

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 ExternalWidget from "./ExternalWidget";
import { WDSTableWidget } from "widgets/wds/WDSTableWidget";
import { WDSCurrencyInputWidget } from "widgets/wds/WDSCurrencyInputWidget";
import { WDSToolbarButtonsWidget } from "widgets/wds/WDSToolbarButtonsWidget";
import { WDSPhoneInputWidget } from "widgets/wds/WDSPhoneInputWidget";
import { WDSCheckboxGroupWidget } from "widgets/wds/WDSCheckboxGroupWidget";
import { WDSComboBoxWidget } from "widgets/wds/WDSComboBoxWidget";
import { WDSSwitchWidget } from "widgets/wds/WDSSwitchWidget";
import { WDSSwitchGroupWidget } from "widgets/wds/WDSSwitchGroupWidget";
import { WDSRadioGroupWidget } from "widgets/wds/WDSRadioGroupWidget";
import { WDSMenuButtonWidget } from "widgets/wds/WDSMenuButtonWidget";
import CustomWidget from "./CustomWidget";
import { WDSSectionWidget } from "widgets/wds/WDSSectionWidget";
import { WDSZoneWidget } from "widgets/wds/WDSZoneWidget";
import { WDSHeadingWidget } from "widgets/wds/WDSHeadingWidget";
import { WDSParagraphWidget } from "widgets/wds/WDSParagraphWidget";
import { WDSModalWidget } from "widgets/wds/WDSModalWidget";
import { WDSStatsWidget } from "widgets/wds/WDSStatsWidget";
import { WDSKeyValueWidget } from "widgets/wds/WDSKeyValueWidget";
import { WDSInlineButtonsWidget } from "widgets/wds/WDSInlineButtonsWidget";
import { WDSEmailInputWidget } from "widgets/wds/WDSEmailInputWidget";
import { WDSPasswordInputWidget } from "widgets/wds/WDSPasswordInputWidget";
import { WDSNumberInputWidget } from "widgets/wds/WDSNumberInputWidget";
import { WDSMultilineInputWidget } from "widgets/wds/WDSMultilineInputWidget";
import { WDSSelectWidget } from "widgets/wds/WDSSelectWidget";
import { WDSCustomWidget } from "widgets/wds/WDSCustomWidget";
import { retryPromise } from "utils/AppsmithUtils";
import { anvilWidgets } from "./wds/constants";
import { EEWDSWidgets } from "ee/widgets/wds";
import { WDSDatePickerWidget } from "widgets/wds/WDSDatePickerWidget";
import { WDSMultiSelectWidget } from "widgets/wds/WDSMultiSelectWidget";
import { EEWidgets } from "ee/widgets";
const LegacyWidgets = [
CanvasWidget,
SkeletonWidget,
ContainerWidget,
TextWidget,
TableWidget,
CheckboxWidget,
RadioGroupWidget,
ButtonWidget,
ImageWidget,
VideoWidget,
TabsWidget,
ModalWidget,
ChartWidget,
MapWidget,
RichTextEditorWidget,
DatePickerWidget2,
SwitchWidget,
FormWidget,
RateWidget,
IframeWidget,
TabsMigratorWidget,
DividerWidget,
MenuButtonWidget,
IconButtonWidget,
CheckboxGroupWidget,
FilePickerWidgetV2,
StatboxWidget,
AudioRecorderWidget,
DocumentViewerWidget,
ButtonGroupWidget,
MultiSelectTreeWidget,
SingleSelectTreeWidget,
SwitchGroupWidget,
AudioWidget,
ProgressBarWidget,
CameraWidget,
MapChartWidget,
SelectWidget,
MultiSelectWidgetV2,
InputWidgetV2,
PhoneInputWidget,
CurrencyInputWidget,
JSONFormWidget,
TableWidgetV2,
NumberSliderWidget,
RangeSliderWidget,
CategorySliderWidget,
CodeScannerWidget,
ListWidgetV2,
ExternalWidget,
];
const DeprecatedWidgets = [
//Deprecated Widgets
InputWidget,
DropdownWidget,
DatePickerWidget,
IconWidget,
FilePickerWidget,
MultiSelectWidget,
FormButtonWidget,
ProgressWidget,
CircularProgressWidget,
ListWidget,
];
const WDSWidgets = [
WDSButtonWidget,
WDSInputWidget,
WDSCheckboxWidget,
WDSIconButtonWidget,
WDSTableWidget,
WDSCurrencyInputWidget,
WDSToolbarButtonsWidget,
WDSPhoneInputWidget,
WDSCheckboxGroupWidget,
WDSComboBoxWidget,
WDSSwitchWidget,
WDSSwitchGroupWidget,
WDSRadioGroupWidget,
WDSMenuButtonWidget,
CustomWidget,
WDSSectionWidget,
WDSZoneWidget,
WDSParagraphWidget,
WDSHeadingWidget,
WDSModalWidget,
WDSStatsWidget,
WDSKeyValueWidget,
WDSInlineButtonsWidget,
WDSEmailInputWidget,
WDSPasswordInputWidget,
WDSNumberInputWidget,
WDSMultilineInputWidget,
WDSSelectWidget,
WDSDatePickerWidget,
WDSCustomWidget,
WDSMultiSelectWidget,
];
const Widgets = [
...WDSWidgets,
...DeprecatedWidgets,
...LegacyWidgets,
// Create widget loader map
const WidgetLoaders = new Map<string, () => Promise<typeof BaseWidget>>([
...EEWDSWidgets,
...EEWidgets,
] as (typeof BaseWidget)[];
// WDS Widgets
[
"WDS_BUTTON_WIDGET",
async () =>
import("widgets/wds/WDSButtonWidget").then((m) => m.WDSButtonWidget),
],
[
"WDS_INPUT_WIDGET",
async () =>
import("widgets/wds/WDSInputWidget").then((m) => m.WDSInputWidget),
],
[
"WDS_CHECKBOX_WIDGET",
async () =>
import("widgets/wds/WDSCheckboxWidget").then((m) => m.WDSCheckboxWidget),
],
[
"WDS_ICON_BUTTON_WIDGET",
async () =>
import("widgets/wds/WDSIconButtonWidget").then(
(m) => m.WDSIconButtonWidget,
),
],
[
"WDS_TABLE_WIDGET",
async () =>
import("widgets/wds/WDSTableWidget").then((m) => m.WDSTableWidget),
],
[
"WDS_CURRENCY_INPUT_WIDGET",
async () =>
import("widgets/wds/WDSCurrencyInputWidget").then(
(m) => m.WDSCurrencyInputWidget,
),
],
[
"WDS_TOOLBAR_BUTTONS_WIDGET",
async () =>
import("widgets/wds/WDSToolbarButtonsWidget").then(
(m) => m.WDSToolbarButtonsWidget,
),
],
[
"WDS_PHONE_INPUT_WIDGET",
async () =>
import("widgets/wds/WDSPhoneInputWidget").then(
(m) => m.WDSPhoneInputWidget,
),
],
[
"WDS_CHECKBOX_GROUP_WIDGET",
async () =>
import("widgets/wds/WDSCheckboxGroupWidget").then(
(m) => m.WDSCheckboxGroupWidget,
),
],
[
"WDS_COMBO_BOX_WIDGET",
async () =>
import("widgets/wds/WDSComboBoxWidget").then((m) => m.WDSComboBoxWidget),
],
[
"WDS_SWITCH_WIDGET",
async () =>
import("widgets/wds/WDSSwitchWidget").then((m) => m.WDSSwitchWidget),
],
[
"WDS_SWITCH_GROUP_WIDGET",
async () =>
import("widgets/wds/WDSSwitchGroupWidget").then(
(m) => m.WDSSwitchGroupWidget,
),
],
[
"WDS_RADIO_GROUP_WIDGET",
async () =>
import("widgets/wds/WDSRadioGroupWidget").then(
(m) => m.WDSRadioGroupWidget,
),
],
[
"WDS_MENU_BUTTON_WIDGET",
async () =>
import("widgets/wds/WDSMenuButtonWidget").then(
(m) => m.WDSMenuButtonWidget,
),
],
[
"CUSTOM_WIDGET",
async () => import("./CustomWidget").then((m) => m.default),
],
[
anvilWidgets.SECTION_WIDGET,
async () =>
import("widgets/wds/WDSSectionWidget").then((m) => m.WDSSectionWidget),
],
[
anvilWidgets.ZONE_WIDGET,
async () =>
import("widgets/wds/WDSZoneWidget").then((m) => m.WDSZoneWidget),
],
[
"WDS_PARAGRAPH_WIDGET",
async () =>
import("widgets/wds/WDSParagraphWidget").then(
(m) => m.WDSParagraphWidget,
),
],
[
"WDS_HEADING_WIDGET",
async () =>
import("widgets/wds/WDSHeadingWidget").then((m) => m.WDSHeadingWidget),
],
[
"WDS_MODAL_WIDGET",
async () =>
import("widgets/wds/WDSModalWidget").then((m) => m.WDSModalWidget),
],
[
"WDS_STATS_WIDGET",
async () =>
import("widgets/wds/WDSStatsWidget").then((m) => m.WDSStatsWidget),
],
[
"WDS_KEY_VALUE_WIDGET",
async () =>
import("widgets/wds/WDSKeyValueWidget").then((m) => m.WDSKeyValueWidget),
],
[
"WDS_INLINE_BUTTONS_WIDGET",
async () =>
import("widgets/wds/WDSInlineButtonsWidget").then(
(m) => m.WDSInlineButtonsWidget,
),
],
[
"WDS_EMAIL_INPUT_WIDGET",
async () =>
import("widgets/wds/WDSEmailInputWidget").then(
(m) => m.WDSEmailInputWidget,
),
],
[
"WDS_PASSWORD_INPUT_WIDGET",
async () =>
import("widgets/wds/WDSPasswordInputWidget").then(
(m) => m.WDSPasswordInputWidget,
),
],
[
"WDS_NUMBER_INPUT_WIDGET",
async () =>
import("widgets/wds/WDSNumberInputWidget").then(
(m) => m.WDSNumberInputWidget,
),
],
[
"WDS_MULTILINE_INPUT_WIDGET",
async () =>
import("widgets/wds/WDSMultilineInputWidget").then(
(m) => m.WDSMultilineInputWidget,
),
],
[
"WDS_SELECT_WIDGET",
async () =>
import("widgets/wds/WDSSelectWidget").then((m) => m.WDSSelectWidget),
],
[
"WDS_DATEPICKER_WIDGET",
async () =>
import("widgets/wds/WDSDatePickerWidget").then(
(m) => m.WDSDatePickerWidget,
),
],
[
"WDS_MULTI_SELECT_WIDGET",
async () =>
import("widgets/wds/WDSMultiSelectWidget").then(
(m) => m.WDSMultiSelectWidget,
),
],
export default Widgets;
// Legacy Widgets
[
"CANVAS_WIDGET",
async () => import("./CanvasWidget").then((m) => m.default),
],
[
"SKELETON_WIDGET",
async () => import("./SkeletonWidget").then((m) => m.default),
],
[
"CONTAINER_WIDGET",
async () => import("./ContainerWidget").then((m) => m.default),
],
["TEXT_WIDGET", async () => import("./TextWidget").then((m) => m.default)],
["TABLE_WIDGET", async () => import("./TableWidget").then((m) => m.default)],
[
"CHECKBOX_WIDGET",
async () => import("./CheckboxWidget").then((m) => m.default),
],
[
"RADIO_GROUP_WIDGET",
async () => import("./RadioGroupWidget").then((m) => m.default),
],
[
"BUTTON_WIDGET",
async () => import("./ButtonWidget").then((m) => m.default),
],
["IMAGE_WIDGET", async () => import("./ImageWidget").then((m) => m.default)],
["VIDEO_WIDGET", async () => import("./VideoWidget").then((m) => m.default)],
["TABS_WIDGET", async () => import("./TabsWidget").then((m) => m.default)],
["MODAL_WIDGET", async () => import("./ModalWidget").then((m) => m.default)],
["CHART_WIDGET", async () => import("./ChartWidget").then((m) => m.default)],
["MAP_WIDGET", async () => import("./MapWidget").then((m) => m.default)],
[
"RICH_TEXT_EDITOR_WIDGET",
async () => import("./RichTextEditorWidget").then((m) => m.default),
],
[
"DATE_PICKER_WIDGET2",
async () => import("./DatePickerWidget2").then((m) => m.default),
],
[
"SWITCH_WIDGET",
async () => import("./SwitchWidget").then((m) => m.default),
],
["FORM_WIDGET", async () => import("./FormWidget").then((m) => m.default)],
["RATE_WIDGET", async () => import("./RateWidget").then((m) => m.default)],
[
"IFRAME_WIDGET",
async () => import("./IframeWidget").then((m) => m.default),
],
[
"TABS_MIGRATOR_WIDGET",
async () => import("./TabsMigrator").then((m) => m.default),
],
[
"DIVIDER_WIDGET",
async () => import("./DividerWidget").then((m) => m.default),
],
[
"MENU_BUTTON_WIDGET",
async () => import("./MenuButtonWidget").then((m) => m.default),
],
[
"ICON_BUTTON_WIDGET",
async () => import("./IconButtonWidget").then((m) => m.default),
],
[
"CHECKBOX_GROUP_WIDGET",
async () => import("./CheckboxGroupWidget").then((m) => m.default),
],
[
"FILE_PICKER_WIDGET_V2",
async () => import("./FilePickerWidgetV2").then((m) => m.default),
],
[
"STATBOX_WIDGET",
async () => import("./StatboxWidget").then((m) => m.default),
],
[
"AUDIO_RECORDER_WIDGET",
async () => import("./AudioRecorderWidget").then((m) => m.default),
],
[
"DOCUMENT_VIEWER_WIDGET",
async () => import("./DocumentViewerWidget").then((m) => m.default),
],
[
"BUTTON_GROUP_WIDGET",
async () => import("./ButtonGroupWidget").then((m) => m.default),
],
[
"WDS_CUSTOM_WIDGET",
async () =>
import("widgets/wds/WDSCustomWidget").then((m) => m.WDSCustomWidget),
],
[
"MULTI_SELECT_TREE_WIDGET",
async () => import("./MultiSelectTreeWidget").then((m) => m.default),
],
[
"SINGLE_SELECT_TREE_WIDGET",
async () => import("./SingleSelectTreeWidget").then((m) => m.default),
],
[
"SWITCH_GROUP_WIDGET",
async () => import("./SwitchGroupWidget").then((m) => m.default),
],
["AUDIO_WIDGET", async () => import("./AudioWidget").then((m) => m.default)],
[
"PROGRESSBAR_WIDGET",
async () => import("./ProgressBarWidget").then((m) => m.default),
],
[
"CAMERA_WIDGET",
async () => import("./CameraWidget").then((m) => m.default),
],
[
"MAP_CHART_WIDGET",
async () => import("./MapChartWidget").then((m) => m.default),
],
[
"SELECT_WIDGET",
async () => import("./SelectWidget").then((m) => m.default),
],
[
"MULTI_SELECT_WIDGET_V2",
async () => import("./MultiSelectWidgetV2").then((m) => m.default),
],
[
"MULTI_SELECT_WIDGET",
async () => import("./MultiSelectWidget").then((m) => m.default),
],
[
"INPUT_WIDGET_V2",
async () => import("./InputWidgetV2").then((m) => m.default),
],
[
"PHONE_INPUT_WIDGET",
async () => import("./PhoneInputWidget").then((m) => m.default),
],
[
"CURRENCY_INPUT_WIDGET",
async () => import("./CurrencyInputWidget").then((m) => m.default),
],
[
"JSON_FORM_WIDGET",
async () => import("./JSONFormWidget").then((m) => m.default),
],
[
"TABLE_WIDGET_V2",
async () => import("./TableWidgetV2").then((m) => m.default),
],
[
"NUMBER_SLIDER_WIDGET",
async () => import("./NumberSliderWidget").then((m) => m.default),
],
[
"RANGE_SLIDER_WIDGET",
async () => import("./RangeSliderWidget").then((m) => m.default),
],
[
"CATEGORY_SLIDER_WIDGET",
async () => import("./CategorySliderWidget").then((m) => m.default),
],
[
"CODE_SCANNER_WIDGET",
async () => import("./CodeScannerWidget").then((m) => m.default),
],
[
"LIST_WIDGET_V2",
async () => import("./ListWidgetV2").then((m) => m.default),
],
[
"EXTERNAL_WIDGET",
async () => import("./ExternalWidget").then((m) => m.default),
],
// Deprecated Widgets
[
"DROP_DOWN_WIDGET",
async () => import("./DropdownWidget").then((m) => m.default),
],
["ICON_WIDGET", async () => import("./IconWidget").then((m) => m.default)],
[
"FILE_PICKER_WIDGET",
async () => import("./FilepickerWidget").then((m) => m.default),
],
[
"FORM_BUTTON_WIDGET",
async () => import("./FormButtonWidget").then((m) => m.default),
],
[
"PROGRESS_WIDGET",
async () => import("./ProgressWidget").then((m) => m.default),
],
[
"CIRCULAR_PROGRESS_WIDGET",
async () => import("./CircularProgressWidget").then((m) => m.default),
],
["LIST_WIDGET", async () => import("./ListWidget").then((m) => m.default)],
[
"DATE_PICKER_WIDGET",
async () => import("./DatePickerWidget").then((m) => m.default),
],
["INPUT_WIDGET", async () => import("./InputWidget").then((m) => m.default)],
]);
// Cache for loaded widgets
const loadedWidgets = new Map<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 { runSaga } from "redux-saga";
import { TriggerKind } from "constants/AppsmithActionConstants/ActionConstants";
import { registerAllWidgets } from "utils/editor/EditorUtils";
export async function UNSTABLE_executeDynamicTrigger(dynamicTrigger: string) {
const state = store.getState();
await registerAllWidgets();
const unEvalTree = getUnevaluatedDataTree(state);
const result = runSaga(

View File

@ -290,75 +290,64 @@ export async function loadLibraries(
const libStore: Record<string, unknown> = {};
try {
for (const lib of libs) {
const url = lib.url as string;
const accessors = lib.accessor;
const keysBefore = Object.keys(self);
let module = null;
await Promise.all(
libs.map(async (lib) => {
const url = lib.url as string;
const accessors = lib.accessor;
const keysBefore = Object.keys(self);
let module = null;
try {
self.importScripts(url);
const keysAfter = Object.keys(self);
let defaultAccessors = difference(keysAfter, keysBefore);
try {
self.importScripts(url);
const keysAfter = Object.keys(self);
let defaultAccessors = difference(keysAfter, keysBefore);
// Changing default export to library accessors name which was saved when it was installed, if default export present
movetheDefaultExportedLibraryToAccessorKey(
defaultAccessors,
accessors[0],
);
movetheDefaultExportedLibraryToAccessorKey(
defaultAccessors,
accessors[0],
);
// Following the same process which was happening earlier
const keysAfterDefaultOperation = Object.keys(self);
const keysAfterDefaultOperation = Object.keys(self);
defaultAccessors = difference(keysAfterDefaultOperation, keysBefore);
defaultAccessors = difference(keysAfterDefaultOperation, keysBefore);
/**
* Installing 2 different version of lodash tries to add the same accessor on the self object. Let take version a & b for example.
* Installation of version a, will add _ to the self object and can be detected by looking at the differences in the previous step.
* Now when version b is installed, differences will be [], since _ already exists in the self object.
* We add all the installations to the libStore and see if the reference it points to in the self object changes.
* If the references changes it means that it a valid accessor.
*/
defaultAccessors.push(
...Object.keys(libStore).filter((k) => libStore[k] !== self[k]),
);
defaultAccessors.push(
...Object.keys(libStore).filter((k) => libStore[k] !== self[k]),
);
/**
* Sort the accessor list from backend and installed accessor list using the same rule to apply all modifications.
* This is required only for UMD builds, since we always generate unique names for ESM.
*/
accessors.sort();
defaultAccessors.sort();
accessors.sort();
defaultAccessors.sort();
for (let i = 0; i < defaultAccessors.length; i++) {
self[accessors[i]] = self[defaultAccessors[i]];
libStore[defaultAccessors[i]] = self[defaultAccessors[i]];
libraryReservedIdentifiers[accessors[i]] = true;
invalidEntityIdentifiers[accessors[i]] = true;
for (let i = 0; i < defaultAccessors.length; i++) {
self[accessors[i]] = self[defaultAccessors[i]];
libStore[defaultAccessors[i]] = self[defaultAccessors[i]];
libraryReservedIdentifiers[accessors[i]] = true;
invalidEntityIdentifiers[accessors[i]] = true;
}
return;
} catch (e) {
log.debug(e);
}
continue;
} catch (e) {
log.debug(e);
}
try {
module = await import(/* webpackIgnore: true */ url);
try {
module = await import(/* webpackIgnore: true */ url);
if (!module || typeof module !== "object") throw "Not an ESM module";
if (!module || typeof module !== "object") throw "Not an ESM module";
const key = accessors[0];
const flattenedModule = flattenModule(module);
const key = accessors[0];
const flattenedModule = flattenModule(module);
libStore[key] = flattenedModule;
self[key] = flattenedModule;
libraryReservedIdentifiers[key] = true;
invalidEntityIdentifiers[key] = true;
} catch (e) {
log.debug(e);
throw new ImportError(url);
}
}
libStore[key] = flattenedModule;
self[key] = flattenedModule;
libraryReservedIdentifiers[key] = true;
invalidEntityIdentifiers[key] = true;
} catch (e) {
log.debug(e);
throw new ImportError(url);
}
}),
);
JSLibraries.push(...libs);
JSLibraryAccessor.regenerateSet();

View File

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