Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 13 additions & 9 deletions app/client/src/ce/actions/evaluationActionsList.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,18 @@ export const LOG_REDUX_ACTIONS = {
[ReduxActionTypes.UPDATE_ACTION_PROPERTY]: true,
};

export const JS_ACTIONS = [
ReduxActionTypes.CREATE_JS_ACTION_SUCCESS,
ReduxActionTypes.DELETE_JS_ACTION_SUCCESS,
ReduxActionTypes.COPY_JS_ACTION_SUCCESS,
ReduxActionTypes.MOVE_JS_ACTION_SUCCESS,
ReduxActionErrorTypes.FETCH_JS_ACTIONS_ERROR,
ReduxActionTypes.FETCH_JS_ACTIONS_FOR_PAGE_SUCCESS,
ReduxActionTypes.FETCH_JS_ACTIONS_VIEW_MODE_SUCCESS,
ReduxActionErrorTypes.FETCH_JS_ACTIONS_VIEW_MODE_ERROR,
ReduxActionTypes.UPDATE_JS_ACTION_BODY_SUCCESS,
];

export const EVALUATE_REDUX_ACTIONS = [
...FIRST_EVAL_REDUX_ACTIONS,
// Actions
Expand All @@ -65,15 +77,7 @@ export const EVALUATE_REDUX_ACTIONS = [
ReduxActionErrorTypes.RUN_ACTION_ERROR,
ReduxActionTypes.CLEAR_ACTION_RESPONSE,
// JS Actions
ReduxActionTypes.CREATE_JS_ACTION_SUCCESS,
ReduxActionTypes.DELETE_JS_ACTION_SUCCESS,
ReduxActionTypes.COPY_JS_ACTION_SUCCESS,
ReduxActionTypes.MOVE_JS_ACTION_SUCCESS,
ReduxActionErrorTypes.FETCH_JS_ACTIONS_ERROR,
ReduxActionTypes.FETCH_JS_ACTIONS_FOR_PAGE_SUCCESS,
ReduxActionTypes.FETCH_JS_ACTIONS_VIEW_MODE_SUCCESS,
ReduxActionErrorTypes.FETCH_JS_ACTIONS_VIEW_MODE_ERROR,
ReduxActionTypes.UPDATE_JS_ACTION_BODY_SUCCESS,
...JS_ACTIONS,
// App Data
ReduxActionTypes.SET_APP_MODE,
ReduxActionTypes.FETCH_USER_DETAILS_SUCCESS,
Expand Down
6 changes: 5 additions & 1 deletion app/client/src/ce/constants/ReduxActionConstants.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import type {
import type { AppLayoutConfig } from "reducers/entityReducers/pageListReducer";
import type { DSLWidget } from "WidgetProvider/constants";
import type { LayoutSystemTypeConfig } from "layoutSystems/types";
import type { AffectedJSObjects } from "sagas/EvaluationsSagaUtils";

export const ReduxSagaChannels = {
WEBSOCKET_APP_LEVEL_WRITE_CHANNEL: "WEBSOCKET_APP_LEVEL_WRITE_CHANNEL",
Expand Down Expand Up @@ -1168,7 +1169,9 @@ export interface ReduxAction<T> {
type: ReduxActionType | ReduxActionErrorType;
payload: T;
}

export interface BufferedReduxAction<T> extends ReduxAction<T> {
affectedJSObjects: AffectedJSObjects;
}
Comment on lines 1172 to 1174

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Define the BufferedReduxAction interface more clearly.

- export interface BufferedReduxAction<T> extends ReduxAction<T> {
-   affectedJSObjects: AffectedJSObjects;
- }
+ /**
+  * Represents a Redux action that is buffered and includes information about affected JavaScript objects.
+  * @template T The type of the payload.
+  */
+ export interface BufferedReduxAction<T> extends ReduxAction<T> {
+   affectedJSObjects: AffectedJSObjects;
+ }

Adding documentation comments will improve code readability and maintainability.


Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation.

Suggested change
export interface BufferedReduxAction<T> extends ReduxAction<T> {
affectedJSObjects: AffectedJSObjects;
}
/**
* Represents a Redux action that is buffered and includes information about affected JavaScript objects.
* @template T The type of the payload.
*/
export interface BufferedReduxAction<T> extends ReduxAction<T> {
affectedJSObjects: AffectedJSObjects;
}

export type ReduxActionWithoutPayload = Pick<ReduxAction<undefined>, "type">;

export interface ReduxActionWithMeta<T, M> extends ReduxAction<T> {
Expand All @@ -1186,6 +1189,7 @@ export type AnyReduxAction = ReduxAction<unknown> | ReduxActionWithoutPayload;

export interface EvaluationReduxAction<T> extends ReduxAction<T> {
postEvalActions?: Array<AnyReduxAction>;
affectedJSObjects?: AffectedJSObjects;
}

export interface PromisePayload {
Expand Down
71 changes: 71 additions & 0 deletions app/client/src/ce/sagas/InferAffectedJSObjects.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import type {
BufferedReduxAction,
ReduxAction,
} from "@appsmith/constants/ReduxActionConstants";
import {
ReduxActionErrorTypes,
ReduxActionTypes,
} from "@appsmith/constants/ReduxActionConstants";
import { JS_ACTIONS } from "@appsmith/actions/evaluationActionsList";
import type { AffectedJSObjects } from "sagas/EvaluationsSagaUtils";
import type { JSCollection } from "entities/JSCollection";

export function getAffectedJSObjectIdsFromJSAction(
action: ReduxAction<unknown> | BufferedReduxAction<unknown>,
): AffectedJSObjects {
if (!JS_ACTIONS.includes(action.type)) {
return {
ids: [],
isAllAffected: false,
};
}
// only JS actions here
action as ReduxAction<unknown>;
// When fetching JSActions fails, we need to diff all JSObjects because the reducer updates it
// to empty collection
if (
action.type === ReduxActionErrorTypes.FETCH_JS_ACTIONS_ERROR ||
action.type === ReduxActionErrorTypes.FETCH_JS_ACTIONS_VIEW_MODE_ERROR
) {
return {
isAllAffected: true,
ids: [],
};
}

const { payload } = action as ReduxAction<{
data: JSCollection;
}> &
ReduxAction<JSCollection>;
// some actions have within data property of the action payload, we need to extract it from there
const innerData = payload?.data || payload;

const ids = Array.isArray(innerData)
? innerData.map(({ id }) => id)
: [innerData.id];

return { ids, isAllAffected: false };
}

function getAffectedJSObjectIdsFromBufferedAction(
action: ReduxAction<unknown> | BufferedReduxAction<unknown>,
): AffectedJSObjects {
if (action.type !== ReduxActionTypes.BUFFERED_ACTION) {
return {
ids: [],
isAllAffected: false,
};
}
// only Buffered actions here
return (
(action as BufferedReduxAction<unknown>).affectedJSObjects || {
ids: [],
isAllAffected: false,
}
);
}

export const AFFECTED_JS_OBJECTS_FNS = [
getAffectedJSObjectIdsFromJSAction,
getAffectedJSObjectIdsFromBufferedAction,
];
1 change: 1 addition & 0 deletions app/client/src/ee/sagas/InferAffectedJSObjects.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "ce/sagas/InferAffectedJSObjects";
41 changes: 0 additions & 41 deletions app/client/src/sagas/EvaluationSaga.utils.ts

This file was deleted.

119 changes: 118 additions & 1 deletion app/client/src/sagas/EvaluationsSaga.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import { evaluateTreeSaga, evalWorker } from "./EvaluationsSaga";
import {
defaultAffectedJSObjects,
evalQueueBuffer,
evaluateTreeSaga,
evalWorker,
} from "./EvaluationsSaga";
import { expectSaga } from "redux-saga-test-plan";
import { EVAL_WORKER_ACTIONS } from "@appsmith/workers/Evaluation/evalWorkerActions";
import { select } from "redux-saga/effects";
Expand All @@ -7,6 +12,14 @@ import { getAllActionValidationConfig } from "@appsmith//selectors/entitiesSelec
import { getSelectedAppTheme } from "selectors/appThemingSelectors";
import { getAppMode } from "@appsmith/selectors/applicationSelectors";
import * as log from "loglevel";

import type { ReduxAction } from "@appsmith/constants/ReduxActionConstants";
import {
ReduxActionErrorTypes,
ReduxActionTypes,
} from "@appsmith/constants/ReduxActionConstants";
import { fetchPluginFormConfigsSuccess } from "actions/pluginActions";
import { createJSCollectionSuccess } from "actions/jsActionActions";
jest.mock("loglevel");

describe("evaluateTreeSaga", () => {
Expand Down Expand Up @@ -37,6 +50,7 @@ describe("evaluateTreeSaga", () => {
appMode: false,
widgetsMeta: {},
shouldRespondWithLogs: true,
affectedJSObjects: { ids: [], isAllAffected: false },
})
.run();
});
Expand Down Expand Up @@ -64,7 +78,110 @@ describe("evaluateTreeSaga", () => {
appMode: false,
widgetsMeta: {},
shouldRespondWithLogs: false,
affectedJSObjects: { ids: [], isAllAffected: false },
})
.run();
});
test("should propagate affectedJSObjects property to evaluation action", async () => {
const unEvalAndConfigTree = { unEvalTree: {}, configTree: {} };
const affectedJSObjects = {
isAllAffected: false,
ids: ["1", "2"],
};

return expectSaga(
evaluateTreeSaga,
unEvalAndConfigTree,
[],
undefined,
undefined,
undefined,
affectedJSObjects,
)
.provide([
[select(getAllActionValidationConfig), {}],
[select(getWidgets), {}],
[select(getMetaWidgets), {}],
[select(getSelectedAppTheme), {}],
[select(getAppMode), false],
[select(getWidgetsMeta), {}],
])
.call(evalWorker.request, EVAL_WORKER_ACTIONS.EVAL_TREE, {
unevalTree: unEvalAndConfigTree,
widgetTypeConfigMap: undefined,
widgets: {},
theme: {},
shouldReplay: true,
allActionValidationConfig: {},
forceEvaluation: false,
metaWidgets: {},
appMode: false,
widgetsMeta: {},
shouldRespondWithLogs: false,
affectedJSObjects,
})
.run();
});
});

describe("evalQueueBuffer", () => {
test("should return a buffered action with the default affectedJSObjects state for an action which does not have affectedJSObjects associated to it", () => {
const buffer = evalQueueBuffer();
// this action does not generate an affectedJSObject
buffer.put(fetchPluginFormConfigsSuccess({} as any));
const bufferedAction = buffer.take();
expect(bufferedAction).toEqual({
type: ReduxActionTypes.BUFFERED_ACTION,
affectedJSObjects: defaultAffectedJSObjects,
postEvalActions: [],
});
});
test("should club all JS actions affectedJSObjects's ids", () => {
const buffer = evalQueueBuffer();
buffer.put(createJSCollectionSuccess({ id: "1" } as any));
buffer.put(createJSCollectionSuccess({ id: "2" } as any));
const bufferedAction = buffer.take();
expect(bufferedAction).toEqual({
type: ReduxActionTypes.BUFFERED_ACTION,
affectedJSObjects: { ids: ["1", "2"], isAllAffected: false },
postEvalActions: [],
});
});
test("should return all JS actions that have changed when there is a pending action which affects all JS actions ", () => {
const buffer = evalQueueBuffer();
buffer.put(createJSCollectionSuccess({ id: "1" } as any));
// this action triggers an isAllAffected flag
buffer.put({
type: ReduxActionErrorTypes.FETCH_JS_ACTIONS_ERROR,
} as ReduxAction<unknown>);
// queue is not empty
expect(buffer.isEmpty()).not.toBeTruthy();

const bufferedAction = buffer.take();
expect(bufferedAction).toEqual({
type: ReduxActionTypes.BUFFERED_ACTION,
affectedJSObjects: { ids: [], isAllAffected: true },
postEvalActions: [],
});
expect(buffer.isEmpty()).toBeTruthy();
});
test("should reset the collectedAffectedJSObjects after the buffered action has been dequeued and the subsequent actions should have the defaultAffectedJSObjects", () => {
const buffer = evalQueueBuffer();
buffer.put(createJSCollectionSuccess({ id: "1" } as any));
const bufferedAction = buffer.take();
expect(bufferedAction).toEqual({
type: ReduxActionTypes.BUFFERED_ACTION,
affectedJSObjects: { ids: ["1"], isAllAffected: false },
postEvalActions: [],
});
expect(buffer.isEmpty()).toBeTruthy();
// this action does not generate an affectedJSObject, So the subsequent buffered action should have default affectedJSObjects
buffer.put(fetchPluginFormConfigsSuccess({ id: "1" } as any));
const bufferedActionsWithDefaultAffectedJSObjects = buffer.take();
expect(bufferedActionsWithDefaultAffectedJSObjects).toEqual({
type: ReduxActionTypes.BUFFERED_ACTION,
affectedJSObjects: defaultAffectedJSObjects,
postEvalActions: [],
});
});
});
Loading