feat: Import javascript libraries (#17895)
This commit is contained in:
parent
585a19401b
commit
2dc7dc90e3
1
app/client/cypress/fixtures/library_export.json
Normal file
1
app/client/cypress/fixtures/library_export.json
Normal file
File diff suppressed because one or more lines are too long
|
|
@ -4,6 +4,7 @@ const {
|
|||
AggregateHelper: agHelper,
|
||||
CommonLocators: locator,
|
||||
EntityExplorer: ee,
|
||||
LibraryInstaller: installer,
|
||||
PropertyPane: propPane,
|
||||
} = ObjectsRegistry;
|
||||
|
||||
|
|
@ -66,9 +67,26 @@ describe("Autocomplete bug fixes", function() {
|
|||
it("4. feat #16426 Autocomplete for fast-xml-parser", function() {
|
||||
ee.SelectEntityByName("Text1");
|
||||
propPane.TypeTextIntoField("Text", "{{xmlParser.j");
|
||||
agHelper.GetNAssertElementText(locator._hints, "j2xParser()");
|
||||
agHelper.GetNAssertElementText(locator._hints, "j2xParser");
|
||||
|
||||
propPane.TypeTextIntoField("Text", "{{new xmlParser.j2xParser().p");
|
||||
agHelper.GetNAssertElementText(locator._hints, "parse()");
|
||||
agHelper.GetNAssertElementText(locator._hints, "parse");
|
||||
});
|
||||
|
||||
it("5. Installed library should show up in autocomplete", function() {
|
||||
ee.ExpandCollapseEntity("Libraries");
|
||||
installer.openInstaller();
|
||||
installer.installLibrary("uuidjs", "UUID");
|
||||
installer.closeInstaller();
|
||||
ee.SelectEntityByName("Text1");
|
||||
propPane.TypeTextIntoField("Text", "{{UUI");
|
||||
agHelper.GetNAssertElementText(locator._hints, "UUID");
|
||||
});
|
||||
|
||||
it("6. No autocomplete for Removed libraries", function() {
|
||||
ee.RenameEntityFromExplorer("Text1Copy", "UUIDTEXT");
|
||||
installer.uninstallLibrary("uuidjs");
|
||||
propPane.TypeTextIntoField("Text", "{{UUID.");
|
||||
agHelper.AssertElementAbsence(locator._hints);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ describe("Entity explorer context menu should hide on scrolling", function() {
|
|||
// Setup to make the explorer scrollable
|
||||
ee.ExpandCollapseEntity("Queries/JS");
|
||||
ee.ExpandCollapseEntity("Datasources");
|
||||
agHelper.ContainsNClick("Dependencies");
|
||||
agHelper.ContainsNClick("Libraries");
|
||||
dataSources.NavigateToDSCreateNew();
|
||||
agHelper.GetNClick(dataSources._mockDB("Users"));
|
||||
cy.wait("@getMockDb").then(($createdMock) => {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,36 @@
|
|||
import { WIDGET } from "../../../../locators/WidgetLocators";
|
||||
import { ObjectsRegistry } from "../../../../support/Objects/Registry";
|
||||
|
||||
const explorer = ObjectsRegistry.EntityExplorer;
|
||||
const installer = ObjectsRegistry.LibraryInstaller;
|
||||
const aggregateHelper = ObjectsRegistry.AggregateHelper;
|
||||
const homePage = ObjectsRegistry.HomePage;
|
||||
|
||||
describe("Tests JS Libraries", () => {
|
||||
it("1. Validates Library install/uninstall", () => {
|
||||
explorer.ExpandCollapseEntity("Libraries");
|
||||
installer.openInstaller();
|
||||
installer.installLibrary("uuidjs", "UUID");
|
||||
installer.uninstallLibrary("uuidjs");
|
||||
installer.assertUnInstall("uuidjs");
|
||||
});
|
||||
it("2. Checks for naming collision", () => {
|
||||
explorer.DragDropWidgetNVerify(WIDGET.TABLE, 200, 200);
|
||||
explorer.NavigateToSwitcher("explorer");
|
||||
explorer.RenameEntityFromExplorer("Table1", "jsonwebtoken");
|
||||
explorer.ExpandCollapseEntity("Libraries");
|
||||
installer.openInstaller();
|
||||
installer.installLibrary("jsonwebtoken", "jsonwebtoken", false);
|
||||
aggregateHelper.AssertContains("Name collision detected: jsonwebtoken");
|
||||
});
|
||||
it("3. Checks installation in exported app", () => {
|
||||
homePage.NavigateToHome();
|
||||
homePage.ImportApp("library_export.json");
|
||||
aggregateHelper.AssertContains("true");
|
||||
});
|
||||
it("4. Checks installation in duplicated app", () => {
|
||||
homePage.NavigateToHome();
|
||||
homePage.DuplicateApplication("Library_export");
|
||||
aggregateHelper.AssertContains("true");
|
||||
});
|
||||
});
|
||||
|
|
@ -6,7 +6,9 @@ const jsEditor = ObjectsRegistry.JSEditor,
|
|||
apiPage = ObjectsRegistry.ApiPage,
|
||||
agHelper = ObjectsRegistry.AggregateHelper,
|
||||
dataSources = ObjectsRegistry.DataSources,
|
||||
propPane = ObjectsRegistry.PropertyPane;
|
||||
propPane = ObjectsRegistry.PropertyPane,
|
||||
installer = ObjectsRegistry.LibraryInstaller,
|
||||
home = ObjectsRegistry.HomePage;
|
||||
|
||||
const successMessage = "Successful Trigger";
|
||||
const errorMessage = "Unsuccessful Trigger";
|
||||
|
|
@ -291,4 +293,48 @@ describe("Linting", () => {
|
|||
// expect no lint error
|
||||
agHelper.AssertElementAbsence(locator._lintErrorElement);
|
||||
});
|
||||
|
||||
it("9. Shows lint errors for usage of library that are not installed yet", () => {
|
||||
const JS_OBJECT_WITH_LIB_API = `export default {
|
||||
myFun1: () => {
|
||||
return UUID.generate();
|
||||
},
|
||||
}`;
|
||||
jsEditor.CreateJSObject(JS_OBJECT_WITH_LIB_API, {
|
||||
paste: true,
|
||||
completeReplace: true,
|
||||
toRun: false,
|
||||
shouldCreateNewJSObj: true,
|
||||
});
|
||||
|
||||
agHelper.AssertElementExist(locator._lintErrorElement);
|
||||
ee.ExpandCollapseEntity("Libraries");
|
||||
// install the library
|
||||
installer.openInstaller();
|
||||
installer.installLibrary("uuidjs", "UUID");
|
||||
installer.closeInstaller();
|
||||
|
||||
agHelper.AssertElementAbsence(locator._lintErrorElement);
|
||||
|
||||
installer.uninstallLibrary("uuidjs");
|
||||
|
||||
agHelper.AssertElementExist(locator._lintErrorElement);
|
||||
agHelper.Sleep(2000);
|
||||
installer.openInstaller();
|
||||
installer.installLibrary("uuidjs", "UUID");
|
||||
installer.closeInstaller();
|
||||
|
||||
home.NavigateToHome();
|
||||
|
||||
home.CreateNewApplication();
|
||||
|
||||
jsEditor.CreateJSObject(JS_OBJECT_WITH_LIB_API, {
|
||||
paste: true,
|
||||
completeReplace: true,
|
||||
toRun: false,
|
||||
shouldCreateNewJSObj: true,
|
||||
});
|
||||
|
||||
agHelper.AssertElementExist(locator._lintErrorElement);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import { DeployMode } from "../Pages/DeployModeHelper";
|
|||
import { GitSync } from "../Pages/GitSync";
|
||||
import { FakerHelper } from "../Pages/FakerHelper";
|
||||
import { DebuggerHelper } from "../Pages/DebuggerHelper";
|
||||
import { LibraryInstaller } from "../Pages/LibraryInstaller";
|
||||
import { AppSettings } from "../Pages/AppSettings/AppSettings";
|
||||
import { GeneralSettings } from "../Pages/AppSettings/GeneralSettings";
|
||||
import { PageSettings } from "../Pages/AppSettings/PageSettings";
|
||||
|
|
@ -161,6 +162,14 @@ export class ObjectsRegistry {
|
|||
}
|
||||
return ObjectsRegistry.themeSettings__;
|
||||
}
|
||||
|
||||
private static LibraryInstaller__: LibraryInstaller;
|
||||
static get LibraryInstaller(): LibraryInstaller {
|
||||
if (ObjectsRegistry.LibraryInstaller__ === undefined) {
|
||||
ObjectsRegistry.LibraryInstaller__ = new LibraryInstaller();
|
||||
}
|
||||
return ObjectsRegistry.LibraryInstaller__;
|
||||
}
|
||||
}
|
||||
|
||||
export const initLocalstorageRegistry = () => {
|
||||
|
|
|
|||
62
app/client/cypress/support/Pages/LibraryInstaller.ts
Normal file
62
app/client/cypress/support/Pages/LibraryInstaller.ts
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
import { ObjectsRegistry } from "../Objects/Registry";
|
||||
|
||||
export class LibraryInstaller {
|
||||
private _aggregateHelper = ObjectsRegistry.AggregateHelper;
|
||||
private _installer_trigger_locator = ".t--entity-add-btn.group.libraries";
|
||||
private _installer_close_locator = ".t--close-installer";
|
||||
|
||||
private getLibraryLocatorInExplorer(libraryName: string) {
|
||||
return `.t--installed-library-${libraryName}`;
|
||||
}
|
||||
|
||||
private getLibraryCardLocator(libraryName: string) {
|
||||
return `div.library-card.t--${libraryName}`;
|
||||
}
|
||||
|
||||
public openInstaller() {
|
||||
this._aggregateHelper.GetNClick(this._installer_trigger_locator);
|
||||
}
|
||||
|
||||
public closeInstaller() {
|
||||
this._aggregateHelper.GetNClick(this._installer_close_locator);
|
||||
}
|
||||
|
||||
public installLibrary(
|
||||
libraryName: string,
|
||||
accessor: string,
|
||||
checkIfSuccessful = true,
|
||||
) {
|
||||
cy.get(this.getLibraryCardLocator(libraryName))
|
||||
.find(".t--download")
|
||||
.click();
|
||||
if (checkIfSuccessful) this.assertInstall(libraryName, accessor);
|
||||
}
|
||||
|
||||
private assertInstall(libraryName: string, accessor: string) {
|
||||
this._aggregateHelper.AssertContains(
|
||||
`Installation Successful. You can access the library via ${accessor}`,
|
||||
);
|
||||
cy.get(this.getLibraryCardLocator(libraryName))
|
||||
.find(".installed")
|
||||
.should("be.visible");
|
||||
this._aggregateHelper.AssertElementExist(
|
||||
this.getLibraryLocatorInExplorer(libraryName),
|
||||
);
|
||||
}
|
||||
|
||||
public uninstallLibrary(libraryName: string) {
|
||||
cy.get(this.getLibraryLocatorInExplorer(libraryName))
|
||||
.realHover()
|
||||
.find(".t--uninstall-library")
|
||||
.click();
|
||||
}
|
||||
|
||||
public assertUnInstall(libraryName: string) {
|
||||
this._aggregateHelper.AssertContains(
|
||||
`${libraryName} is uninstalled successfully.`,
|
||||
);
|
||||
this._aggregateHelper.AssertElementAbsence(
|
||||
this.getLibraryLocatorInExplorer(libraryName),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -17,7 +17,7 @@ module.exports = {
|
|||
moduleFileExtensions: ["ts", "tsx", "js", "jsx", "json", "node", "css"],
|
||||
moduleDirectories: ["node_modules", "src", "test"],
|
||||
transformIgnorePatterns: [
|
||||
"<rootDir>/node_modules/(?!codemirror|design-system|react-dnd|dnd-core|@babel|(@blueprintjs/core/lib/esnext)|(@blueprintjs/core/lib/esm)|@github|lodash-es|@draft-js-plugins|react-documents)",
|
||||
"<rootDir>/node_modules/(?!codemirror|design-system|react-dnd|dnd-core|@babel|(@blueprintjs/core/lib/esnext)|(@blueprintjs/core/lib/esm)|@github|lodash-es|@draft-js-plugins|react-documents|linkedom)",
|
||||
],
|
||||
moduleNameMapper: {
|
||||
"\\.(css|less)$": "<rootDir>/test/__mocks__/styleMock.js",
|
||||
|
|
|
|||
|
|
@ -70,6 +70,7 @@
|
|||
"jshint": "^2.13.4",
|
||||
"klona": "^2.0.5",
|
||||
"libphonenumber-js": "^1.9.44",
|
||||
"linkedom": "^0.14.20",
|
||||
"lint-staged": "^13.0.3",
|
||||
"localforage": "^1.7.3",
|
||||
"lodash": "^4.17.21",
|
||||
|
|
|
|||
1
app/client/public/libraries/fast-csv@4.3.6.js
Normal file
1
app/client/public/libraries/fast-csv@4.3.6.js
Normal file
File diff suppressed because one or more lines are too long
1
app/client/public/libraries/jsonwebtoken@8.5.1.js
Normal file
1
app/client/public/libraries/jsonwebtoken@8.5.1.js
Normal file
File diff suppressed because one or more lines are too long
36
app/client/src/actions/JSLibraryActions.ts
Normal file
36
app/client/src/actions/JSLibraryActions.ts
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
import { ReduxActionTypes } from "@appsmith/constants/ReduxActionConstants";
|
||||
import { TJSLibrary } from "workers/common/JSLibrary";
|
||||
|
||||
export function fetchJSLibraries(applicationId: string) {
|
||||
return {
|
||||
type: ReduxActionTypes.FETCH_JS_LIBRARIES_INIT,
|
||||
payload: applicationId,
|
||||
};
|
||||
}
|
||||
|
||||
export function installLibraryInit(payload: Partial<TJSLibrary>) {
|
||||
return {
|
||||
type: ReduxActionTypes.INSTALL_LIBRARY_INIT,
|
||||
payload,
|
||||
};
|
||||
}
|
||||
|
||||
export function toggleInstaller(payload: boolean) {
|
||||
return {
|
||||
type: ReduxActionTypes.TOGGLE_INSTALLER,
|
||||
payload,
|
||||
};
|
||||
}
|
||||
|
||||
export function uninstallLibraryInit(payload: TJSLibrary) {
|
||||
return {
|
||||
type: ReduxActionTypes.UNINSTALL_LIBRARY_INIT,
|
||||
payload,
|
||||
};
|
||||
}
|
||||
|
||||
export function clearInstalls() {
|
||||
return {
|
||||
type: ReduxActionTypes.CLEAR_PROCESSED_INSTALLS,
|
||||
};
|
||||
}
|
||||
30
app/client/src/api/LibraryAPI.tsx
Normal file
30
app/client/src/api/LibraryAPI.tsx
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
import { APP_MODE } from "entities/App";
|
||||
import { TJSLibrary } from "workers/common/JSLibrary";
|
||||
import Api from "./Api";
|
||||
|
||||
export default class LibraryApi extends Api {
|
||||
static base_url = "v1/libraries";
|
||||
|
||||
static getUpdateLibraryBaseURL = (applicationId: string) =>
|
||||
`${LibraryApi.base_url}/${applicationId}`;
|
||||
|
||||
static addLibrary(
|
||||
applicationId: string,
|
||||
library: Partial<TJSLibrary> & { defs: string },
|
||||
) {
|
||||
const url = LibraryApi.getUpdateLibraryBaseURL(applicationId) + "/add";
|
||||
return Api.patch(url, library);
|
||||
}
|
||||
|
||||
static removeLibrary(applicationId: string, library: Partial<TJSLibrary>) {
|
||||
const url = LibraryApi.getUpdateLibraryBaseURL(applicationId) + "/remove";
|
||||
return Api.patch(url, library);
|
||||
}
|
||||
|
||||
static getLibraries(applicationId: string, mode: APP_MODE) {
|
||||
const url = `${LibraryApi.getUpdateLibraryBaseURL(applicationId)}${
|
||||
mode === APP_MODE.PUBLISHED ? "/view" : ""
|
||||
}`;
|
||||
return Api.get(url);
|
||||
}
|
||||
}
|
||||
|
|
@ -18,6 +18,15 @@ export const ReduxSagaChannels = {
|
|||
};
|
||||
|
||||
export const ReduxActionTypes = {
|
||||
TOGGLE_INSTALLER: "TOGGLE_INSTALLER",
|
||||
FETCH_JS_LIBRARIES_INIT: "FETCH_JS_LIBRARIES_INIT",
|
||||
FETCH_JS_LIBRARIES_SUCCESS: "FETCH_JS_LIBRARIES_SUCCESS",
|
||||
CLEAR_PROCESSED_INSTALLS: "CLEAR_PROCESSED_INSTALLS",
|
||||
INSTALL_LIBRARY_INIT: "INSTALL_LIBRARY_INIT",
|
||||
INSTALL_LIBRARY_START: "INSTALL_LIBRARY_START",
|
||||
INSTALL_LIBRARY_SUCCESS: "INSTALL_LIBRARY_SUCCESS",
|
||||
UNINSTALL_LIBRARY_INIT: "UNINSTALL_LIBRARY_INIT",
|
||||
UNINSTALL_LIBRARY_SUCCESS: "UNINSTALL_LIBRARY_SUCCESS",
|
||||
GIT_DISCARD_CHANGES_SUCCESS: "GIT_DISCARD_CHANGES_SUCCESS",
|
||||
GIT_DISCARD_CHANGES: "GIT_DISCARD_CHANGES",
|
||||
DELETE_BRANCH_INIT: "DELETE_BRANCH_INIT",
|
||||
|
|
@ -734,6 +743,8 @@ export const ReduxActionTypes = {
|
|||
SET_LINT_ERRORS: "SET_LINT_ERRORS",
|
||||
SET_AUTO_HEIGHT_WITH_LIMITS_CHANGING: "SET_AUTO_HEIGHT_WITH_LIMITS_CHANGING",
|
||||
PROCESS_AUTO_HEIGHT_UPDATES: "PROCESS_AUTO_HEIGHT_UPDATES",
|
||||
LINT_TREE: "LINT_TREE",
|
||||
UPDATE_LINT_GLOBALS: "UPDATE_LINT_GLOBALS",
|
||||
REMOVE_TEMP_DATASOURCE_SUCCESS: "REMOVE_TEMP_DATASOURCE_SUCCESS",
|
||||
SET_DATASOURCE_SAVE_ACTION_FLAG: "SET_DATASOURCE_SAVE_ACTION_FLAG",
|
||||
SET_DATASOURCE_SAVE_ACTION_FROM_POPUP_FLAG:
|
||||
|
|
@ -909,6 +920,9 @@ export const ReduxActionErrorTypes = {
|
|||
GET_TEMPLATE_ERROR: "GET_TEMPLATE_ERROR",
|
||||
GET_TEMPLATE_FILTERS_ERROR: "GET_TEMPLATE_FILTERS_ERROR",
|
||||
FETCH_CURRENT_TENANT_CONFIG_ERROR: "FETCH_CURRENT_TENANT_CONFIG_ERROR",
|
||||
INSTALL_LIBRARY_FAILED: "INSTALL_LIBRARY_FAILED",
|
||||
UNINSTALL_LIBRARY_FAILED: "UNINSTALL_LIBRARY_FAILED",
|
||||
FETCH_JS_LIBRARIES_FAILED: "FETCH_JS_LIBRARIES_FAILED",
|
||||
};
|
||||
|
||||
export const ReduxFormActionTypes = {
|
||||
|
|
|
|||
|
|
@ -1121,7 +1121,7 @@ export const API_PANE_NO_BODY = () => "This request does not have a body";
|
|||
export const TABLE_WIDGET_TOTAL_RECORD_TOOLTIP = () =>
|
||||
"It stores the total no. of rows in the table. Helps in calculating the no. of pages that further allows to enable or disable the next/previous control in pagination.";
|
||||
export const CREATE_DATASOURCE_TOOLTIP = () => "Add a new datasource";
|
||||
export const ADD_QUERY_JS_TOOLTIP = () => "Create New";
|
||||
export const ADD_QUERY_JS_TOOLTIP = () => "Add a new query / JS Object";
|
||||
|
||||
// Add datasource
|
||||
export const GENERATE_APPLICATION_TITLE = () => "Generate Page";
|
||||
|
|
@ -1420,6 +1420,39 @@ export const ALERT_STYLE_OPTIONS = [
|
|||
{ label: "Warning", value: "'warning'", id: "warning" },
|
||||
];
|
||||
|
||||
export const customJSLibraryMessages = {
|
||||
ADD_JS_LIBRARY: () => "Add JS Libraries",
|
||||
REC_LIBRARY: () => "Recommended Libraries",
|
||||
INSTALLATION_SUCCESSFUL: (accessor: string) =>
|
||||
`Installation Successful. You can access the library via ${accessor}`,
|
||||
INSTALLATION_FAILED: () => "Installation failed",
|
||||
INSTALLED_ALREADY: (accessor: string) =>
|
||||
`This library is installed already. You could access it via ${accessor}.`,
|
||||
UNINSTALL_FAILED: (name: string) =>
|
||||
`Couldn't uninstall ${name}. Please try again after sometime.`,
|
||||
UNINSTALL_SUCCESS: (accessor: string) =>
|
||||
`${accessor} is uninstalled successfully.`,
|
||||
LEARN_MORE_DESC: () => "Learn more about Custom JS Libraries",
|
||||
UNSUPPORTED_LIB: () => `Library is unsupported`,
|
||||
UNSUPPORTED_LIB_DESC: () =>
|
||||
`Unfortunately, this library cannot be supported due to platform limitations. Please try installing a different library.`,
|
||||
LEARN_MORE: () => `Learn more`,
|
||||
REPORT_ISSUE: () => `Report issue`,
|
||||
AUTOCOMPLETE_FAILED: (name: string) =>
|
||||
`Code completion for ${name} will not work.`,
|
||||
CLIENT_LOAD_FAILED: (url: string) => `Failed to load the script at ${url}.`,
|
||||
LIB_OVERRIDE_ERROR: (
|
||||
name: string,
|
||||
) => `The library ${name} is already installed.
|
||||
If you are trying to install a different version, uninstall the library first.`,
|
||||
DEFS_FAILED_ERROR: (name: string) =>
|
||||
`Failed to generate autocomplete definitions for ${name}.`,
|
||||
IMPORT_URL_ERROR: (url: string) =>
|
||||
`The script at ${url} cannot be installed.`,
|
||||
NAME_COLLISION_ERROR: (accessors: string) =>
|
||||
`Name collision detected: ${accessors}`,
|
||||
};
|
||||
|
||||
export const USAGE_AND_BILLING = {
|
||||
usage: () => "Usage",
|
||||
billing: () => "Billing",
|
||||
|
|
|
|||
|
|
@ -67,6 +67,7 @@ import tenantReducer, {
|
|||
} from "@appsmith/reducers/tenantReducer";
|
||||
import { FocusHistoryState } from "reducers/uiReducers/focusHistoryReducer";
|
||||
import { EditorContextState } from "reducers/uiReducers/editorContextReducer";
|
||||
import { LibraryState } from "reducers/uiReducers/libraryReducer";
|
||||
import { AutoHeightLayoutTreeReduxState } from "reducers/entityReducers/autoHeightReducers/autoHeightLayoutTreeReducer";
|
||||
import { CanvasLevelsReduxState } from "reducers/entityReducers/autoHeightReducers/canvasLevelsReducer";
|
||||
import { LintErrors } from "reducers/lintingReducers/lintErrorsReducers";
|
||||
|
|
@ -129,6 +130,7 @@ export interface AppState {
|
|||
appSettingsPane: AppSettingsPaneReduxState;
|
||||
focusHistory: FocusHistoryState;
|
||||
editorContext: EditorContextState;
|
||||
libraries: LibraryState;
|
||||
autoHeightUI: AutoHeightUIState;
|
||||
};
|
||||
entities: {
|
||||
|
|
|
|||
|
|
@ -40,8 +40,10 @@ import SuperUserSagas from "@appsmith/sagas/SuperUserSagas";
|
|||
import NavigationSagas from "sagas/NavigationSagas";
|
||||
import editorContextSagas from "sagas/editorContextSagas";
|
||||
import PageVisibilitySaga from "sagas/PageVisibilitySagas";
|
||||
import JSLibrarySaga from "sagas/JSLibrarySaga";
|
||||
import AutoHeightSagas from "sagas/autoHeightSagas";
|
||||
import tenantSagas from "@appsmith/sagas/tenantSagas";
|
||||
import LintingSaga from "sagas/LintingSagas";
|
||||
|
||||
export const sagas = [
|
||||
initSagas,
|
||||
|
|
@ -88,4 +90,6 @@ export const sagas = [
|
|||
PageVisibilitySaga,
|
||||
AutoHeightSagas,
|
||||
tenantSagas,
|
||||
JSLibrarySaga,
|
||||
LintingSaga,
|
||||
];
|
||||
|
|
|
|||
|
|
@ -47,6 +47,10 @@ import AppEngine, {
|
|||
PluginFormConfigsNotFoundError,
|
||||
PluginsNotFoundError,
|
||||
} from ".";
|
||||
import { fetchJSLibraries } from "actions/JSLibraryActions";
|
||||
import CodemirrorTernService from "utils/autocomplete/CodemirrorTernService";
|
||||
import { selectFeatureFlags } from "selectors/usersSelectors";
|
||||
import FeatureFlags from "entities/FeatureFlags";
|
||||
|
||||
export default class AppEditorEngine extends AppEngine {
|
||||
constructor(mode: APP_MODE) {
|
||||
|
|
@ -71,6 +75,7 @@ export default class AppEditorEngine extends AppEngine {
|
|||
public *setupEngine(payload: AppEnginePayload): any {
|
||||
yield* super.setupEngine.call(this, payload);
|
||||
yield put(resetEditorSuccess());
|
||||
CodemirrorTernService.resetServer();
|
||||
}
|
||||
|
||||
public startPerformanceTracking() {
|
||||
|
|
@ -113,6 +118,12 @@ export default class AppEditorEngine extends AppEngine {
|
|||
ReduxActionErrorTypes.FETCH_PAGE_ERROR,
|
||||
];
|
||||
|
||||
const featureFlags: FeatureFlags = yield select(selectFeatureFlags);
|
||||
if (featureFlags.CUSTOM_JS_LIBRARY) {
|
||||
initActionsCalls.push(fetchJSLibraries(applicationId));
|
||||
successActionEffects.push(ReduxActionTypes.FETCH_JS_LIBRARIES_SUCCESS);
|
||||
}
|
||||
|
||||
const allActionCalls: boolean = yield call(
|
||||
failFastApiCalls,
|
||||
initActionsCalls,
|
||||
|
|
|
|||
|
|
@ -17,12 +17,15 @@ import {
|
|||
ReduxActionTypes,
|
||||
} from "@appsmith/constants/ReduxActionConstants";
|
||||
import { APP_MODE } from "entities/App";
|
||||
import { call, put } from "redux-saga/effects";
|
||||
import { call, put, select } from "redux-saga/effects";
|
||||
import { failFastApiCalls } from "sagas/InitSagas";
|
||||
import PerformanceTracker, {
|
||||
PerformanceTransactionName,
|
||||
} from "utils/PerformanceTracker";
|
||||
import AppEngine, { ActionsNotFoundError, AppEnginePayload } from ".";
|
||||
import { fetchJSLibraries } from "actions/JSLibraryActions";
|
||||
import FeatureFlags from "entities/FeatureFlags";
|
||||
import { selectFeatureFlags } from "selectors/usersSelectors";
|
||||
|
||||
export default class AppViewerEngine extends AppEngine {
|
||||
constructor(mode: APP_MODE) {
|
||||
|
|
@ -67,28 +70,42 @@ export default class AppViewerEngine extends AppEngine {
|
|||
}
|
||||
|
||||
*loadAppEntities(toLoadPageId: string, applicationId: string): any {
|
||||
const resultOfPrimaryCalls: boolean = yield failFastApiCalls(
|
||||
[
|
||||
const initActionsCalls: any = [
|
||||
fetchActionsForView({ applicationId }),
|
||||
fetchJSCollectionsForView({ applicationId }),
|
||||
fetchSelectedAppThemeAction(applicationId),
|
||||
fetchAppThemesAction(applicationId),
|
||||
fetchPublishedPage(toLoadPageId, true, true),
|
||||
],
|
||||
[
|
||||
];
|
||||
|
||||
const successActionEffects = [
|
||||
ReduxActionTypes.FETCH_ACTIONS_VIEW_MODE_SUCCESS,
|
||||
ReduxActionTypes.FETCH_JS_ACTIONS_VIEW_MODE_SUCCESS,
|
||||
ReduxActionTypes.FETCH_APP_THEMES_SUCCESS,
|
||||
ReduxActionTypes.FETCH_SELECTED_APP_THEME_SUCCESS,
|
||||
fetchPublishedPageSuccess().type,
|
||||
],
|
||||
[
|
||||
];
|
||||
const failureActionEffects = [
|
||||
ReduxActionErrorTypes.FETCH_ACTIONS_VIEW_MODE_ERROR,
|
||||
ReduxActionErrorTypes.FETCH_JS_ACTIONS_VIEW_MODE_ERROR,
|
||||
ReduxActionErrorTypes.FETCH_APP_THEMES_ERROR,
|
||||
ReduxActionErrorTypes.FETCH_SELECTED_APP_THEME_ERROR,
|
||||
ReduxActionErrorTypes.FETCH_PUBLISHED_PAGE_ERROR,
|
||||
],
|
||||
];
|
||||
|
||||
const featureFlags: FeatureFlags = yield select(selectFeatureFlags);
|
||||
if (featureFlags.CUSTOM_JS_LIBRARY) {
|
||||
initActionsCalls.push(fetchJSLibraries(applicationId));
|
||||
successActionEffects.push(ReduxActionTypes.FETCH_JS_LIBRARIES_SUCCESS);
|
||||
failureActionEffects.push(
|
||||
ReduxActionErrorTypes.FETCH_JS_LIBRARIES_FAILED,
|
||||
);
|
||||
}
|
||||
|
||||
const resultOfPrimaryCalls: boolean = yield failFastApiCalls(
|
||||
initActionsCalls,
|
||||
successActionEffects,
|
||||
failureActionEffects,
|
||||
);
|
||||
|
||||
if (!resultOfPrimaryCalls)
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ type FeatureFlags = {
|
|||
CONTEXT_SWITCHING?: boolean;
|
||||
USAGE?: boolean;
|
||||
DATASOURCE_ENVIRONMENTS?: boolean;
|
||||
CUSTOM_JS_LIBRARY?: boolean;
|
||||
};
|
||||
|
||||
export default FeatureFlags;
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import styled from "styled-components";
|
|||
import Divider from "components/editorComponents/Divider";
|
||||
import Search from "./ExplorerSearch";
|
||||
import { NonIdealState, Classes } from "@blueprintjs/core";
|
||||
import JSDependencies from "./JSDependencies";
|
||||
import JSDependencies from "./Libraries";
|
||||
import PerformanceTracker, {
|
||||
PerformanceTransactionName,
|
||||
} from "utils/PerformanceTracker";
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ import { TOOLTIP_HOVER_ON_DELAY } from "constants/AppConstants";
|
|||
import { EntityClassNames } from "../Entity";
|
||||
import { TooltipComponent } from "design-system";
|
||||
import {
|
||||
ADD_QUERY_JS_BUTTON,
|
||||
ADD_QUERY_JS_TOOLTIP,
|
||||
createMessage,
|
||||
} from "@appsmith/constants/messages";
|
||||
import { useCloseMenuOnScroll } from "../hooks";
|
||||
|
|
@ -223,7 +223,7 @@ export default function ExplorerSubMenu({
|
|||
className={EntityClassNames.TOOLTIP}
|
||||
content={
|
||||
<>
|
||||
{createMessage(ADD_QUERY_JS_BUTTON)} (
|
||||
{createMessage(ADD_QUERY_JS_TOOLTIP)} (
|
||||
{comboHelpText[SEARCH_CATEGORY_ID.ACTION_OPERATION]})
|
||||
</>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,133 +0,0 @@
|
|||
import React, { useState } from "react";
|
||||
import styled from "styled-components";
|
||||
import { AppIcon as Icon, Size, TooltipComponent } from "design-system";
|
||||
import { Colors } from "constants/Colors";
|
||||
import { BindingText } from "pages/Editor/APIEditor/CommonEditorForm";
|
||||
import { extraLibraries } from "utils/DynamicBindingUtils";
|
||||
import CollapseToggle from "./Entity/CollapseToggle";
|
||||
import Collapse from "./Entity/Collapse";
|
||||
|
||||
const Wrapper = styled.div`
|
||||
font-size: 14px;
|
||||
`;
|
||||
const ListItem = styled.li`
|
||||
list-style: none;
|
||||
color: ${Colors.GREY_8};
|
||||
height: 36px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
cursor: pointer;
|
||||
padding: 0 12px 0 20px;
|
||||
position: relative;
|
||||
&:hover {
|
||||
background: ${Colors.ALABASTER_ALT};
|
||||
|
||||
& .t--open-new-tab {
|
||||
display: block;
|
||||
}
|
||||
|
||||
& .t--package-version {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
& .t--open-new-tab {
|
||||
position: absolute;
|
||||
right: 8px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
& .t--package-version {
|
||||
display: block;
|
||||
}
|
||||
`;
|
||||
const Name = styled.span``;
|
||||
const Version = styled.span``;
|
||||
const Title = styled.div`
|
||||
display: grid;
|
||||
grid-template-columns: 20px auto 20px;
|
||||
cursor: pointer;
|
||||
height: 32px;
|
||||
align-items: center;
|
||||
padding-right: 4px;
|
||||
padding-left: 0.25rem;
|
||||
font-size: 14px;
|
||||
&:hover {
|
||||
background: ${Colors.ALABASTER_ALT};
|
||||
}
|
||||
& .t--help-icon {
|
||||
svg {
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
span {
|
||||
color: ${Colors.GREY_800};
|
||||
}
|
||||
`;
|
||||
|
||||
function JSDependencies() {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const openDocs = (name: string, url: string) => () => window.open(url, name);
|
||||
const dependencyList = extraLibraries.map((lib) => {
|
||||
return (
|
||||
<ListItem
|
||||
key={lib.displayName}
|
||||
onClick={openDocs(lib.displayName, lib.docsURL)}
|
||||
>
|
||||
<Name>{lib.displayName}</Name>
|
||||
<Version className="t--package-version">{lib.version}</Version>
|
||||
<Icon className="t--open-new-tab" name="open-new-tab" size={Size.xxs} />
|
||||
</ListItem>
|
||||
);
|
||||
});
|
||||
|
||||
const toggleDependencies = React.useCallback(
|
||||
() => setIsOpen((open) => !open),
|
||||
[],
|
||||
);
|
||||
const showDocs = React.useCallback((e: any) => {
|
||||
window.open(
|
||||
"https://docs.appsmith.com/v/v1.2.1/core-concepts/writing-code/ext-libraries",
|
||||
"appsmith-docs:working-with-js-libraries",
|
||||
);
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
}, []);
|
||||
|
||||
const TooltipContent = (
|
||||
<div>
|
||||
<span>Access these JS libraries to transform data within </span>
|
||||
<BindingText>{`{{ }}`}</BindingText>
|
||||
<span>. Try </span>
|
||||
<BindingText>{`{{ _.add(1,1) }}`}</BindingText>
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<Wrapper>
|
||||
<Title onClick={toggleDependencies}>
|
||||
<CollapseToggle
|
||||
className={""}
|
||||
disabled={false}
|
||||
isOpen={isOpen}
|
||||
isVisible={!!dependencyList}
|
||||
onClick={toggleDependencies}
|
||||
/>
|
||||
<span className="ml-1 font-medium">Dependencies</span>
|
||||
<TooltipComponent content={TooltipContent} hoverOpenDelay={200}>
|
||||
<Icon
|
||||
className="t--help-icon"
|
||||
name="help"
|
||||
onClick={showDocs}
|
||||
size={Size.xxs}
|
||||
/>
|
||||
</TooltipComponent>
|
||||
</Title>
|
||||
<Collapse isOpen={isOpen} step={0}>
|
||||
{dependencyList}
|
||||
</Collapse>
|
||||
</Wrapper>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(JSDependencies);
|
||||
538
app/client/src/pages/Editor/Explorer/Libraries/Installer.tsx
Normal file
538
app/client/src/pages/Editor/Explorer/Libraries/Installer.tsx
Normal file
|
|
@ -0,0 +1,538 @@
|
|||
import React, {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import styled from "styled-components";
|
||||
import {
|
||||
Button,
|
||||
Category,
|
||||
FormGroup,
|
||||
Icon,
|
||||
IconSize,
|
||||
Size,
|
||||
Spinner,
|
||||
Text,
|
||||
TextInput,
|
||||
TextType,
|
||||
Toaster,
|
||||
Variant,
|
||||
} from "design-system";
|
||||
import {
|
||||
createMessage,
|
||||
customJSLibraryMessages,
|
||||
} from "@appsmith/constants/messages";
|
||||
import ProfileImage from "pages/common/ProfileImage";
|
||||
import { Colors } from "constants/Colors";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import {
|
||||
selectInstallationStatus,
|
||||
selectInstalledLibraries,
|
||||
selectIsInstallerOpen,
|
||||
selectIsLibraryInstalled,
|
||||
selectQueuedLibraries,
|
||||
selectStatusForURL,
|
||||
} from "selectors/entitiesSelector";
|
||||
import SaveSuccessIcon from "remixicon-react/CheckboxCircleFillIcon";
|
||||
import { InstallState } from "reducers/uiReducers/libraryReducer";
|
||||
import recommendedLibraries from "pages/Editor/Explorer/Libraries/recommendedLibraries";
|
||||
import { AppState } from "@appsmith/reducers";
|
||||
import {
|
||||
clearInstalls,
|
||||
installLibraryInit,
|
||||
toggleInstaller,
|
||||
} from "actions/JSLibraryActions";
|
||||
import classNames from "classnames";
|
||||
import { TJSLibrary } from "workers/common/JSLibrary";
|
||||
import AnalyticsUtil from "utils/AnalyticsUtil";
|
||||
|
||||
const openDoc = (e: React.MouseEvent, url: string) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
window.open(url, "_blank");
|
||||
};
|
||||
|
||||
const Wrapper = styled.div<{ left: number }>`
|
||||
display: flex;
|
||||
height: auto;
|
||||
width: 400px;
|
||||
max-height: 80vh;
|
||||
flex-direction: column;
|
||||
padding: 0 24px 4px;
|
||||
position: absolute;
|
||||
background: white;
|
||||
z-index: 25;
|
||||
left: ${(props) => props.left}px;
|
||||
bottom: 15px;
|
||||
.installation-header {
|
||||
padding: 20px 0 0;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.search-area {
|
||||
margin-bottom: 16px;
|
||||
.left-icon {
|
||||
margin-left: 14px;
|
||||
.cs-icon {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
.bp3-form-group {
|
||||
margin: 0;
|
||||
.remixicon-icon {
|
||||
cursor: initial;
|
||||
}
|
||||
}
|
||||
.bp3-label {
|
||||
font-size: 12px;
|
||||
}
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
.search-bar {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
}
|
||||
.search-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
.search-CTA {
|
||||
margin-bottom: 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.search-results {
|
||||
.library-card {
|
||||
gap: 8px;
|
||||
padding: 8px 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
border-bottom: 1px solid var(--appsmith-color-black-100);
|
||||
.description {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
font-size: 12px;
|
||||
line-clamp: 2;
|
||||
font-weight: 400;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
img {
|
||||
cursor: initial;
|
||||
}
|
||||
}
|
||||
.library-card.no-border {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const InstallationProgressWrapper = styled.div<{ addBorder: boolean }>`
|
||||
border-top: ${(props) =>
|
||||
props.addBorder ? `1px solid var(--appsmith-color-black-300)` : "none"};
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: var(--appsmith-color-black-50);
|
||||
text-overflow: ellipsis;
|
||||
padding: 8px 8px 12px;
|
||||
.install-url {
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
word-break: break-all;
|
||||
}
|
||||
.error-card {
|
||||
display: flex;
|
||||
padding: 10px;
|
||||
flex-direction: row;
|
||||
background: #ffe9e9;
|
||||
.unsupported {
|
||||
line-height: 17px;
|
||||
.header {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #393939;
|
||||
}
|
||||
.body {
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const StatusIconWrapper = styled.div<{
|
||||
addHoverState: boolean;
|
||||
}>`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
cursor: initial;
|
||||
.failed {
|
||||
svg {
|
||||
cursor: initial;
|
||||
}
|
||||
}}
|
||||
${(props) =>
|
||||
props.addHoverState
|
||||
? `
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
background: ${Colors.SHARK2} !important;
|
||||
svg {
|
||||
path {
|
||||
fill: ${Colors.WHITE} !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
: "svg { cursor: initial }"}
|
||||
`;
|
||||
|
||||
function isValidJSFileURL(url: string) {
|
||||
const JS_FILE_REGEX = /^https?:\/\/.*\.js$/;
|
||||
return JS_FILE_REGEX.test(url);
|
||||
}
|
||||
|
||||
function StatusIcon(props: {
|
||||
status: InstallState;
|
||||
isInstalled?: boolean;
|
||||
action?: any;
|
||||
}) {
|
||||
const { action, isInstalled = false, status } = props;
|
||||
const actionProps = useMemo(() => (action ? { onClick: action } : {}), [
|
||||
action,
|
||||
]);
|
||||
if (status === InstallState.Success || isInstalled)
|
||||
return (
|
||||
<StatusIconWrapper addHoverState={false} className="installed">
|
||||
<SaveSuccessIcon color={Colors.GREEN} size={18} />
|
||||
</StatusIconWrapper>
|
||||
);
|
||||
if (status === InstallState.Failed)
|
||||
return (
|
||||
<StatusIconWrapper addHoverState={false} className="failed">
|
||||
<Icon fillColor={Colors.GRAY} name="warning-line" size={IconSize.XL} />
|
||||
</StatusIconWrapper>
|
||||
);
|
||||
if (status === InstallState.Queued)
|
||||
return (
|
||||
<StatusIconWrapper addHoverState={false} className="queued">
|
||||
<Spinner />
|
||||
</StatusIconWrapper>
|
||||
);
|
||||
return (
|
||||
<StatusIconWrapper addHoverState className="t--download" {...actionProps}>
|
||||
<Icon fillColor={Colors.GRAY} name="download" size={IconSize.XL} />
|
||||
</StatusIconWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
function ProgressTracker({
|
||||
isFirst,
|
||||
isLast,
|
||||
status,
|
||||
url,
|
||||
}: {
|
||||
isFirst: boolean;
|
||||
isLast: boolean;
|
||||
status: InstallState;
|
||||
url: string;
|
||||
}) {
|
||||
return (
|
||||
<InstallationProgressWrapper
|
||||
addBorder={!isFirst}
|
||||
className={classNames({
|
||||
"mb-2": isLast,
|
||||
})}
|
||||
>
|
||||
{[InstallState.Queued, InstallState.Installing].includes(status) && (
|
||||
<div className="text-gray-700 text-xs">Installing...</div>
|
||||
)}
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="flex justify-between items-center gap-2 fw-500 text-sm">
|
||||
<div className="install-url text-sm font-medium">{url}</div>
|
||||
<div className="shrink-0">
|
||||
<StatusIcon status={status} />
|
||||
</div>
|
||||
</div>
|
||||
{status === InstallState.Failed && (
|
||||
<div className="gap-2 error-card items-start">
|
||||
<Icon name="danger" size={IconSize.XL} />
|
||||
<div className="flex flex-col unsupported gap-1">
|
||||
<div className="header">
|
||||
{createMessage(customJSLibraryMessages.UNSUPPORTED_LIB)}
|
||||
</div>
|
||||
<div className="body">
|
||||
{createMessage(customJSLibraryMessages.UNSUPPORTED_LIB_DESC)}
|
||||
</div>
|
||||
<div className="footer text-xs font-medium gap-2 flex flex-row">
|
||||
<a onClick={(e) => openDoc(e, EXT_LINK.reportIssue)}>
|
||||
{createMessage(customJSLibraryMessages.REPORT_ISSUE)}
|
||||
</a>
|
||||
<a onClick={(e) => openDoc(e, EXT_LINK.learnMore)}>
|
||||
{createMessage(customJSLibraryMessages.LEARN_MORE)}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</InstallationProgressWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
function InstallationProgress() {
|
||||
const installStatusMap = useSelector(selectInstallationStatus);
|
||||
const urls = Object.keys(installStatusMap).filter(
|
||||
(url) => !recommendedLibraries.find((lib) => lib.url === url),
|
||||
);
|
||||
if (urls.length === 0) return null;
|
||||
return (
|
||||
<div>
|
||||
{urls.reverse().map((url, idx) => (
|
||||
<ProgressTracker
|
||||
isFirst={idx === 0}
|
||||
isLast={idx === urls.length - 1}
|
||||
key={`${url}_${idx}`}
|
||||
status={installStatusMap[url]}
|
||||
url={url}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const EXT_LINK = {
|
||||
learnMore:
|
||||
"https://docs.appsmith.com/core-concepts/writing-code/ext-libraries",
|
||||
reportIssue: "https://github.com/appsmithorg/appsmith/issues/19037",
|
||||
jsDelivr: "https://www.jsdelivr.com/",
|
||||
};
|
||||
|
||||
export function Installer(props: { left: number }) {
|
||||
const { left } = props;
|
||||
const [URL, setURL] = useState("");
|
||||
const [isValid, setIsValid] = useState(true);
|
||||
const dispatch = useDispatch();
|
||||
const installedLibraries = useSelector(selectInstalledLibraries);
|
||||
const queuedLibraries = useSelector(selectQueuedLibraries);
|
||||
const isOpen = useSelector(selectIsInstallerOpen);
|
||||
const installerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const closeInstaller = useCallback(() => {
|
||||
dispatch(clearInstalls());
|
||||
dispatch(toggleInstaller(false));
|
||||
}, []);
|
||||
|
||||
const handleOutsideClick = useCallback((e: MouseEvent) => {
|
||||
const paths = e.composedPath();
|
||||
if (
|
||||
installerRef &&
|
||||
installerRef.current &&
|
||||
!paths?.includes(installerRef.current)
|
||||
)
|
||||
closeInstaller();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener("mousedown", handleOutsideClick);
|
||||
return () => {
|
||||
document.removeEventListener("mousedown", handleOutsideClick);
|
||||
};
|
||||
}, [isOpen]);
|
||||
|
||||
const updateURL = useCallback((value: string) => {
|
||||
setURL(value);
|
||||
}, []);
|
||||
|
||||
const validate = useCallback((text) => {
|
||||
const isValid = !text || isValidJSFileURL(text);
|
||||
setIsValid(isValid);
|
||||
return {
|
||||
isValid,
|
||||
message: isValid ? "" : "Please enter a valid URL",
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
URL &&
|
||||
AnalyticsUtil.logEvent("EDIT_LIBRARY_URL", { url: URL, valid: isValid });
|
||||
}, [URL, isValid]);
|
||||
|
||||
const installLibrary = useCallback(
|
||||
(lib?: Partial<TJSLibrary>) => {
|
||||
const url = lib?.url || URL;
|
||||
const isQueued = queuedLibraries.find((libURL) => libURL === url);
|
||||
if (isQueued) return;
|
||||
|
||||
const libInstalled = installedLibraries.find((lib) => lib.url === url);
|
||||
if (libInstalled) {
|
||||
Toaster.show({
|
||||
text: createMessage(
|
||||
customJSLibraryMessages.INSTALLED_ALREADY,
|
||||
libInstalled.accessor,
|
||||
),
|
||||
variant: Variant.info,
|
||||
});
|
||||
return;
|
||||
}
|
||||
dispatch(
|
||||
installLibraryInit({
|
||||
url,
|
||||
name: lib?.name,
|
||||
}),
|
||||
);
|
||||
},
|
||||
[URL, installedLibraries, queuedLibraries],
|
||||
);
|
||||
|
||||
return !isOpen ? null : (
|
||||
<Wrapper className="bp3-popover" left={left} ref={installerRef}>
|
||||
<div className="installation-header">
|
||||
<Text type={TextType.H1} weight={"bold"}>
|
||||
{createMessage(customJSLibraryMessages.ADD_JS_LIBRARY)}
|
||||
</Text>
|
||||
<Icon
|
||||
className="t--close-installer"
|
||||
fillColor={Colors.GRAY}
|
||||
name="close-modal"
|
||||
onClick={closeInstaller}
|
||||
size={IconSize.XXL}
|
||||
/>
|
||||
</div>
|
||||
<div className="search-area t--library-container">
|
||||
<div className="flex flex-row gap-2 justify-between items-end">
|
||||
<FormGroup className="flex-1" label={"Library URL"}>
|
||||
<TextInput
|
||||
$padding="12px"
|
||||
autoFocus
|
||||
data-testid="library-url"
|
||||
height="30px"
|
||||
label={"Library URL"}
|
||||
leftIcon="link-2"
|
||||
onChange={updateURL}
|
||||
padding="12px"
|
||||
placeholder="https://cdn.jsdelivr.net/npm/example@1.1.1/example.min.js"
|
||||
validator={validate}
|
||||
width="100%"
|
||||
/>
|
||||
</FormGroup>
|
||||
<Button
|
||||
category={Category.primary}
|
||||
data-testid="install-library-btn"
|
||||
disabled={!(URL && isValid)}
|
||||
icon="download"
|
||||
onClick={() => installLibrary()}
|
||||
size={Size.medium}
|
||||
tag="button"
|
||||
text="INSTALL"
|
||||
type="button"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="search-body overflow-auto">
|
||||
<div className="search-CTA mb-3 text-xs">
|
||||
<span>
|
||||
Explore libraries on{" "}
|
||||
<a
|
||||
className="text-primary-500"
|
||||
onClick={(e) => openDoc(e, EXT_LINK.jsDelivr)}
|
||||
>
|
||||
jsDelivr
|
||||
</a>
|
||||
{". "}
|
||||
{createMessage(customJSLibraryMessages.LEARN_MORE_DESC)}{" "}
|
||||
<a
|
||||
className="text-primary-500"
|
||||
onClick={(e) => openDoc(e, EXT_LINK.learnMore)}
|
||||
>
|
||||
here
|
||||
</a>
|
||||
{"."}
|
||||
</span>
|
||||
</div>
|
||||
<InstallationProgress />
|
||||
<div className="pb-2 sticky top-0 z-2 bg-white">
|
||||
<Text type={TextType.P1} weight={"600"}>
|
||||
{createMessage(customJSLibraryMessages.REC_LIBRARY)}
|
||||
</Text>
|
||||
</div>
|
||||
<div className="search-results">
|
||||
{recommendedLibraries.map((lib, idx) => (
|
||||
<LibraryCard
|
||||
isLastCard={idx === recommendedLibraries.length - 1}
|
||||
key={`${idx}_${lib.name}`}
|
||||
lib={lib}
|
||||
onClick={() => installLibrary(lib)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</Wrapper>
|
||||
);
|
||||
}
|
||||
|
||||
function LibraryCard({
|
||||
isLastCard,
|
||||
lib,
|
||||
onClick,
|
||||
}: {
|
||||
lib: typeof recommendedLibraries[0];
|
||||
onClick: (url: string) => void;
|
||||
isLastCard: boolean;
|
||||
}) {
|
||||
const status = useSelector(selectStatusForURL(lib.url));
|
||||
const isInstalled = useSelector((state: AppState) =>
|
||||
selectIsLibraryInstalled(state, lib.url),
|
||||
);
|
||||
return (
|
||||
<div
|
||||
className={classNames({
|
||||
[`library-card t--${lib.name}`]: true,
|
||||
"no-border": isLastCard,
|
||||
})}
|
||||
>
|
||||
<div className="flex flex-row justify-between items-center">
|
||||
<div className="flex flex-row gap-2 items-center">
|
||||
<Text type={TextType.P0} weight="500">
|
||||
{lib.name}
|
||||
</Text>
|
||||
<StatusIconWrapper
|
||||
addHoverState
|
||||
onClick={(e) => openDoc(e, lib.docsURL)}
|
||||
>
|
||||
<Icon
|
||||
fillColor={Colors.GRAY}
|
||||
name="share-2"
|
||||
size={IconSize.SMALL}
|
||||
/>
|
||||
</StatusIconWrapper>
|
||||
</div>
|
||||
<div className="mr-2">
|
||||
<StatusIcon
|
||||
action={onClick}
|
||||
isInstalled={isInstalled}
|
||||
status={status}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-row description">{lib.description}</div>
|
||||
<div className="flex flex-row items-center gap-1">
|
||||
<ProfileImage size={20} source={lib.icon} />
|
||||
<Text type={TextType.P3}>{lib.author}</Text>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,152 @@
|
|||
import React from "react";
|
||||
import { render, screen, fireEvent, cleanup } from "@testing-library/react";
|
||||
import "@testing-library/jest-dom/extend-expect";
|
||||
import { Provider } from "react-redux";
|
||||
import store from "store";
|
||||
import { ThemeProvider } from "styled-components";
|
||||
import { lightTheme } from "selectors/themeSelectors";
|
||||
import { ReduxActionTypes } from "@appsmith/constants/ReduxActionConstants";
|
||||
import { Installer } from "pages/Editor/Explorer/Libraries/Installer";
|
||||
|
||||
export const fetchApplicationMockResponse = {
|
||||
responseMeta: {
|
||||
status: 200,
|
||||
success: true,
|
||||
},
|
||||
data: {
|
||||
application: {
|
||||
id: "605c435a91dea93f0eaf91b8",
|
||||
name: "My Application",
|
||||
slug: "my-application",
|
||||
workspaceId: "",
|
||||
evaluationVersion: 1,
|
||||
appIsExample: false,
|
||||
gitApplicationMetadata: undefined,
|
||||
applicationVersion: 2,
|
||||
},
|
||||
pages: [
|
||||
{
|
||||
id: "605c435a91dea93f0eaf91ba",
|
||||
name: "Page1",
|
||||
isDefault: true,
|
||||
slug: "page-1",
|
||||
},
|
||||
{
|
||||
id: "605c435a91dea93f0eaf91bc",
|
||||
name: "Page2",
|
||||
isDefault: false,
|
||||
slug: "page-2",
|
||||
},
|
||||
],
|
||||
workspaceId: "",
|
||||
},
|
||||
};
|
||||
|
||||
describe("Contains all UI tests for JS libraries", () => {
|
||||
store.dispatch({
|
||||
type: ReduxActionTypes.TOGGLE_INSTALLER,
|
||||
payload: true,
|
||||
});
|
||||
afterEach(cleanup);
|
||||
|
||||
it("Headers should exist", () => {
|
||||
render(
|
||||
<Provider store={store}>
|
||||
<ThemeProvider theme={lightTheme}>
|
||||
<Installer left={250} />
|
||||
</ThemeProvider>
|
||||
</Provider>,
|
||||
);
|
||||
expect(screen.getByText("Add JS Libraries")).toBeDefined();
|
||||
expect(screen.getByText("Recommended Libraries")).toBeDefined();
|
||||
expect(screen.getByTestId("library-url")).toBeDefined();
|
||||
expect(screen.getByTestId("install-library-btn")).toBeDisabled();
|
||||
});
|
||||
|
||||
it("Validates URL", () => {
|
||||
render(
|
||||
<Provider store={store}>
|
||||
<ThemeProvider theme={lightTheme}>
|
||||
<Installer left={250} />
|
||||
</ThemeProvider>
|
||||
</Provider>,
|
||||
);
|
||||
const input = screen.getByTestId("library-url");
|
||||
fireEvent.change(input, { target: { value: "https://valid.com/file.js" } });
|
||||
expect(screen.getByTestId("install-library-btn")).toBeEnabled();
|
||||
expect(screen.queryByText("Please enter a valid URL")).toBeNull();
|
||||
fireEvent.change(input, { target: { value: "23" } });
|
||||
expect(screen.queryByText("Please enter a valid URL")).toBeDefined();
|
||||
expect(screen.getByTestId("install-library-btn")).toBeDisabled();
|
||||
});
|
||||
|
||||
it("Renders progress bar", () => {
|
||||
render(
|
||||
<Provider store={store}>
|
||||
<ThemeProvider theme={lightTheme}>
|
||||
<Installer left={250} />
|
||||
</ThemeProvider>
|
||||
</Provider>,
|
||||
);
|
||||
store.dispatch({
|
||||
type: ReduxActionTypes.FETCH_APPLICATION_SUCCESS,
|
||||
payload: {
|
||||
...fetchApplicationMockResponse.data.application,
|
||||
pages: fetchApplicationMockResponse.data.pages,
|
||||
},
|
||||
});
|
||||
const input = screen.getByTestId("library-url");
|
||||
fireEvent.change(input, {
|
||||
target: {
|
||||
value:
|
||||
"https://cdnjs.cloudflare.com/ajax/libs/dayjs/1.11.6/dayjs.min.js",
|
||||
},
|
||||
});
|
||||
const installButton = screen.getByTestId("install-library-btn");
|
||||
expect(installButton).toBeDefined();
|
||||
fireEvent.click(installButton);
|
||||
|
||||
expect(
|
||||
screen.queryByText(
|
||||
`Installing library for ${fetchApplicationMockResponse.data.application.name}`,
|
||||
),
|
||||
).toBeDefined();
|
||||
});
|
||||
|
||||
it("Progress bar should disappear once the installation succeeds or fails", () => {
|
||||
render(
|
||||
<Provider store={store}>
|
||||
<ThemeProvider theme={lightTheme}>
|
||||
<Installer left={250} />
|
||||
</ThemeProvider>
|
||||
</Provider>,
|
||||
);
|
||||
store.dispatch({
|
||||
type: ReduxActionTypes.INSTALL_LIBRARY_INIT,
|
||||
payload:
|
||||
"https://cdnjs.cloudflare.com/ajax/libs/dayjs/1.11.6/dayjs.min.js",
|
||||
});
|
||||
|
||||
expect(
|
||||
screen.queryByText(
|
||||
`Installing library for ${fetchApplicationMockResponse.data.application.name}`,
|
||||
),
|
||||
).toBeDefined();
|
||||
|
||||
store.dispatch({
|
||||
type: ReduxActionTypes.INSTALL_LIBRARY_SUCCESS,
|
||||
payload: {
|
||||
name: "dayjs",
|
||||
version: "1.11.6",
|
||||
accessor: ["dayjs"],
|
||||
url: "https://cdnjs.cloudflare.com/ajax/libs/dayjs/1.11.6/dayjs.min.js",
|
||||
},
|
||||
});
|
||||
|
||||
expect(
|
||||
screen.queryByText(
|
||||
`Installing library for ${fetchApplicationMockResponse.data.application.name}`,
|
||||
),
|
||||
).toBeNull();
|
||||
});
|
||||
});
|
||||
338
app/client/src/pages/Editor/Explorer/Libraries/index.tsx
Normal file
338
app/client/src/pages/Editor/Explorer/Libraries/index.tsx
Normal file
|
|
@ -0,0 +1,338 @@
|
|||
import React, { MutableRefObject, useCallback, useRef } from "react";
|
||||
import styled from "styled-components";
|
||||
import {
|
||||
Icon,
|
||||
IconSize,
|
||||
Spinner,
|
||||
Toaster,
|
||||
TooltipComponent,
|
||||
Variant,
|
||||
} from "design-system";
|
||||
import { Colors } from "constants/Colors";
|
||||
import Entity, { EntityClassNames } from "../Entity";
|
||||
import {
|
||||
createMessage,
|
||||
customJSLibraryMessages,
|
||||
} from "@appsmith/constants/messages";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import {
|
||||
selectInstallationStatus,
|
||||
selectIsInstallerOpen,
|
||||
selectLibrariesForExplorer,
|
||||
} from "selectors/entitiesSelector";
|
||||
import { InstallState } from "reducers/uiReducers/libraryReducer";
|
||||
import { Collapse } from "@blueprintjs/core";
|
||||
import { ReactComponent as CopyIcon } from "assets/icons/menu/copy-snippet.svg";
|
||||
import useClipboard from "utils/hooks/useClipboard";
|
||||
import {
|
||||
toggleInstaller,
|
||||
uninstallLibraryInit,
|
||||
} from "actions/JSLibraryActions";
|
||||
import EntityAddButton from "../Entity/AddButton";
|
||||
import { TOOLTIP_HOVER_ON_DELAY } from "constants/AppConstants";
|
||||
import { TJSLibrary } from "workers/common/JSLibrary";
|
||||
import { getPagePermissions } from "selectors/editorSelectors";
|
||||
import { hasCreateActionPermission } from "@appsmith/utils/permissionHelpers";
|
||||
import { selectFeatureFlags } from "selectors/usersSelectors";
|
||||
import recommendedLibraries from "./recommendedLibraries";
|
||||
|
||||
const docsURLMap = recommendedLibraries.reduce((acc, lib) => {
|
||||
acc[lib.url] = lib.docsURL;
|
||||
return acc;
|
||||
}, {} as Record<string, string>);
|
||||
|
||||
const Library = styled.li`
|
||||
list-style: none;
|
||||
flex-direction: column;
|
||||
color: ${Colors.GRAY_700};
|
||||
font-weight: 400;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
line-height: 17px;
|
||||
padding-left: 8px;
|
||||
|
||||
> div:first-child {
|
||||
height: 36px;
|
||||
}
|
||||
|
||||
.share {
|
||||
display: none;
|
||||
width: 30px;
|
||||
height: 36px;
|
||||
background: transparent;
|
||||
margin-left: 8px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: ${Colors.SEA_SHELL};
|
||||
|
||||
& .t--open-new-tab {
|
||||
display: block;
|
||||
}
|
||||
|
||||
& .delete,
|
||||
.share {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: transparent;
|
||||
&:hover {
|
||||
background: black;
|
||||
.uninstall-library,
|
||||
.open-link {
|
||||
color: white;
|
||||
svg > path {
|
||||
fill: white;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.loading {
|
||||
display: none;
|
||||
width: 30px;
|
||||
height: 36px;
|
||||
background: transparent;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
& .t--open-new-tab {
|
||||
position: absolute;
|
||||
right: 8px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.delete {
|
||||
display: none;
|
||||
width: 30px;
|
||||
height: 36px;
|
||||
background: transparent;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
& .t--package-version {
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
height: 16px;
|
||||
}
|
||||
.open-collapse {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
.content {
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
padding: 4px 8px;
|
||||
color: ${Colors.GRAY_700};
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
.accessor {
|
||||
padding-left: 8px;
|
||||
flex-grow: 1;
|
||||
outline: 1px solid #b3b3b3 !important;
|
||||
font-size: 12px;
|
||||
font-family: monospace;
|
||||
background: white;
|
||||
display: flex;
|
||||
height: 25px;
|
||||
width: calc(100% - 80px);
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
color: ${Colors.ENTERPRISE_DARK};
|
||||
> div {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: transparent;
|
||||
width: 25px;
|
||||
&: hover {
|
||||
background: ${Colors.SHARK2};
|
||||
> svg > path {
|
||||
fill: ${Colors.WHITE};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
const Name = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
line-height: 17px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
word-break: break-all;
|
||||
`;
|
||||
const Version = styled.div<{ version?: string }>`
|
||||
flex-shrink: 0;
|
||||
display: ${(props) => (props.version ? "block" : "none")};
|
||||
margin: ${(props) => (props.version ? "0 8px" : "0")};
|
||||
`;
|
||||
|
||||
const PrimaryCTA = function({ lib }: { lib: TJSLibrary }) {
|
||||
const installationStatus = useSelector(selectInstallationStatus);
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const url = lib.url as string;
|
||||
|
||||
const uninstallLibrary = useCallback(
|
||||
(e) => {
|
||||
e.stopPropagation();
|
||||
dispatch(uninstallLibraryInit(lib));
|
||||
},
|
||||
[lib],
|
||||
);
|
||||
|
||||
if (installationStatus[url] === InstallState.Queued)
|
||||
return (
|
||||
<div className="loading">
|
||||
<Spinner size={IconSize.MEDIUM} />
|
||||
</div>
|
||||
);
|
||||
|
||||
if (url) {
|
||||
//Default libraries will not have url
|
||||
return (
|
||||
<div className="delete" onClick={uninstallLibrary}>
|
||||
<Icon
|
||||
className="uninstall-library t--uninstall-library"
|
||||
name="trash-outline"
|
||||
size={IconSize.MEDIUM}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
function LibraryEntity({ lib }: { lib: TJSLibrary }) {
|
||||
const openDocs = useCallback(
|
||||
(url?: string) => (e: React.MouseEvent) => {
|
||||
e?.stopPropagation();
|
||||
url && window.open(url, "_blank");
|
||||
},
|
||||
[],
|
||||
);
|
||||
const propertyRef: MutableRefObject<HTMLDivElement | null> = useRef(null);
|
||||
const write = useClipboard(propertyRef);
|
||||
|
||||
const copyToClipboard = useCallback(() => {
|
||||
write(lib.accessor[lib.accessor.length - 1]);
|
||||
Toaster.show({
|
||||
text: "Copied to clipboard",
|
||||
variant: Variant.success,
|
||||
});
|
||||
}, [lib.accessor]);
|
||||
|
||||
const [isOpen, open] = React.useState(false);
|
||||
const docsURL = docsURLMap[lib.url || ""] || lib.docsURL;
|
||||
return (
|
||||
<Library className={`t--installed-library-${lib.name}`}>
|
||||
<div
|
||||
className="flex flex-row items-center h-full"
|
||||
onClick={() => open(!isOpen)}
|
||||
>
|
||||
<Icon
|
||||
className={isOpen ? "open-collapse" : ""}
|
||||
fillColor={Colors.GREY_7}
|
||||
name="right-arrow-2"
|
||||
size={IconSize.XXXL}
|
||||
/>
|
||||
<div className="flex items-center flex-start flex-1 overflow-hidden">
|
||||
<Name>{lib.name}</Name>
|
||||
{docsURL && (
|
||||
<div className="share" onClick={openDocs(docsURL)}>
|
||||
<Icon
|
||||
className="open-link"
|
||||
fillColor={Colors.GRAY_700}
|
||||
name="share-2"
|
||||
size={IconSize.SMALL}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<Version className="t--package-version" version={lib.version}>
|
||||
{lib.version}
|
||||
</Version>
|
||||
<PrimaryCTA lib={lib} />
|
||||
</div>
|
||||
<Collapse className="text-xs" isOpen={isOpen}>
|
||||
<div className="content pr-2">
|
||||
Available as{" "}
|
||||
<div className="accessor">
|
||||
{lib.accessor[lib.accessor.length - 1]}{" "}
|
||||
<div>
|
||||
<CopyIcon onClick={copyToClipboard} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Collapse>
|
||||
</Library>
|
||||
);
|
||||
}
|
||||
|
||||
function JSDependencies() {
|
||||
const libraries = useSelector(selectLibrariesForExplorer);
|
||||
const dependencyList = libraries.map((lib) => (
|
||||
<LibraryEntity key={lib.name} lib={lib} />
|
||||
));
|
||||
const isOpen = useSelector(selectIsInstallerOpen);
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const pagePermissions = useSelector(getPagePermissions);
|
||||
|
||||
const canCreateActions = hasCreateActionPermission(pagePermissions);
|
||||
|
||||
const openInstaller = useCallback(() => {
|
||||
dispatch(toggleInstaller(true));
|
||||
}, []);
|
||||
|
||||
const featureFlags = useSelector(selectFeatureFlags);
|
||||
|
||||
return (
|
||||
<Entity
|
||||
className={"libraries"}
|
||||
customAddButton={
|
||||
<TooltipComponent
|
||||
boundary="viewport"
|
||||
className={EntityClassNames.TOOLTIP}
|
||||
content={createMessage(customJSLibraryMessages.ADD_JS_LIBRARY)}
|
||||
disabled={isOpen}
|
||||
hoverOpenDelay={TOOLTIP_HOVER_ON_DELAY}
|
||||
position="right"
|
||||
>
|
||||
<EntityAddButton
|
||||
className={`${EntityClassNames.ADD_BUTTON} group libraries h-100 ${
|
||||
isOpen ? "selected" : ""
|
||||
}`}
|
||||
onClick={openInstaller}
|
||||
/>
|
||||
</TooltipComponent>
|
||||
}
|
||||
entityId="library_section"
|
||||
icon={null}
|
||||
isDefaultExpanded={isOpen}
|
||||
isSticky
|
||||
name="Libraries"
|
||||
showAddButton={canCreateActions && featureFlags?.CUSTOM_JS_LIBRARY}
|
||||
step={0}
|
||||
>
|
||||
{dependencyList}
|
||||
</Entity>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(JSDependencies);
|
||||
|
|
@ -0,0 +1,150 @@
|
|||
export default [
|
||||
{
|
||||
name: "uuidjs",
|
||||
url: "https://cdn.jsdelivr.net/npm/uuidjs@4.2.12/src/uuid.min.js",
|
||||
description:
|
||||
"UUID.js is a JavaScript/ECMAScript library to generate RFC 4122 compliant Universally Unique IDentifiers (UUIDs). This library supports both version 4 UUIDs (UUIDs from random numbers) and version 1 UUIDs (time-based UUIDs), and provides an object-oriented interface to print a generated or parsed UUID in a variety of forms.",
|
||||
author: "LiosK",
|
||||
docsURL:
|
||||
"https://github.com/LiosK/UUID.js/#uuidjs---rfc-compliant-uuid-generator-for-javascript",
|
||||
version: "4.2.12",
|
||||
icon: "https://github.com/LiosK.png?s=20",
|
||||
},
|
||||
{
|
||||
name: "i18next",
|
||||
url: "https://cdn.jsdelivr.net/npm/i18next@22.1.4/dist/umd/i18next.min.js",
|
||||
description: "i18next internationalization framework",
|
||||
author: "i18next",
|
||||
version: "22.1.4",
|
||||
icon: "https://github.com/i18next.png?s=20",
|
||||
docsURL: "https://www.i18next.com/overview/getting-started",
|
||||
},
|
||||
{
|
||||
name: "jsonwebtoken",
|
||||
description: "JSON Web Token implementation (symmetric and asymmetric)",
|
||||
author: "auth0",
|
||||
docsURL: "https://github.com/auth0/node-jsonwebtoken#readme",
|
||||
version: "8.5.1",
|
||||
url: `/libraries/jsonwebtoken@8.5.1.js`,
|
||||
icon: "https://github.com/auth0.png?s=20",
|
||||
},
|
||||
{
|
||||
name: "@supabase/supabase-js",
|
||||
description: "Isomorphic Javascript client for Supabase",
|
||||
author: "supabase",
|
||||
docsURL: "https://supabase.com/docs/reference/javascript",
|
||||
version: "2.1.0",
|
||||
url:
|
||||
"https://cdn.jsdelivr.net/npm/@supabase/supabase-js@2.1.0/dist/umd/supabase.min.js",
|
||||
icon: "https://github.com/supabase.png?s=20",
|
||||
},
|
||||
{
|
||||
name: "@segment/analytics-next",
|
||||
url:
|
||||
"https://cdn.jsdelivr.net/npm/@segment/analytics-next@1.46.1/dist/umd/index.js",
|
||||
description:
|
||||
"Analytics Next (aka Analytics 2.0) is the latest version of Segment’s JavaScript SDK - enabling you to send your data to any tool without having to learn, test, or use a new API every time.",
|
||||
author: "segmentio",
|
||||
docsURL:
|
||||
"https://github.com/segmentio/analytics-next/tree/master/packages/browser#readme",
|
||||
version: "1.46.1",
|
||||
icon: "https://github.com/segmentio.png?s=20",
|
||||
},
|
||||
{
|
||||
name: "mixpanel-browser",
|
||||
description: "The official Mixpanel JavaScript browser client library",
|
||||
author: "mixpanel",
|
||||
docsURL: "https://developer.mixpanel.com/docs/javascript",
|
||||
version: "2.1.0",
|
||||
url: `https://cdn.jsdelivr.net/npm/mixpanel-browser@2.45.0/dist/mixpanel.umd.js`,
|
||||
icon: "https://github.com/mixpanel.png?s=20",
|
||||
},
|
||||
{
|
||||
name: "fast-csv",
|
||||
description: "CSV parser and writer",
|
||||
author: "C2FO",
|
||||
docsURL:
|
||||
"https://c2fo.github.io/fast-csv/docs/introduction/getting-started/",
|
||||
version: "4.3.6",
|
||||
url: `/libraries/fast-csv@4.3.6.js`,
|
||||
icon: "https://github.com/C2FO.png?s=20",
|
||||
},
|
||||
{
|
||||
name: "ky",
|
||||
description: "Tiny and elegant HTTP client based on the browser Fetch API",
|
||||
author: "sindresorhus",
|
||||
docsURL: "https://github.com/sindresorhus/ky#usage",
|
||||
version: "0.25.0",
|
||||
url: "https://www.unpkg.com/ky@0.25.0/umd.js",
|
||||
icon: "https://github.com/sindresorhus.png?s=20",
|
||||
},
|
||||
{
|
||||
name: "jspdf",
|
||||
description: "PDF Document creation from JavaScript",
|
||||
author: "MrRio",
|
||||
docsURL: "https://raw.githack.com/MrRio/jsPDF/master/docs/index.html",
|
||||
version: "2.5.1",
|
||||
url: "https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js",
|
||||
icon: "https://github.com/MrRio.png?s=20",
|
||||
},
|
||||
{
|
||||
name: "@amplitude/analytics-browser",
|
||||
description: "Official Amplitude SDK for Web",
|
||||
author: "amplitude",
|
||||
docsURL:
|
||||
"https://github.com/amplitude/Amplitude-TypeScript/tree/main/packages/analytics-browser#usage",
|
||||
version: "1.6.1",
|
||||
url:
|
||||
"https://cdn.jsdelivr.net/npm/@amplitude/analytics-browser@1.6.1/lib/scripts/amplitude-min.umd.js",
|
||||
icon: "https://github.com/amplitude.png?s=20",
|
||||
},
|
||||
{
|
||||
name: "uzip-module",
|
||||
description: "Module version of UZIP.js",
|
||||
author: "greggman",
|
||||
docsURL: "https://github.com/greggman/uzip-module#uzip-module",
|
||||
version: "1.0.3",
|
||||
url: `https://cdn.jsdelivr.net/npm/uzip-module@1.0.3/dist/uzip.js`,
|
||||
icon: "https://github.com/greggman.png?s=20",
|
||||
},
|
||||
{
|
||||
name: "@sentry/browser",
|
||||
description: "Official Sentry SDK for browsers",
|
||||
author: "getsentry",
|
||||
docsURL: "https://docs.sentry.io/platforms/javascript/",
|
||||
version: "7.17.3",
|
||||
url: "https://browser.sentry-cdn.com/7.17.3/bundle.min.js",
|
||||
icon: "https://github.com/getsentry.png?s=20",
|
||||
},
|
||||
{
|
||||
name: "browser-image-compression",
|
||||
url:
|
||||
"https://cdn.jsdelivr.net/npm/browser-image-compression@2.0.0/dist/browser-image-compression.min.js",
|
||||
version: "2.0.0",
|
||||
author: "Donaldcwl",
|
||||
docsURL:
|
||||
"https://github.com/Donaldcwl/browser-image-compression/#browser-image-compression",
|
||||
description: "Compress images in the browser",
|
||||
icon: "https://github.com/Donaldcwl.png?s=20",
|
||||
},
|
||||
{
|
||||
name: "crypto-js",
|
||||
url:
|
||||
"https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.1.1/crypto-js.min.js",
|
||||
description: "JavaScript library of crypto standards",
|
||||
version: "4.1.1",
|
||||
author: "brix",
|
||||
docsURL: "https://github.com/brix/crypto-js/#crypto-js",
|
||||
icon: "https://github.com/brix.png?s=20",
|
||||
},
|
||||
{
|
||||
name: "jsonpath",
|
||||
url: "https://cdn.jsdelivr.net/npm/jsonpath@1.1.1/jsonpath.min.js",
|
||||
description:
|
||||
"Query JavaScript objects with JSONPath expressions. Robust / safe JSONPath engine for Node.js",
|
||||
version: "1.1.1",
|
||||
author: "dchester",
|
||||
docsURL: "https://github.com/dchester/jsonpath/#jsonpath",
|
||||
icon: "https://github.com/dchester.png?s=20",
|
||||
},
|
||||
];
|
||||
|
|
@ -49,6 +49,7 @@ import { setExplorerPinnedAction } from "actions/explorerActions";
|
|||
import { setIsGitSyncModalOpen } from "actions/gitSyncActions";
|
||||
import { GitSyncModalTab } from "entities/GitSync";
|
||||
import { matchBuilderPath } from "constants/routes";
|
||||
import { toggleInstaller } from "actions/JSLibraryActions";
|
||||
|
||||
type Props = {
|
||||
copySelectedWidget: () => void;
|
||||
|
|
@ -77,6 +78,7 @@ type Props = {
|
|||
setExplorerPinnedAction: (shouldPinned: boolean) => void;
|
||||
showCommitModal: () => void;
|
||||
getMousePosition: () => { x: number; y: number };
|
||||
hideInstaller: () => void;
|
||||
};
|
||||
|
||||
@HotkeysTarget
|
||||
|
|
@ -109,6 +111,7 @@ class GlobalHotKeys extends React.Component<Props> {
|
|||
|
||||
const category = filterCategories[categoryId];
|
||||
this.props.setGlobalSearchCategory(category);
|
||||
this.props.hideInstaller();
|
||||
AnalyticsUtil.logEvent("OPEN_OMNIBAR", {
|
||||
source: "HOTKEY_COMBO",
|
||||
category: category.title,
|
||||
|
|
@ -359,6 +362,7 @@ class GlobalHotKeys extends React.Component<Props> {
|
|||
label="Pin/Unpin Entity Explorer"
|
||||
onKeyDown={() => {
|
||||
this.props.setExplorerPinnedAction(!this.props.isExplorerPinned);
|
||||
this.props.hideInstaller();
|
||||
}}
|
||||
/>
|
||||
<Hotkey
|
||||
|
|
@ -414,6 +418,7 @@ const mapDispatchToProps = (dispatch: any) => {
|
|||
dispatch(
|
||||
setIsGitSyncModalOpen({ isOpen: true, tab: GitSyncModalTab.DEPLOY }),
|
||||
),
|
||||
hideInstaller: () => dispatch(toggleInstaller(false)),
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import EntityExplorerSidebar from "components/editorComponents/Sidebar";
|
|||
import classNames from "classnames";
|
||||
import { previewModeSelector } from "selectors/editorSelectors";
|
||||
import { routeChanged } from "actions/focusHistoryActions";
|
||||
import { Installer } from "pages/Editor/Explorer/Libraries/Installer";
|
||||
import { getExplorerWidth } from "selectors/explorerSelector";
|
||||
import { AppsmithLocationState } from "utils/history";
|
||||
|
||||
|
|
@ -96,6 +97,7 @@ function MainContainer() {
|
|||
"transition-all transform duration-400": true,
|
||||
})}
|
||||
/>
|
||||
<Installer left={sidebarWidth} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -51,7 +51,13 @@ const initialState: EditorContextState = {
|
|||
explorerSwitchIndex: 0,
|
||||
};
|
||||
|
||||
const entitySections = ["Pages", "Widgets", "Queries/JS", "Datasources"];
|
||||
const entitySections = [
|
||||
"Pages",
|
||||
"Widgets",
|
||||
"Queries/JS",
|
||||
"Datasources",
|
||||
"Libraries",
|
||||
];
|
||||
|
||||
export const isSubEntities = (name: string): boolean => {
|
||||
return entitySections.indexOf(name) < 0;
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ import mainCanvasReducer from "./mainCanvasReducer";
|
|||
import focusHistoryReducer from "./focusHistoryReducer";
|
||||
import { editorContextReducer } from "./editorContextReducer";
|
||||
import guidedTourReducer from "./guidedTourReducer";
|
||||
import libraryReducer from "./libraryReducer";
|
||||
import appSettingsPaneReducer from "./appSettingsPaneReducer";
|
||||
import autoHeightUIReducer from "./autoHeightReducer";
|
||||
|
||||
|
|
@ -90,6 +91,7 @@ const uiReducer = combineReducers({
|
|||
appSettingsPane: appSettingsPaneReducer,
|
||||
focusHistory: focusHistoryReducer,
|
||||
editorContext: editorContextReducer,
|
||||
libraries: libraryReducer,
|
||||
autoHeightUI: autoHeightUIReducer,
|
||||
});
|
||||
|
||||
|
|
|
|||
116
app/client/src/reducers/uiReducers/libraryReducer.ts
Normal file
116
app/client/src/reducers/uiReducers/libraryReducer.ts
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
import { createImmerReducer } from "utils/ReducerUtils";
|
||||
import {
|
||||
ReduxAction,
|
||||
ReduxActionErrorTypes,
|
||||
ReduxActionTypes,
|
||||
} from "@appsmith/constants/ReduxActionConstants";
|
||||
import recommendedLibraries from "pages/Editor/Explorer/Libraries/recommendedLibraries";
|
||||
import { defaultLibraries, TJSLibrary } from "workers/common/JSLibrary";
|
||||
|
||||
export enum InstallState {
|
||||
Queued,
|
||||
Installing,
|
||||
Failed,
|
||||
Success,
|
||||
}
|
||||
|
||||
export type LibraryState = {
|
||||
installationStatus: Record<string, InstallState>;
|
||||
installedLibraries: TJSLibrary[];
|
||||
isInstallerOpen: boolean;
|
||||
};
|
||||
|
||||
const initialState = {
|
||||
isInstallerOpen: false,
|
||||
installationStatus: {},
|
||||
installedLibraries: defaultLibraries.map((lib: TJSLibrary) => {
|
||||
return {
|
||||
name: lib.name,
|
||||
docsURL: lib.docsURL,
|
||||
version: lib.version,
|
||||
accessor: lib.accessor,
|
||||
};
|
||||
}),
|
||||
reservedNames: [],
|
||||
};
|
||||
|
||||
const jsLibraryReducer = createImmerReducer(initialState, {
|
||||
[ReduxActionTypes.INSTALL_LIBRARY_INIT]: (
|
||||
state: LibraryState,
|
||||
action: ReduxAction<Partial<TJSLibrary>>,
|
||||
) => {
|
||||
const { url } = action.payload;
|
||||
state.installationStatus[url as string] =
|
||||
state.installationStatus[url as string] || InstallState.Queued;
|
||||
},
|
||||
[ReduxActionTypes.INSTALL_LIBRARY_START]: (
|
||||
state: LibraryState,
|
||||
action: ReduxAction<string>,
|
||||
) => {
|
||||
state.installationStatus[action.payload] = InstallState.Queued;
|
||||
},
|
||||
[ReduxActionTypes.INSTALL_LIBRARY_SUCCESS]: (
|
||||
state: LibraryState,
|
||||
action: ReduxAction<{
|
||||
accessor: string[];
|
||||
url: string;
|
||||
version: string;
|
||||
}>,
|
||||
) => {
|
||||
const { accessor, url, version } = action.payload;
|
||||
const name = accessor[accessor.length - 1] as string;
|
||||
const recommendedLibrary = recommendedLibraries.find(
|
||||
(lib) => lib.url === url,
|
||||
);
|
||||
state.installationStatus[url] = InstallState.Success;
|
||||
state.installedLibraries.unshift({
|
||||
name: recommendedLibrary?.name || name,
|
||||
docsURL: recommendedLibrary?.url || url,
|
||||
version: recommendedLibrary?.version || version,
|
||||
url,
|
||||
accessor,
|
||||
});
|
||||
},
|
||||
[ReduxActionErrorTypes.INSTALL_LIBRARY_FAILED]: (
|
||||
state: LibraryState,
|
||||
action: ReduxAction<{ url: string }>,
|
||||
) => {
|
||||
state.installationStatus[action.payload.url] = InstallState.Failed;
|
||||
},
|
||||
[ReduxActionTypes.CLEAR_PROCESSED_INSTALLS]: (state: LibraryState) => {
|
||||
for (const key in state.installationStatus) {
|
||||
if (
|
||||
[InstallState.Success, InstallState.Failed].includes(
|
||||
state.installationStatus[key],
|
||||
)
|
||||
) {
|
||||
delete state.installationStatus[key];
|
||||
}
|
||||
}
|
||||
},
|
||||
[ReduxActionTypes.FETCH_JS_LIBRARIES_SUCCESS]: (
|
||||
state: LibraryState,
|
||||
action: ReduxAction<TJSLibrary[]>,
|
||||
) => {
|
||||
state.installedLibraries = action.payload.concat(
|
||||
initialState.installedLibraries,
|
||||
);
|
||||
},
|
||||
[ReduxActionTypes.UNINSTALL_LIBRARY_SUCCESS]: (
|
||||
state: LibraryState,
|
||||
action: ReduxAction<TJSLibrary>,
|
||||
) => {
|
||||
const uLib = action.payload;
|
||||
state.installedLibraries = state.installedLibraries.filter(
|
||||
(lib) => uLib.url !== lib.url,
|
||||
);
|
||||
},
|
||||
[ReduxActionTypes.TOGGLE_INSTALLER]: (
|
||||
state: LibraryState,
|
||||
action: ReduxAction<boolean>,
|
||||
) => {
|
||||
state.isInstallerOpen = action.payload;
|
||||
},
|
||||
});
|
||||
|
||||
export default jsLibraryReducer;
|
||||
|
|
@ -8,6 +8,20 @@ import {
|
|||
} from "entities/DataTree/actionTriggers";
|
||||
import { ActionValidationError } from "sagas/ActionExecution/errorUtils";
|
||||
import { isBase64String, isUrlString } from "./downloadActionUtils";
|
||||
import { isBlobUrl } from "utils/AppsmithUtils";
|
||||
|
||||
function downloadBlobURL(url: string, name: string) {
|
||||
const ele = document.createElement("a");
|
||||
ele.href = url;
|
||||
ele.download = name;
|
||||
ele.style.display = "none";
|
||||
document.body.appendChild(ele);
|
||||
ele.click();
|
||||
setTimeout(() => {
|
||||
URL.revokeObjectURL(url);
|
||||
document.body.removeChild(ele);
|
||||
});
|
||||
}
|
||||
|
||||
export default async function downloadSaga(
|
||||
action: DownloadActionDescription["payload"],
|
||||
|
|
@ -21,6 +35,13 @@ export default async function downloadSaga(
|
|||
getType(name),
|
||||
);
|
||||
}
|
||||
if (isBlobUrl(data)) {
|
||||
downloadBlobURL(data, name);
|
||||
AppsmithConsole.info({
|
||||
text: `download('${data}', '${name}', '${type}') was triggered`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
const dataType = getType(data);
|
||||
if (dataType === Types.ARRAY || dataType === Types.OBJECT) {
|
||||
const jsonString = JSON.stringify(data, null, 2);
|
||||
|
|
|
|||
|
|
@ -27,9 +27,12 @@ import WidgetFactory, { WidgetTypeConfigMap } from "utils/WidgetFactory";
|
|||
import { GracefulWorkerService } from "utils/WorkerUtil";
|
||||
import {
|
||||
EvalError,
|
||||
EVAL_WORKER_ACTIONS,
|
||||
PropertyEvaluationErrorType,
|
||||
} from "utils/DynamicBindingUtils";
|
||||
import {
|
||||
EVAL_WORKER_ACTIONS,
|
||||
MAIN_THREAD_ACTION,
|
||||
} from "workers/Evaluation/evalWorkerActions";
|
||||
import log from "loglevel";
|
||||
import { WidgetProps } from "widgets/BaseWidget";
|
||||
import PerformanceTracker, {
|
||||
|
|
@ -103,11 +106,12 @@ import { CanvasWidgetsReduxState } from "reducers/entityReducers/canvasWidgetsRe
|
|||
import { AppTheme } from "entities/AppTheming";
|
||||
import { ActionValidationConfigMap } from "constants/PropertyControlConstants";
|
||||
import { storeLogs, updateTriggerMeta } from "./DebuggerSagas";
|
||||
import { lintTreeSaga, lintWorker } from "./LintingSagas";
|
||||
import { lintWorker } from "./LintingSagas";
|
||||
import {
|
||||
EvalTreeRequestData,
|
||||
EvalTreeResponseData,
|
||||
} from "workers/Evaluation/types";
|
||||
import { MessageType, TMessage } from "utils/MessageUtil";
|
||||
|
||||
const evalWorker = new GracefulWorkerService(
|
||||
new Worker(
|
||||
|
|
@ -121,10 +125,21 @@ const evalWorker = new GracefulWorkerService(
|
|||
|
||||
let widgetTypeConfigMap: WidgetTypeConfigMap;
|
||||
|
||||
function* evaluateTreeSaga(
|
||||
/**
|
||||
* This saga is responsible for evaluating the data tree
|
||||
* @param postEvalActions
|
||||
* @param shouldReplay
|
||||
* @param requiresLinting
|
||||
* @param forceEvaluation - if true, will re-evaluate the entire tree
|
||||
* @returns
|
||||
* @example
|
||||
* yield call(evaluateTreeSaga, postEvalActions, shouldReplay, requiresLinting, forceEvaluation)
|
||||
*/
|
||||
export function* evaluateTreeSaga(
|
||||
postEvalActions?: Array<AnyReduxAction>,
|
||||
shouldReplay = true,
|
||||
requiresLinting = false,
|
||||
forceEvaluation = false,
|
||||
) {
|
||||
const allActionValidationConfig: {
|
||||
[actionId: string]: ActionValidationConfigMap;
|
||||
|
|
@ -146,6 +161,7 @@ function* evaluateTreeSaga(
|
|||
shouldReplay,
|
||||
allActionValidationConfig,
|
||||
requiresLinting: isEditMode && requiresLinting,
|
||||
forceEvaluation,
|
||||
};
|
||||
|
||||
const workerResponse: EvalTreeResponseData = yield call(
|
||||
|
|
@ -270,8 +286,8 @@ export function* evaluateAndExecuteDynamicTrigger(
|
|||
) {
|
||||
const unEvalTree: DataTree = yield select(getUnevaluatedDataTree);
|
||||
log.debug({ execute: dynamicTrigger });
|
||||
const { isFinishedChannel } = yield call(
|
||||
evalWorker.duplexRequest,
|
||||
const response: unknown = yield call(
|
||||
evalWorker.request,
|
||||
EVAL_WORKER_ACTIONS.EVAL_TRIGGER,
|
||||
{
|
||||
unEvalTree,
|
||||
|
|
@ -283,122 +299,90 @@ export function* evaluateAndExecuteDynamicTrigger(
|
|||
},
|
||||
);
|
||||
|
||||
let keepAlive = true;
|
||||
|
||||
while (keepAlive) {
|
||||
const { requestData } = yield take(isFinishedChannel);
|
||||
log.debug({ requestData, eventType, triggerMeta, dynamicTrigger });
|
||||
|
||||
if (requestData.finished) {
|
||||
keepAlive = false;
|
||||
|
||||
const { result } = requestData;
|
||||
const { logs = [], errors = [], triggers = [] } = response as any;
|
||||
yield call(updateTriggerMeta, triggerMeta, dynamicTrigger);
|
||||
|
||||
// Check for any logs in the response and store them in the redux store
|
||||
if (
|
||||
!!result &&
|
||||
result.hasOwnProperty("logs") &&
|
||||
!!result.logs &&
|
||||
result.logs.length
|
||||
) {
|
||||
yield call(
|
||||
storeLogs,
|
||||
result.logs,
|
||||
logs,
|
||||
triggerMeta.source?.name || triggerMeta.triggerPropertyName || "",
|
||||
eventType === EventType.ON_JS_FUNCTION_EXECUTE
|
||||
? ENTITY_TYPE.JSACTION
|
||||
: ENTITY_TYPE.WIDGET,
|
||||
triggerMeta.source?.id || "",
|
||||
);
|
||||
}
|
||||
|
||||
/* Handle errors during evaluation
|
||||
* A finish event with errors means that the error was not caught by the user code.
|
||||
* We raise an error telling the user that an uncaught error has occurred
|
||||
* */
|
||||
yield call(evalErrorHandler, errors);
|
||||
if (errors.length) {
|
||||
if (
|
||||
!!result &&
|
||||
result.hasOwnProperty("errors") &&
|
||||
!!result.errors &&
|
||||
result.errors.length
|
||||
) {
|
||||
if (
|
||||
result.errors[0].errorMessage !==
|
||||
errors[0].errorMessage !==
|
||||
"UncaughtPromiseRejection: User cancelled action execution"
|
||||
) {
|
||||
throw new UncaughtPromiseError(result.errors[0].errorMessage);
|
||||
throw new UncaughtPromiseError(errors[0].errorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
// It is possible to get a few triggers here if the user
|
||||
// still uses the old way of action runs and not promises. For that we
|
||||
// need to manually execute these triggers outside the promise flow
|
||||
const { triggers } = result;
|
||||
if (triggers && triggers.length) {
|
||||
log.debug({ triggers });
|
||||
yield all(
|
||||
triggers.map((trigger: ActionDescription) =>
|
||||
call(executeActionTriggers, trigger, eventType, triggerMeta),
|
||||
),
|
||||
);
|
||||
|
||||
return response;
|
||||
}
|
||||
// Return value of a promise is returned
|
||||
isFinishedChannel.close();
|
||||
return result;
|
||||
}
|
||||
yield call(evalErrorHandler, requestData.errors);
|
||||
isFinishedChannel.close();
|
||||
|
||||
export function* handleEvalWorkerRequestSaga(listenerChannel: Channel<any>) {
|
||||
while (true) {
|
||||
const request: TMessage<any> = yield take(listenerChannel);
|
||||
yield spawn(handleEvalWorkerMessage, request);
|
||||
}
|
||||
}
|
||||
|
||||
export function* executeDynamicTriggerRequest(
|
||||
mainThreadRequestChannel: Channel<any>,
|
||||
) {
|
||||
while (true) {
|
||||
const { mainThreadResponseChannel, requestData, requestId } = yield take(
|
||||
mainThreadRequestChannel,
|
||||
);
|
||||
log.debug({ requestData });
|
||||
if (requestData?.logs) {
|
||||
const { eventType, triggerMeta } = requestData;
|
||||
export function* handleEvalWorkerMessage(message: TMessage<any>) {
|
||||
const { body, messageType } = message;
|
||||
const { data, method } = body;
|
||||
switch (method) {
|
||||
case MAIN_THREAD_ACTION.LINT_TREE: {
|
||||
yield put({
|
||||
type: ReduxActionTypes.LINT_TREE,
|
||||
payload: {
|
||||
pathsToLint: data.lintOrder,
|
||||
unevalTree: data.unevalTree,
|
||||
},
|
||||
});
|
||||
break;
|
||||
}
|
||||
case MAIN_THREAD_ACTION.PROCESS_LOGS: {
|
||||
const { logs = [], triggerMeta, eventType } = data;
|
||||
yield call(
|
||||
storeLogs,
|
||||
requestData.logs,
|
||||
logs,
|
||||
triggerMeta?.source?.name || triggerMeta?.triggerPropertyName || "",
|
||||
eventType === EventType.ON_JS_FUNCTION_EXECUTE
|
||||
? ENTITY_TYPE.JSACTION
|
||||
: ENTITY_TYPE.WIDGET,
|
||||
triggerMeta?.source?.id || "",
|
||||
);
|
||||
break;
|
||||
}
|
||||
if (requestData?.trigger) {
|
||||
// if we have found a trigger, we need to execute it and respond back
|
||||
log.debug({ trigger: requestData.trigger });
|
||||
yield spawn(
|
||||
case MAIN_THREAD_ACTION.PROCESS_TRIGGER: {
|
||||
const { eventType, trigger, triggerMeta } = data;
|
||||
log.debug({ trigger: data.trigger });
|
||||
const result: ResponsePayload = yield call(
|
||||
executeTriggerRequestSaga,
|
||||
requestId,
|
||||
requestData,
|
||||
requestData.eventType,
|
||||
mainThreadResponseChannel,
|
||||
requestData.triggerMeta,
|
||||
trigger,
|
||||
eventType,
|
||||
triggerMeta,
|
||||
);
|
||||
}
|
||||
if (requestData.type === EVAL_WORKER_ACTIONS.LINT_TREE) {
|
||||
yield spawn(lintTreeSaga, {
|
||||
pathsToLint: requestData.lintOrder,
|
||||
unevalTree: requestData.unevalTree,
|
||||
});
|
||||
}
|
||||
if (requestData?.errors) {
|
||||
yield call(evalErrorHandler, requestData.errors);
|
||||
if (messageType === MessageType.REQUEST)
|
||||
yield call(evalWorker.respond, message.messageId, result);
|
||||
break;
|
||||
}
|
||||
}
|
||||
yield call(evalErrorHandler, data?.errors || []);
|
||||
}
|
||||
|
||||
interface ResponsePayload {
|
||||
data: {
|
||||
subRequestId: string;
|
||||
reason?: string;
|
||||
resolve?: unknown;
|
||||
};
|
||||
|
|
@ -411,17 +395,14 @@ interface ResponsePayload {
|
|||
* resolve or reject it with the data the execution has provided
|
||||
*/
|
||||
function* executeTriggerRequestSaga(
|
||||
requestId: string,
|
||||
requestData: { trigger: ActionDescription; subRequestId: string },
|
||||
trigger: ActionDescription,
|
||||
eventType: EventType,
|
||||
responseFromExecutionChannel: Channel<unknown>,
|
||||
triggerMeta: TriggerMeta,
|
||||
) {
|
||||
const responsePayload: ResponsePayload = {
|
||||
data: {
|
||||
resolve: undefined,
|
||||
reason: undefined,
|
||||
subRequestId: requestData.subRequestId,
|
||||
},
|
||||
success: false,
|
||||
eventType,
|
||||
|
|
@ -429,7 +410,7 @@ function* executeTriggerRequestSaga(
|
|||
try {
|
||||
responsePayload.data.resolve = yield call(
|
||||
executeActionTriggers,
|
||||
requestData.trigger,
|
||||
trigger,
|
||||
eventType,
|
||||
triggerMeta,
|
||||
);
|
||||
|
|
@ -442,11 +423,7 @@ function* executeTriggerRequestSaga(
|
|||
responsePayload.data.reason = { message: error.message };
|
||||
responsePayload.success = false;
|
||||
}
|
||||
responseFromExecutionChannel.put({
|
||||
method: EVAL_WORKER_ACTIONS.PROCESS_TRIGGER,
|
||||
requestId: requestId,
|
||||
...responsePayload,
|
||||
});
|
||||
return responsePayload;
|
||||
}
|
||||
|
||||
export function* clearEvalCache() {
|
||||
|
|
@ -497,9 +474,8 @@ export function* executeFunction(
|
|||
},
|
||||
);
|
||||
|
||||
const { logs } = response;
|
||||
const { logs = [] } = response;
|
||||
// Check for any logs in the response and store them in the redux store
|
||||
if (!!logs && logs.length > 0) {
|
||||
yield call(
|
||||
storeLogs,
|
||||
logs,
|
||||
|
|
@ -508,7 +484,6 @@ export function* executeFunction(
|
|||
collectionId,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const { errors, result } = response;
|
||||
|
||||
|
|
@ -611,16 +586,16 @@ function getPostEvalActions(
|
|||
return postEvalActions;
|
||||
}
|
||||
|
||||
function* evaluationChangeListenerSaga() {
|
||||
function* evaluationChangeListenerSaga(): any {
|
||||
// Explicitly shutdown old worker if present
|
||||
yield all([call(evalWorker.shutdown), call(lintWorker.shutdown)]);
|
||||
const [{ mainThreadRequestChannel }] = yield all([
|
||||
const [evalWorkerListenerChannel] = yield all([
|
||||
call(evalWorker.start),
|
||||
call(lintWorker.start),
|
||||
]);
|
||||
|
||||
yield call(evalWorker.request, EVAL_WORKER_ACTIONS.SETUP);
|
||||
yield spawn(executeDynamicTriggerRequest, mainThreadRequestChannel);
|
||||
yield spawn(handleEvalWorkerRequestSaga, evalWorkerListenerChannel);
|
||||
|
||||
widgetTypeConfigMap = WidgetFactory.getWidgetTypeConfigMap();
|
||||
const initAction: {
|
||||
|
|
@ -654,7 +629,7 @@ export function* evaluateSnippetSaga(action: any) {
|
|||
let { expression } = action.payload;
|
||||
const { dataType, isTrigger } = action.payload;
|
||||
if (isTrigger) {
|
||||
expression = `function() { ${expression} }`;
|
||||
expression = `(function() { ${expression} })()`;
|
||||
}
|
||||
const workerResponse: {
|
||||
errors: any;
|
||||
|
|
@ -663,7 +638,6 @@ export function* evaluateSnippetSaga(action: any) {
|
|||
} = yield call(evalWorker.request, EVAL_WORKER_ACTIONS.EVAL_EXPRESSION, {
|
||||
expression,
|
||||
dataType,
|
||||
isTrigger,
|
||||
});
|
||||
const { errors, result, triggers } = workerResponse;
|
||||
if (triggers && triggers.length > 0) {
|
||||
|
|
@ -830,3 +804,5 @@ export default function* evaluationSagaListeners() {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
export { evalWorker as EvalWorker };
|
||||
|
|
|
|||
405
app/client/src/sagas/JSLibrarySaga.ts
Normal file
405
app/client/src/sagas/JSLibrarySaga.ts
Normal file
|
|
@ -0,0 +1,405 @@
|
|||
import { ApiResponse } from "api/ApiResponses";
|
||||
import LibraryApi from "api/LibraryAPI";
|
||||
import {
|
||||
createMessage,
|
||||
customJSLibraryMessages,
|
||||
} from "@appsmith/constants/messages";
|
||||
import {
|
||||
ReduxAction,
|
||||
ReduxActionErrorTypes,
|
||||
ReduxActionTypes,
|
||||
} from "@appsmith/constants/ReduxActionConstants";
|
||||
import { Toaster, Variant } from "design-system";
|
||||
import {
|
||||
actionChannel,
|
||||
ActionPattern,
|
||||
all,
|
||||
call,
|
||||
put,
|
||||
select,
|
||||
take,
|
||||
takeEvery,
|
||||
takeLatest,
|
||||
} from "redux-saga/effects";
|
||||
import { getCurrentApplicationId } from "selectors/editorSelectors";
|
||||
import CodemirrorTernService from "utils/autocomplete/CodemirrorTernService";
|
||||
import { EVAL_WORKER_ACTIONS } from "workers/Evaluation/evalWorkerActions";
|
||||
import { validateResponse } from "./ErrorSagas";
|
||||
import { evaluateTreeSaga, EvalWorker } from "./EvaluationsSaga";
|
||||
import log from "loglevel";
|
||||
import { APP_MODE } from "entities/App";
|
||||
import { getAppMode } from "selectors/applicationSelectors";
|
||||
import AnalyticsUtil from "utils/AnalyticsUtil";
|
||||
import { TJSLibrary } from "workers/common/JSLibrary";
|
||||
import { getUsedActionNames } from "selectors/actionSelectors";
|
||||
import AppsmithConsole from "utils/AppsmithConsole";
|
||||
import { selectInstalledLibraries } from "selectors/entitiesSelector";
|
||||
|
||||
export function parseErrorMessage(text: string) {
|
||||
return text
|
||||
.split(": ")
|
||||
.slice(1)
|
||||
.join("");
|
||||
}
|
||||
|
||||
function* handleInstallationFailure(
|
||||
url: string,
|
||||
message: string,
|
||||
accessor?: string[],
|
||||
) {
|
||||
if (accessor) {
|
||||
yield call(
|
||||
EvalWorker.request,
|
||||
EVAL_WORKER_ACTIONS.UNINSTALL_LIBRARY,
|
||||
accessor,
|
||||
);
|
||||
}
|
||||
|
||||
AppsmithConsole.error({
|
||||
text: `Failed to install library script at ${url}`,
|
||||
});
|
||||
|
||||
Toaster.show({
|
||||
text: message || `Failed to install library script at ${url}`,
|
||||
variant: Variant.danger,
|
||||
});
|
||||
yield put({
|
||||
type: ReduxActionErrorTypes.INSTALL_LIBRARY_FAILED,
|
||||
payload: { url, show: false },
|
||||
});
|
||||
AnalyticsUtil.logEvent("INSTALL_LIBRARY", { url, success: false });
|
||||
}
|
||||
|
||||
export function* installLibrarySaga(lib: Partial<TJSLibrary>) {
|
||||
const { url } = lib;
|
||||
|
||||
const takenNamesMap: Record<string, true> = yield select(
|
||||
getUsedActionNames,
|
||||
"",
|
||||
);
|
||||
|
||||
const installedLibraries: TJSLibrary[] = yield select(
|
||||
selectInstalledLibraries,
|
||||
);
|
||||
|
||||
const takenAccessors = ([] as string[]).concat(
|
||||
...installedLibraries.map((lib) => lib.accessor),
|
||||
);
|
||||
|
||||
const { accessor, defs, error, success } = yield call(
|
||||
EvalWorker.request,
|
||||
EVAL_WORKER_ACTIONS.INSTALL_LIBRARY,
|
||||
{
|
||||
url,
|
||||
takenNamesMap,
|
||||
takenAccessors,
|
||||
},
|
||||
);
|
||||
|
||||
if (!success) {
|
||||
log.debug("Failed to install locally");
|
||||
yield call(handleInstallationFailure, url as string, error?.message);
|
||||
return;
|
||||
}
|
||||
|
||||
const name: string = lib.name || accessor[accessor.length - 1];
|
||||
const applicationId: string = yield select(getCurrentApplicationId);
|
||||
|
||||
const versionMatch = (url as string).match(/(?:@)(\d+\.)(\d+\.)(\d+)/);
|
||||
let [version = ""] = versionMatch ? versionMatch : [];
|
||||
version = version.startsWith("@") ? version.slice(1) : version;
|
||||
|
||||
let stringifiedDefs = "";
|
||||
|
||||
try {
|
||||
stringifiedDefs = JSON.stringify(defs);
|
||||
} catch (e) {
|
||||
stringifiedDefs = JSON.stringify({
|
||||
"!name": `LIB/${accessor[accessor.length - 1]}`,
|
||||
});
|
||||
}
|
||||
|
||||
const response: ApiResponse<boolean> = yield call(
|
||||
LibraryApi.addLibrary,
|
||||
applicationId,
|
||||
{
|
||||
name,
|
||||
version,
|
||||
accessor,
|
||||
defs: stringifiedDefs,
|
||||
url,
|
||||
},
|
||||
);
|
||||
|
||||
try {
|
||||
const isValidResponse: boolean = yield validateResponse(response, false);
|
||||
if (!isValidResponse || !response.data) {
|
||||
log.debug("Install API failed");
|
||||
yield call(handleInstallationFailure, url as string, "", accessor);
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
yield call(
|
||||
handleInstallationFailure,
|
||||
url as string,
|
||||
(e as Error).message,
|
||||
accessor,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
CodemirrorTernService.updateDef(defs["!name"], defs);
|
||||
AnalyticsUtil.logEvent("DEFINITIONS_GENERATION", { url, success: true });
|
||||
} catch (e) {
|
||||
Toaster.show({
|
||||
text: createMessage(customJSLibraryMessages.AUTOCOMPLETE_FAILED, name),
|
||||
variant: Variant.warning,
|
||||
});
|
||||
AppsmithConsole.warning({
|
||||
text: `Failed to generate code definitions for ${name}`,
|
||||
});
|
||||
AnalyticsUtil.logEvent("DEFINITIONS_GENERATION", { url, success: false });
|
||||
log.debug("Failed to update Tern defs", e);
|
||||
}
|
||||
|
||||
yield put({
|
||||
type: ReduxActionTypes.UPDATE_LINT_GLOBALS,
|
||||
payload: {
|
||||
libs: [
|
||||
{
|
||||
name,
|
||||
version,
|
||||
url,
|
||||
accessor,
|
||||
},
|
||||
],
|
||||
add: true,
|
||||
},
|
||||
});
|
||||
|
||||
//TODO: Check if we could avoid this.
|
||||
yield call(evaluateTreeSaga, [], false, true, true);
|
||||
|
||||
yield put({
|
||||
type: ReduxActionTypes.INSTALL_LIBRARY_SUCCESS,
|
||||
payload: {
|
||||
url,
|
||||
accessor,
|
||||
version,
|
||||
name,
|
||||
},
|
||||
});
|
||||
|
||||
Toaster.show({
|
||||
text: createMessage(
|
||||
customJSLibraryMessages.INSTALLATION_SUCCESSFUL,
|
||||
accessor[accessor.length - 1],
|
||||
),
|
||||
variant: Variant.success,
|
||||
});
|
||||
AnalyticsUtil.logEvent("INSTALL_LIBRARY", {
|
||||
url,
|
||||
namespace: accessor.join("."),
|
||||
success: true,
|
||||
});
|
||||
|
||||
AppsmithConsole.info({
|
||||
text: `${name} installed successfully`,
|
||||
});
|
||||
}
|
||||
|
||||
function* uninstallLibrarySaga(action: ReduxAction<TJSLibrary>) {
|
||||
const { accessor, name } = action.payload;
|
||||
const applicationId: string = yield select(getCurrentApplicationId);
|
||||
|
||||
try {
|
||||
const response: ApiResponse = yield call(
|
||||
LibraryApi.removeLibrary,
|
||||
applicationId,
|
||||
action.payload,
|
||||
);
|
||||
|
||||
const isValidResponse: boolean = yield validateResponse(response);
|
||||
|
||||
if (!isValidResponse) {
|
||||
yield put({
|
||||
type: ReduxActionErrorTypes.UNINSTALL_LIBRARY_FAILED,
|
||||
payload: accessor,
|
||||
});
|
||||
AnalyticsUtil.logEvent("UNINSTALL_LIBRARY", {
|
||||
url: action.payload.url,
|
||||
success: false,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
yield put({
|
||||
type: ReduxActionTypes.UPDATE_LINT_GLOBALS,
|
||||
payload: {
|
||||
libs: [action.payload],
|
||||
add: false,
|
||||
},
|
||||
});
|
||||
|
||||
const { success }: { success: boolean } = yield call(
|
||||
EvalWorker.request,
|
||||
EVAL_WORKER_ACTIONS.UNINSTALL_LIBRARY,
|
||||
accessor,
|
||||
);
|
||||
if (!success) {
|
||||
Toaster.show({
|
||||
text: createMessage(customJSLibraryMessages.UNINSTALL_FAILED, name),
|
||||
variant: Variant.danger,
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
CodemirrorTernService.removeDef(`LIB/${accessor[accessor.length - 1]}`);
|
||||
} catch (e) {
|
||||
log.debug(`Failed to remove definitions for ${name}`, e);
|
||||
}
|
||||
|
||||
yield call(evaluateTreeSaga, [], false, true, true);
|
||||
|
||||
yield put({
|
||||
type: ReduxActionTypes.UNINSTALL_LIBRARY_SUCCESS,
|
||||
payload: action.payload,
|
||||
});
|
||||
|
||||
Toaster.show({
|
||||
text: createMessage(customJSLibraryMessages.UNINSTALL_SUCCESS, name),
|
||||
variant: Variant.success,
|
||||
});
|
||||
AnalyticsUtil.logEvent("UNINSTALL_LIBRARY", {
|
||||
url: action.payload.url,
|
||||
success: true,
|
||||
});
|
||||
} catch (e) {
|
||||
Toaster.show({
|
||||
text: createMessage(customJSLibraryMessages.UNINSTALL_FAILED, name),
|
||||
variant: Variant.danger,
|
||||
});
|
||||
AnalyticsUtil.logEvent("UNINSTALL_LIBRARY", {
|
||||
url: action.payload.url,
|
||||
success: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function* fetchJSLibraries(action: ReduxAction<string>) {
|
||||
const applicationId: string = action.payload;
|
||||
const mode: APP_MODE = yield select(getAppMode);
|
||||
try {
|
||||
const response: ApiResponse = yield call(
|
||||
LibraryApi.getLibraries,
|
||||
applicationId,
|
||||
mode,
|
||||
);
|
||||
const isValidResponse: boolean = yield validateResponse(response);
|
||||
if (!isValidResponse) return;
|
||||
|
||||
const libraries = response.data as Array<TJSLibrary & { defs: string }>;
|
||||
|
||||
const {
|
||||
message,
|
||||
success,
|
||||
}: { success: boolean; message: string } = yield call(
|
||||
EvalWorker.request,
|
||||
EVAL_WORKER_ACTIONS.LOAD_LIBRARIES,
|
||||
libraries.map((lib) => ({
|
||||
name: lib.name,
|
||||
version: lib.version,
|
||||
url: lib.url,
|
||||
accessor: lib.accessor,
|
||||
})),
|
||||
);
|
||||
|
||||
if (!success) {
|
||||
if (mode === APP_MODE.EDIT) {
|
||||
yield put({
|
||||
type: ReduxActionTypes.FETCH_JS_LIBRARIES_SUCCESS,
|
||||
payload: libraries.map((lib) => ({
|
||||
name: lib.name,
|
||||
accessor: lib.accessor,
|
||||
version: lib.version,
|
||||
url: lib.url,
|
||||
docsURL: lib.docsURL,
|
||||
})),
|
||||
});
|
||||
const errorMessage = parseErrorMessage(message);
|
||||
Toaster.show({
|
||||
text: errorMessage,
|
||||
variant: Variant.warning,
|
||||
});
|
||||
} else {
|
||||
yield put({
|
||||
type: ReduxActionErrorTypes.FETCH_JS_LIBRARIES_FAILED,
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (mode === APP_MODE.EDIT) {
|
||||
for (const lib of libraries) {
|
||||
try {
|
||||
const defs = JSON.parse(lib.defs);
|
||||
CodemirrorTernService.updateDef(defs["!name"], defs);
|
||||
} catch (e) {
|
||||
Toaster.show({
|
||||
text: createMessage(
|
||||
customJSLibraryMessages.AUTOCOMPLETE_FAILED,
|
||||
lib.name,
|
||||
),
|
||||
variant: Variant.info,
|
||||
});
|
||||
}
|
||||
}
|
||||
yield put({
|
||||
type: ReduxActionTypes.UPDATE_LINT_GLOBALS,
|
||||
payload: {
|
||||
libs: libraries,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
yield put({
|
||||
type: ReduxActionTypes.FETCH_JS_LIBRARIES_SUCCESS,
|
||||
payload: libraries.map((lib) => ({
|
||||
name: lib.name,
|
||||
accessor: lib.accessor,
|
||||
version: lib.version,
|
||||
url: lib.url,
|
||||
docsURL: lib.docsURL,
|
||||
})),
|
||||
});
|
||||
} catch (e) {
|
||||
yield put({
|
||||
type: ReduxActionErrorTypes.FETCH_JS_LIBRARIES_FAILED,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function* startInstallationRequestChannel() {
|
||||
const queueInstallChannel: ActionPattern<any> = yield actionChannel([
|
||||
ReduxActionTypes.INSTALL_LIBRARY_INIT,
|
||||
]);
|
||||
while (true) {
|
||||
const action: ReduxAction<Partial<TJSLibrary>> = yield take(
|
||||
queueInstallChannel,
|
||||
);
|
||||
yield put({
|
||||
type: ReduxActionTypes.INSTALL_LIBRARY_START,
|
||||
payload: action.payload.url,
|
||||
});
|
||||
yield call(installLibrarySaga, action.payload);
|
||||
}
|
||||
}
|
||||
|
||||
export default function*() {
|
||||
yield all([
|
||||
takeEvery(ReduxActionTypes.UNINSTALL_LIBRARY_INIT, uninstallLibrarySaga),
|
||||
takeLatest(ReduxActionTypes.FETCH_JS_LIBRARIES_INIT, fetchJSLibraries),
|
||||
call(startInstallationRequestChannel),
|
||||
]);
|
||||
}
|
||||
|
|
@ -1,8 +1,13 @@
|
|||
import { setLintingErrors } from "actions/lintingActions";
|
||||
import {
|
||||
ReduxAction,
|
||||
ReduxActionTypes,
|
||||
} from "@appsmith/constants/ReduxActionConstants";
|
||||
import { APP_MODE } from "entities/App";
|
||||
import { call, put, select } from "redux-saga/effects";
|
||||
import { call, put, select, takeEvery } from "redux-saga/effects";
|
||||
import { getAppMode } from "selectors/entitiesSelector";
|
||||
import { GracefulWorkerService } from "utils/WorkerUtil";
|
||||
import { TJSLibrary } from "workers/common/JSLibrary";
|
||||
import {
|
||||
LintTreeRequest,
|
||||
LintTreeResponse,
|
||||
|
|
@ -18,10 +23,19 @@ export const lintWorker = new GracefulWorkerService(
|
|||
}),
|
||||
);
|
||||
|
||||
export function* lintTreeSaga({
|
||||
pathsToLint,
|
||||
unevalTree,
|
||||
}: LintTreeSagaRequestData) {
|
||||
function* updateLintGlobals(action: ReduxAction<TJSLibrary>) {
|
||||
const appMode: APP_MODE = yield select(getAppMode);
|
||||
const isEditorMode = appMode === APP_MODE.EDIT;
|
||||
if (!isEditorMode) return;
|
||||
yield call(
|
||||
lintWorker.request,
|
||||
LINT_WORKER_ACTIONS.UPDATE_LINT_GLOBALS,
|
||||
action.payload,
|
||||
);
|
||||
}
|
||||
|
||||
export function* lintTreeSaga(action: ReduxAction<LintTreeSagaRequestData>) {
|
||||
const { pathsToLint, unevalTree } = action.payload;
|
||||
// only perform lint operations in edit mode
|
||||
const appMode: APP_MODE = yield select(getAppMode);
|
||||
if (appMode !== APP_MODE.EDIT) return;
|
||||
|
|
@ -40,3 +54,8 @@ export function* lintTreeSaga({
|
|||
yield put(setLintingErrors(errors));
|
||||
yield call(logLatestLintPropertyErrors, { errors, dataTree: unevalTree });
|
||||
}
|
||||
|
||||
export default function* lintTreeSagaWatcher() {
|
||||
yield takeEvery(ReduxActionTypes.UPDATE_LINT_GLOBALS, updateLintGlobals);
|
||||
yield takeEvery(ReduxActionTypes.LINT_TREE, lintTreeSaga);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -69,13 +69,7 @@ import {
|
|||
} from "utils/helpers";
|
||||
import { extractCurrentDSL } from "utils/WidgetPropsUtils";
|
||||
import { checkIfMigrationIsNeeded } from "utils/DSLMigrations";
|
||||
import {
|
||||
getAllPageIds,
|
||||
getEditorConfigs,
|
||||
getExistingPageNames,
|
||||
getWidgets,
|
||||
} from "./selectors";
|
||||
import { getDataTree } from "selectors/dataTreeSelectors";
|
||||
import { getAllPageIds, getEditorConfigs, getWidgets } from "./selectors";
|
||||
import { IncorrectBindingError, validateResponse } from "./ErrorSagas";
|
||||
import { ApiResponse } from "api/ApiResponses";
|
||||
import {
|
||||
|
|
@ -124,7 +118,6 @@ import {
|
|||
|
||||
import WidgetFactory from "utils/WidgetFactory";
|
||||
import { toggleShowDeviationDialog } from "actions/onboardingActions";
|
||||
import { DataTree } from "entities/DataTree/dataTreeFactory";
|
||||
import { builderURL } from "RouteBuilder";
|
||||
import { failFastApiCalls } from "./InitSagas";
|
||||
import { hasManagePagePermission } from "@appsmith/utils/permissionHelpers";
|
||||
|
|
@ -133,6 +126,7 @@ import { getSelectedWidgets } from "selectors/ui";
|
|||
import { checkAndLogErrorsIfCyclicDependency } from "./helper";
|
||||
import { LOCAL_STORAGE_KEYS } from "utils/localStorage";
|
||||
import { generateAutoHeightLayoutTreeAction } from "actions/autoHeightActions";
|
||||
import { getUsedActionNames } from "selectors/actionSelectors";
|
||||
import { getPageList } from "selectors/entitiesSelector";
|
||||
|
||||
const WidgetTypes = WidgetFactory.widgetTypes;
|
||||
|
|
@ -788,10 +782,10 @@ export function* updateWidgetNameSaga(
|
|||
try {
|
||||
const { widgetName } = yield select(getWidgetName, action.payload.id);
|
||||
const layoutId: string | undefined = yield select(getCurrentLayoutId);
|
||||
const evalTree: DataTree = yield select(getDataTree);
|
||||
const pageId: string | undefined = yield select(getCurrentPageId);
|
||||
const existingPageNames: Record<string, unknown> = yield select(
|
||||
getExistingPageNames,
|
||||
const getUsedNames: Record<string, true> = yield select(
|
||||
getUsedActionNames,
|
||||
"",
|
||||
);
|
||||
|
||||
// TODO(abhinav): Why do we need to jump through these hoops just to
|
||||
|
|
@ -868,12 +862,7 @@ export function* updateWidgetNameSaga(
|
|||
} else {
|
||||
// check if name is not conflicting with any
|
||||
// existing entity/api/queries/reserved words
|
||||
if (
|
||||
isNameValid(action.payload.newName, {
|
||||
...evalTree,
|
||||
...existingPageNames,
|
||||
})
|
||||
) {
|
||||
if (isNameValid(action.payload.newName, getUsedNames)) {
|
||||
const request: UpdateWidgetNameRequest = {
|
||||
newName: action.payload.newName,
|
||||
oldName: widgetName,
|
||||
|
|
|
|||
|
|
@ -2,8 +2,12 @@ import { DataTree } from "entities/DataTree/dataTreeFactory";
|
|||
import { createSelector } from "reselect";
|
||||
import WidgetFactory from "utils/WidgetFactory";
|
||||
import { FlattenedWidgetProps } from "widgets/constants";
|
||||
import { TJSLibrary } from "workers/common/JSLibrary";
|
||||
import { getDataTree } from "./dataTreeSelectors";
|
||||
import { getExistingPageNames } from "./entitiesSelector";
|
||||
import {
|
||||
getExistingPageNames,
|
||||
selectInstalledLibraries,
|
||||
} from "./entitiesSelector";
|
||||
import {
|
||||
getErrorForApiName,
|
||||
getErrorForJSObjectName,
|
||||
|
|
@ -19,10 +23,12 @@ export const getUsedActionNames = createSelector(
|
|||
getExistingPageNames,
|
||||
getDataTree,
|
||||
getParentWidget,
|
||||
selectInstalledLibraries,
|
||||
(
|
||||
pageNames: Record<string, any>,
|
||||
dataTree: DataTree,
|
||||
parentWidget: FlattenedWidgetProps | undefined,
|
||||
installedLibraries: TJSLibrary[],
|
||||
) => {
|
||||
const map: Record<string, boolean> = {};
|
||||
// The logic has been copied from Explorer/Entity/Name.tsx Component.
|
||||
|
|
@ -41,6 +47,12 @@ export const getUsedActionNames = createSelector(
|
|||
Object.keys(dataTree).forEach((treeItem: string) => {
|
||||
map[treeItem] = true;
|
||||
});
|
||||
const libAccessors = ([] as string[]).concat(
|
||||
...installedLibraries.map((lib) => lib.accessor),
|
||||
);
|
||||
for (const accessor of libAccessors) {
|
||||
map[accessor] = true;
|
||||
}
|
||||
}
|
||||
|
||||
return map;
|
||||
|
|
|
|||
|
|
@ -29,6 +29,9 @@ import {
|
|||
EVAL_ERROR_PATH,
|
||||
PropertyEvaluationErrorType,
|
||||
} from "utils/DynamicBindingUtils";
|
||||
import { InstallState } from "reducers/uiReducers/libraryReducer";
|
||||
import recommendedLibraries from "pages/Editor/Explorer/Libraries/recommendedLibraries";
|
||||
import { TJSLibrary } from "workers/common/JSLibrary";
|
||||
|
||||
export const getEntities = (state: AppState): AppState["entities"] =>
|
||||
state.entities;
|
||||
|
|
@ -848,3 +851,51 @@ export const getNumberOfEntitiesInCurrentPage = createSelector(
|
|||
);
|
||||
},
|
||||
);
|
||||
|
||||
export const selectIsInstallerOpen = (state: AppState) =>
|
||||
state.ui.libraries.isInstallerOpen;
|
||||
export const selectInstallationStatus = (state: AppState) =>
|
||||
state.ui.libraries.installationStatus;
|
||||
export const selectInstalledLibraries = (state: AppState) =>
|
||||
state.ui.libraries.installedLibraries;
|
||||
export const selectStatusForURL = (url: string) =>
|
||||
createSelector(selectInstallationStatus, (statusMap) => {
|
||||
return statusMap[url];
|
||||
});
|
||||
export const selectIsLibraryInstalled = createSelector(
|
||||
[selectInstalledLibraries, (_: AppState, url: string) => url],
|
||||
(installedLibraries, url) => {
|
||||
return !!installedLibraries.find((lib) => lib.url === url);
|
||||
},
|
||||
);
|
||||
|
||||
export const selectQueuedLibraries = createSelector(
|
||||
selectInstallationStatus,
|
||||
(statusMap) => {
|
||||
return Object.keys(statusMap).filter(
|
||||
(url) => statusMap[url] === InstallState.Queued,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
export const selectLibrariesForExplorer = createSelector(
|
||||
selectInstalledLibraries,
|
||||
selectInstallationStatus,
|
||||
(libs, libStatus) => {
|
||||
const queuedInstalls = Object.keys(libStatus)
|
||||
.filter((key) => libStatus[key] === InstallState.Queued)
|
||||
.map((url) => {
|
||||
const recommendedLibrary = recommendedLibraries.find(
|
||||
(lib) => lib.url === url,
|
||||
);
|
||||
return {
|
||||
name: recommendedLibrary?.name || url,
|
||||
docsURL: recommendedLibrary?.url || url,
|
||||
version: recommendedLibrary?.version || "",
|
||||
url: recommendedLibrary?.url || url,
|
||||
accessor: [],
|
||||
} as TJSLibrary;
|
||||
});
|
||||
return [...queuedInstalls, ...libs];
|
||||
},
|
||||
);
|
||||
|
|
|
|||
|
|
@ -275,7 +275,14 @@ export type EventName =
|
|||
| "BRANDING_PROPERTY_UPDATE"
|
||||
| "BRANDING_SUBMIT_CLICK"
|
||||
| "Cmd+Click Navigation"
|
||||
| "WIDGET_PROPERTY_SEARCH";
|
||||
| "WIDGET_PROPERTY_SEARCH"
|
||||
| LIBRARY_EVENTS;
|
||||
|
||||
export type LIBRARY_EVENTS =
|
||||
| "INSTALL_LIBRARY"
|
||||
| "DEFINITIONS_GENERATION"
|
||||
| "UNINSTALL_LIBRARY"
|
||||
| "EDIT_LIBRARY_URL";
|
||||
|
||||
export type AUDIT_LOGS_EVENT_NAMES =
|
||||
| "AUDIT_LOGS_CLEAR_FILTERS"
|
||||
|
|
|
|||
|
|
@ -1,17 +1,13 @@
|
|||
import _, { get, isString, VERSION as lodashVersion } from "lodash";
|
||||
import _, { get, isString } from "lodash";
|
||||
import { DATA_BIND_REGEX } from "constants/BindingsConstants";
|
||||
import { Action } from "entities/Action";
|
||||
import moment from "moment-timezone";
|
||||
import { WidgetProps } from "widgets/BaseWidget";
|
||||
import parser from "fast-xml-parser";
|
||||
|
||||
import { Severity } from "entities/AppsmithConsole";
|
||||
import {
|
||||
getEntityNameAndPropertyPath,
|
||||
isJSAction,
|
||||
isTrueObject,
|
||||
} from "workers/Evaluation/evaluationUtils";
|
||||
import forge from "node-forge";
|
||||
import { DataTreeEntity } from "entities/DataTree/dataTreeFactory";
|
||||
import { getType, Types } from "./TypeHelpers";
|
||||
import { ViewTypes } from "components/formControls/utils";
|
||||
|
|
@ -130,76 +126,6 @@ export type EvalError = {
|
|||
context?: Record<string, any>;
|
||||
};
|
||||
|
||||
export enum EVAL_WORKER_ACTIONS {
|
||||
SETUP = "SETUP",
|
||||
EVAL_TREE = "EVAL_TREE",
|
||||
EVAL_ACTION_BINDINGS = "EVAL_ACTION_BINDINGS",
|
||||
EVAL_TRIGGER = "EVAL_TRIGGER",
|
||||
PROCESS_TRIGGER = "PROCESS_TRIGGER",
|
||||
CLEAR_CACHE = "CLEAR_CACHE",
|
||||
VALIDATE_PROPERTY = "VALIDATE_PROPERTY",
|
||||
UNDO = "undo",
|
||||
REDO = "redo",
|
||||
EVAL_EXPRESSION = "EVAL_EXPRESSION",
|
||||
UPDATE_REPLAY_OBJECT = "UPDATE_REPLAY_OBJECT",
|
||||
SET_EVALUATION_VERSION = "SET_EVALUATION_VERSION",
|
||||
INIT_FORM_EVAL = "INIT_FORM_EVAL",
|
||||
EXECUTE_SYNC_JS = "EXECUTE_SYNC_JS",
|
||||
LINT_TREE = "LINT_TREE",
|
||||
}
|
||||
|
||||
export type ExtraLibrary = {
|
||||
version: string;
|
||||
docsURL: string;
|
||||
displayName: string;
|
||||
accessor: string;
|
||||
lib: any;
|
||||
};
|
||||
|
||||
export const extraLibraries: ExtraLibrary[] = [
|
||||
{
|
||||
accessor: "_",
|
||||
lib: _,
|
||||
version: lodashVersion,
|
||||
docsURL: `https://lodash.com/docs/${lodashVersion}`,
|
||||
displayName: "lodash",
|
||||
},
|
||||
{
|
||||
accessor: "moment",
|
||||
lib: moment,
|
||||
version: moment.version,
|
||||
docsURL: `https://momentjs.com/docs/`,
|
||||
displayName: "moment",
|
||||
},
|
||||
{
|
||||
accessor: "xmlParser",
|
||||
lib: parser,
|
||||
version: "3.17.5",
|
||||
docsURL: "https://github.com/NaturalIntelligence/fast-xml-parser",
|
||||
displayName: "xmlParser",
|
||||
},
|
||||
{
|
||||
accessor: "forge",
|
||||
// We are removing some functionalities of node-forge because they wont
|
||||
// work in the worker thread
|
||||
lib: _.omit(forge, ["tls", "http", "xhr", "socket", "task"]),
|
||||
version: "1.3.0",
|
||||
docsURL: "https://github.com/digitalbazaar/forge",
|
||||
displayName: "forge",
|
||||
},
|
||||
];
|
||||
/**
|
||||
* creates dynamic list of constants based on
|
||||
* current list of extra libraries i.e lodash("_"), moment etc
|
||||
* to be used in widget and entity name validations
|
||||
*/
|
||||
export const extraLibrariesNames = extraLibraries.reduce(
|
||||
(prev: Record<string, string>, curr) => {
|
||||
prev[curr.accessor] = curr.accessor;
|
||||
return prev;
|
||||
},
|
||||
{},
|
||||
);
|
||||
export interface DynamicPath {
|
||||
key: string;
|
||||
value?: string;
|
||||
|
|
@ -319,7 +245,6 @@ export const unsafeFunctionForEval = [
|
|||
"setInterval",
|
||||
"clearInterval",
|
||||
"setImmediate",
|
||||
"importScripts",
|
||||
"Navigator",
|
||||
];
|
||||
|
||||
|
|
|
|||
47
app/client/src/utils/MessageUtil.ts
Normal file
47
app/client/src/utils/MessageUtil.ts
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
/**
|
||||
* This file contains the utility function to send and receive messages from the worker.
|
||||
* TRequestMessage<TBody> is used to send a request to/from the worker.
|
||||
* TResponseMessage<TBody> is used to send a response to/from the worker.
|
||||
* TDefaultMessage<TBody> is used to send a message to/from worker. Does not expect a response.
|
||||
*/
|
||||
|
||||
export enum MessageType {
|
||||
REQUEST = "REQUEST",
|
||||
RESPONSE = "RESPONSE",
|
||||
DEFAULT = "DEFAULT",
|
||||
}
|
||||
|
||||
type TRequestMessage<TBody> = {
|
||||
body: TBody;
|
||||
messageId: string;
|
||||
messageType: MessageType.REQUEST;
|
||||
};
|
||||
|
||||
type TResponseMessage<TBody> = {
|
||||
body: TBody;
|
||||
messageId: string;
|
||||
messageType: MessageType.RESPONSE;
|
||||
};
|
||||
|
||||
type TDefaultMessage<TBody> = {
|
||||
body: TBody;
|
||||
messageType: MessageType.DEFAULT;
|
||||
};
|
||||
|
||||
export type TMessage<TBody> =
|
||||
| TRequestMessage<TBody>
|
||||
| TResponseMessage<TBody>
|
||||
| TDefaultMessage<TBody>;
|
||||
|
||||
/** Avoid from using postMessage directly.
|
||||
* This function should be used to send messages to the worker and back.
|
||||
* Purpose: To have some standardization in the messages that are transferred.
|
||||
* TODO: Add support for window postMessage options
|
||||
* TODO: Add support for transferable objects.
|
||||
*/
|
||||
export function sendMessage(
|
||||
this: Worker | typeof globalThis,
|
||||
message: TMessage<unknown>,
|
||||
) {
|
||||
this.postMessage(message);
|
||||
}
|
||||
|
|
@ -56,8 +56,9 @@ class MockWorkerClass implements WorkerClass {
|
|||
this.messages.push(message);
|
||||
const counter = setTimeout(() => {
|
||||
const response = {
|
||||
requestId: message.requestId,
|
||||
responseData: message.requestData,
|
||||
messageId: message.messageId,
|
||||
messageType: "RESPONSE",
|
||||
body: { data: message.body.data },
|
||||
};
|
||||
this.sendEvent({ data: response });
|
||||
this.responses.delete(counter);
|
||||
|
|
@ -218,65 +219,4 @@ describe("GracefulWorkerService", () => {
|
|||
await shutdown.toPromise();
|
||||
expect(await task.toPromise()).not.toEqual(message);
|
||||
});
|
||||
|
||||
test("duplex request starter", async () => {
|
||||
const MockWorker = new MockWorkerClass();
|
||||
const w = new GracefulWorkerService(MockWorker);
|
||||
await runSaga({}, w.start);
|
||||
// Need this to work with eslint
|
||||
if (MockWorker.instance === undefined) {
|
||||
expect(MockWorker.instance).toBeDefined();
|
||||
return;
|
||||
}
|
||||
const requestData = { message: "Hello" };
|
||||
const method = "duplex_test";
|
||||
MockWorker.instance.postMessage = jest.fn();
|
||||
const duplexRequest = await runSaga(
|
||||
{},
|
||||
w.duplexRequest,
|
||||
method,
|
||||
requestData,
|
||||
);
|
||||
const handlers = await duplexRequest.toPromise();
|
||||
expect(handlers).toHaveProperty("isFinishedChannel");
|
||||
expect(MockWorker.instance.postMessage).toBeCalledWith({
|
||||
method,
|
||||
requestData,
|
||||
requestId: expect.stringContaining(method),
|
||||
});
|
||||
});
|
||||
|
||||
test("duplex response channel handler", async () => {
|
||||
const MockWorker = new MockWorkerClass();
|
||||
const w = new GracefulWorkerService(MockWorker);
|
||||
await runSaga({}, w.start);
|
||||
|
||||
// Need this to work with eslint
|
||||
if (MockWorker.instance === undefined) {
|
||||
expect(MockWorker.instance).toBeDefined();
|
||||
return;
|
||||
}
|
||||
const mainThreadResponseChannel = channel();
|
||||
const workerRequestId = "testID";
|
||||
runSaga(
|
||||
{},
|
||||
// @ts-expect-error: type mismatch
|
||||
w.duplexResponseHandler,
|
||||
mainThreadResponseChannel,
|
||||
);
|
||||
MockWorker.instance.postMessage = jest.fn();
|
||||
|
||||
let randomRequestCount = Math.floor(Math.random() * 10);
|
||||
|
||||
for (randomRequestCount; randomRequestCount > 0; randomRequestCount--) {
|
||||
mainThreadResponseChannel.put({
|
||||
test: randomRequestCount,
|
||||
requestId: workerRequestId,
|
||||
});
|
||||
expect(MockWorker.instance.postMessage).toBeCalledWith({
|
||||
test: randomRequestCount,
|
||||
requestId: workerRequestId,
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
import { cancelled, delay, put, spawn, take } from "redux-saga/effects";
|
||||
import { cancelled, delay, put, take } from "redux-saga/effects";
|
||||
import { channel, Channel, buffers } from "redux-saga";
|
||||
import { uniqueId } from "lodash";
|
||||
import log from "loglevel";
|
||||
// import { executeDynamicTriggerRequest } from "sagas/EvaluationsSaga";
|
||||
import { TMessage, MessageType, sendMessage } from "./MessageUtil";
|
||||
|
||||
/**
|
||||
* Wrap a webworker to provide a synchronous request-response semantic.
|
||||
*
|
||||
|
|
@ -30,7 +31,6 @@ import log from "loglevel";
|
|||
* Note: The worker will hold ALL requests, even in case of restarts.
|
||||
* If we do not want that behaviour, we should create a new GracefulWorkerService.
|
||||
*/
|
||||
// TODO: Add a compatible listener layer on the worker to complete the framework.
|
||||
// TODO: Extract the worker wrapper into a library to be useful to anyone with WebWorkers + redux-saga.
|
||||
// TODO: Add support for timeouts on requests and shutdown.
|
||||
// TODO: Add a readiness + liveness probes.
|
||||
|
|
@ -50,24 +50,21 @@ export class GracefulWorkerService {
|
|||
|
||||
private readonly _workerClass: Worker;
|
||||
|
||||
public mainThreadRequestChannel: Channel<any>;
|
||||
public mainThreadResponseChannel: Channel<any>;
|
||||
private listenerChannel: Channel<TMessage<any>>;
|
||||
|
||||
constructor(workerClass: Worker) {
|
||||
this.shutdown = this.shutdown.bind(this);
|
||||
this.start = this.start.bind(this);
|
||||
this.request = this.request.bind(this);
|
||||
this._broker = this._broker.bind(this);
|
||||
this.duplexRequest = this.duplexRequest.bind(this);
|
||||
this.duplexResponseHandler = this.duplexResponseHandler.bind(this);
|
||||
this.request = this.request.bind(this);
|
||||
this.respond = this.respond.bind(this);
|
||||
|
||||
// Do not buffer messages on this channel
|
||||
this._readyChan = channel(buffers.none());
|
||||
this._isReady = false;
|
||||
this._channels = new Map<string, Channel<any>>();
|
||||
this._workerClass = workerClass;
|
||||
this.mainThreadRequestChannel = channel();
|
||||
this.mainThreadResponseChannel = channel();
|
||||
this.listenerChannel = channel();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -81,10 +78,7 @@ export class GracefulWorkerService {
|
|||
// Inform all pending requests that we're good to go!
|
||||
this._isReady = true;
|
||||
yield put(this._readyChan, true);
|
||||
yield spawn(this.duplexResponseHandler, this.mainThreadResponseChannel);
|
||||
return {
|
||||
mainThreadRequestChannel: this.mainThreadRequestChannel,
|
||||
};
|
||||
return this.listenerChannel;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -104,8 +98,7 @@ export class GracefulWorkerService {
|
|||
this._Worker.removeEventListener("message", this._broker);
|
||||
this._Worker.terminate();
|
||||
this._Worker = undefined;
|
||||
this.mainThreadRequestChannel.close();
|
||||
this.mainThreadResponseChannel.close();
|
||||
this.listenerChannel.close();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -120,6 +113,20 @@ export class GracefulWorkerService {
|
|||
return false;
|
||||
}
|
||||
|
||||
*respond(messageId = "", data = {}): any {
|
||||
if (!messageId) return;
|
||||
yield this.ready(true);
|
||||
if (!this._Worker) return;
|
||||
const messageType = MessageType.RESPONSE;
|
||||
sendMessage.call(this._Worker, {
|
||||
body: {
|
||||
data,
|
||||
},
|
||||
messageId,
|
||||
messageType,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a request to the worker for processing.
|
||||
* If the worker isn't ready, we wait for it to become ready.
|
||||
|
|
@ -129,7 +136,7 @@ export class GracefulWorkerService {
|
|||
*
|
||||
* @returns response from the worker
|
||||
*/
|
||||
*request(method: string, requestData = {}): any {
|
||||
*request(method: string, data = {}): any {
|
||||
yield this.ready(true);
|
||||
// Impossible case, but helps avoid `?` later in code and makes it clearer.
|
||||
if (!this._Worker) return;
|
||||
|
|
@ -137,22 +144,25 @@ export class GracefulWorkerService {
|
|||
/**
|
||||
* We create a unique channel to wait for a response of this specific request.
|
||||
*/
|
||||
const requestId = `${method}__${uniqueId()}`;
|
||||
const messageId = `${method}__${uniqueId()}`;
|
||||
const ch = channel();
|
||||
this._channels.set(requestId, ch);
|
||||
this._channels.set(messageId, ch);
|
||||
const mainThreadStartTime = performance.now();
|
||||
let timeTaken;
|
||||
|
||||
try {
|
||||
this._Worker.postMessage({
|
||||
sendMessage.call(this._Worker, {
|
||||
messageType: MessageType.REQUEST,
|
||||
body: {
|
||||
method,
|
||||
requestData,
|
||||
requestId,
|
||||
data,
|
||||
},
|
||||
messageId,
|
||||
});
|
||||
// The `this._broker` method is listening to events and will pass response to us over this channel.
|
||||
const response = yield take(ch);
|
||||
timeTaken = response.timeTaken;
|
||||
const { responseData } = response;
|
||||
const { data: responseData } = response;
|
||||
return responseData;
|
||||
} finally {
|
||||
// Log perf of main thread and worker
|
||||
|
|
@ -173,86 +183,23 @@ export class GracefulWorkerService {
|
|||
}
|
||||
// Cleanup
|
||||
ch.close();
|
||||
this._channels.delete(requestId);
|
||||
this._channels.delete(messageId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* When there needs to be a back and forth between both the threads,
|
||||
* you can use duplex request to avoid closing a channel
|
||||
* */
|
||||
*duplexRequest(method: string, requestData = {}): any {
|
||||
yield this.ready(false);
|
||||
// Impossible case, but helps avoid `?` later in code and makes it clearer.
|
||||
if (!this._Worker) return;
|
||||
|
||||
/**
|
||||
* We create a unique channel to wait for a response of this specific request.
|
||||
*/
|
||||
const workerRequestId = `${method}__${uniqueId()}`;
|
||||
// The worker channel is the main channel
|
||||
// where the web worker messages will get posted
|
||||
const isFinishedChannel = channel();
|
||||
this._channels.set(workerRequestId, isFinishedChannel);
|
||||
// And post the first message to the worker
|
||||
this._Worker.postMessage({
|
||||
method,
|
||||
requestData,
|
||||
requestId: workerRequestId,
|
||||
});
|
||||
|
||||
// Returning these channels to the main thread so that they can listen and post on it
|
||||
return {
|
||||
isFinishedChannel: isFinishedChannel,
|
||||
};
|
||||
}
|
||||
|
||||
*duplexResponseHandler(mainThreadResponseChannel: Channel<any>) {
|
||||
if (!this._Worker) return;
|
||||
try {
|
||||
const keepAlive = true;
|
||||
while (keepAlive) {
|
||||
// Wait for the main thread to respond back after a request
|
||||
const response: { finished: unknown; requestId: string } = yield take(
|
||||
mainThreadResponseChannel,
|
||||
);
|
||||
// send response to worker
|
||||
this._Worker.postMessage({
|
||||
...response,
|
||||
requestId: response.requestId,
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
log.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
private _broker(event: MessageEvent) {
|
||||
if (!event || !event.data) {
|
||||
return;
|
||||
}
|
||||
const { promisified, requestId, responseData, timeTaken } = event.data;
|
||||
const ch = this._channels.get(requestId);
|
||||
// Channel could have been deleted if the request gets cancelled before the WebWorker can respond.
|
||||
// In that case, we want to drop the request.
|
||||
if (promisified) {
|
||||
if (responseData.finished) {
|
||||
private _broker(event: MessageEvent<TMessage<any>>) {
|
||||
if (!event || !event.data) return;
|
||||
const { body, messageType } = event.data;
|
||||
if (messageType === MessageType.RESPONSE) {
|
||||
const { messageId } = event.data;
|
||||
if (!messageId) return;
|
||||
const ch = this._channels.get(messageId);
|
||||
if (ch) {
|
||||
ch.put({ requestData: responseData, timeTaken, requestId });
|
||||
this._channels.delete(requestId);
|
||||
ch.put(body);
|
||||
this._channels.delete(messageId);
|
||||
}
|
||||
} else {
|
||||
this.mainThreadRequestChannel.put({
|
||||
requestData: responseData,
|
||||
timeTaken,
|
||||
requestId,
|
||||
mainThreadResponseChannel: this.mainThreadResponseChannel,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
if (ch) {
|
||||
ch.put({ responseData, timeTaken, requestId });
|
||||
}
|
||||
this.listenerChannel.put(event.data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
/* eslint-disable @typescript-eslint/ban-ts-comment */
|
||||
// Heavily inspired from https://github.com/codemirror/CodeMirror/blob/master/addon/tern/tern.js
|
||||
import tern, { Server, Def } from "tern";
|
||||
import { Server, Def } from "tern";
|
||||
import ecma from "constants/defs/ecmascript.json";
|
||||
import lodash from "constants/defs/lodash.json";
|
||||
import base64 from "constants/defs/base64-js.json";
|
||||
|
|
@ -147,13 +147,10 @@ class CodeMirrorTernService {
|
|||
this.server = new TernWorkerServer(this);
|
||||
}
|
||||
|
||||
resetServer() {
|
||||
this.server = new tern.Server({
|
||||
async: true,
|
||||
defs: DEFS,
|
||||
});
|
||||
resetServer = () => {
|
||||
this.server = new TernWorkerServer(this);
|
||||
this.docs = Object.create(null);
|
||||
}
|
||||
};
|
||||
|
||||
complete(cm: CodeMirror.Editor) {
|
||||
cm.showHint({
|
||||
|
|
@ -247,7 +244,7 @@ class CodeMirrorTernService {
|
|||
const dataType = getDataType(completion.type);
|
||||
if (data.guess) className += " " + cls + "guess";
|
||||
let completionText = completion.name + after;
|
||||
if (dataType === "FUNCTION") {
|
||||
if (dataType === "FUNCTION" && !completion.origin?.startsWith("LIB/")) {
|
||||
completionText = completionText + "()";
|
||||
}
|
||||
const codeMirrorCompletion: Completion = {
|
||||
|
|
|
|||
|
|
@ -575,12 +575,9 @@ describe("isNameValid()", () => {
|
|||
it("works properly", () => {
|
||||
const invalidEntityNames = [
|
||||
"console",
|
||||
"moment",
|
||||
"Promise",
|
||||
"appsmith",
|
||||
"Math",
|
||||
"_",
|
||||
"forge",
|
||||
"yield",
|
||||
"Boolean",
|
||||
"ReferenceError",
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ import { get, set, isNil, has, uniq } from "lodash";
|
|||
import { Workspace } from "@appsmith/constants/workspaceConstants";
|
||||
import { hasCreateNewAppPermission } from "@appsmith/utils/permissionHelpers";
|
||||
import moment from "moment";
|
||||
import { extraLibrariesNames, isDynamicValue } from "./DynamicBindingUtils";
|
||||
import { isDynamicValue } from "./DynamicBindingUtils";
|
||||
import { ApiResponse } from "api/ApiResponses";
|
||||
import { DSLWidget } from "widgets/constants";
|
||||
import * as Sentry from "@sentry/react";
|
||||
|
|
@ -412,7 +412,6 @@ export const isNameValid = (
|
|||
has(DATA_TREE_KEYWORDS, name) ||
|
||||
has(DEDICATED_WORKER_GLOBAL_SCOPE_IDENTIFIERS, name) ||
|
||||
has(APPSMITH_GLOBAL_FUNCTIONS, name) ||
|
||||
has(extraLibrariesNames, name) ||
|
||||
has(invalidNames, name)
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -300,7 +300,6 @@ export const DATA_TREE_FUNCTIONS: Record<
|
|||
|
||||
export const enhanceDataTreeWithFunctions = (
|
||||
dataTree: Readonly<DataTree>,
|
||||
requestId = "",
|
||||
// Whether not to add functions like "run", "clear" to entity
|
||||
skipEntityFunctions = false,
|
||||
eventType?: EventType,
|
||||
|
|
@ -324,7 +323,6 @@ export const enhanceDataTreeWithFunctions = (
|
|||
pusher.bind(
|
||||
{
|
||||
TRIGGER_COLLECTOR: self.TRIGGER_COLLECTOR,
|
||||
REQUEST_ID: requestId,
|
||||
EVENT_TYPE: eventType,
|
||||
},
|
||||
func,
|
||||
|
|
@ -339,7 +337,6 @@ export const enhanceDataTreeWithFunctions = (
|
|||
pusher.bind(
|
||||
{
|
||||
TRIGGER_COLLECTOR: self.TRIGGER_COLLECTOR,
|
||||
REQUEST_ID: requestId,
|
||||
},
|
||||
funcOrFuncCreator,
|
||||
),
|
||||
|
|
@ -367,7 +364,6 @@ export const enhanceDataTreeWithFunctions = (
|
|||
export const pusher = function(
|
||||
this: {
|
||||
TRIGGER_COLLECTOR: ActionDescription[];
|
||||
REQUEST_ID: string;
|
||||
EVENT_TYPE?: EventType;
|
||||
},
|
||||
action: ActionDispatcherWithExecutionType,
|
||||
|
|
@ -383,6 +379,6 @@ export const pusher = function(
|
|||
if (executionType && executionType === ExecutionType.TRIGGER) {
|
||||
this.TRIGGER_COLLECTOR.push(actionPayload);
|
||||
} else {
|
||||
return promisifyAction(this.REQUEST_ID, actionPayload, this.EVENT_TYPE);
|
||||
return promisifyAction(actionPayload, this.EVENT_TYPE);
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { createGlobalData, EvalResult } from "workers/Evaluation/evaluate";
|
||||
import { createGlobalData } from "workers/Evaluation/evaluate";
|
||||
const ctx: Worker = self as any;
|
||||
|
||||
/*
|
||||
|
|
@ -8,17 +8,14 @@ const ctx: Worker = self as any;
|
|||
*
|
||||
* needs a REQUEST_ID to be passed in to know which request is going on right now
|
||||
*/
|
||||
import { EVAL_WORKER_ACTIONS } from "utils/DynamicBindingUtils";
|
||||
import {
|
||||
ActionDescription,
|
||||
ActionTriggerType,
|
||||
} from "entities/DataTree/actionTriggers";
|
||||
import { ActionDescription } from "entities/DataTree/actionTriggers";
|
||||
import _ from "lodash";
|
||||
import { EventType } from "constants/AppsmithActionConstants/ActionConstants";
|
||||
import { dataTreeEvaluator } from "workers/Evaluation/evaluation.worker";
|
||||
import { dataTreeEvaluator } from "./handlers/evalTree";
|
||||
import { TMessage, sendMessage, MessageType } from "utils/MessageUtil";
|
||||
import { MAIN_THREAD_ACTION } from "./evalWorkerActions";
|
||||
|
||||
export const promisifyAction = (
|
||||
workerRequestId: string,
|
||||
actionDescription: ActionDescription,
|
||||
eventType?: EventType,
|
||||
) => {
|
||||
|
|
@ -31,34 +28,32 @@ export const promisifyAction = (
|
|||
self.IS_ASYNC = true;
|
||||
throw new Error("Async function called in a sync field");
|
||||
}
|
||||
const workerRequestIdCopy = workerRequestId.concat("");
|
||||
return new Promise((resolve, reject) => {
|
||||
// We create a new sub request id for each request going on so that we can resolve the correct one later on
|
||||
const subRequestId = _.uniqueId(`${workerRequestIdCopy}_`);
|
||||
const messageId = _.uniqueId(`${actionDescription.type}_`);
|
||||
// send an execution request to the main thread
|
||||
const responseData = {
|
||||
const data = {
|
||||
trigger: actionDescription,
|
||||
errors: [],
|
||||
subRequestId,
|
||||
eventType,
|
||||
};
|
||||
ctx.postMessage({
|
||||
type: EVAL_WORKER_ACTIONS.PROCESS_TRIGGER,
|
||||
responseData,
|
||||
requestId: workerRequestIdCopy,
|
||||
promisified: true,
|
||||
sendMessage.call(ctx, {
|
||||
messageId,
|
||||
messageType: MessageType.REQUEST,
|
||||
body: {
|
||||
method: MAIN_THREAD_ACTION.PROCESS_TRIGGER,
|
||||
data,
|
||||
},
|
||||
});
|
||||
const processResponse = function(event: MessageEvent) {
|
||||
const { data, eventType, method, requestId, success } = event.data;
|
||||
const processResponse = function(event: MessageEvent<TMessage<any>>) {
|
||||
const { messageType } = event.data;
|
||||
if (messageType !== MessageType.RESPONSE) return;
|
||||
const { body, messageId: resMessageId } = event.data;
|
||||
const { data: messageData } = body;
|
||||
const { data, eventType, success } = messageData;
|
||||
// This listener will get all the messages that come to the worker
|
||||
// we need to find the correct one pertaining to this promise
|
||||
if (
|
||||
method === EVAL_WORKER_ACTIONS.PROCESS_TRIGGER &&
|
||||
requestId === workerRequestIdCopy &&
|
||||
subRequestId === event.data.data.subRequestId
|
||||
) {
|
||||
if (resMessageId === messageId && messageType === MessageType.RESPONSE) {
|
||||
// If we get a response for this same promise we will resolve or reject it
|
||||
|
||||
// We could not find a data tree evaluator,
|
||||
// maybe the page changed, or we have a cyclical dependency
|
||||
if (!dataTreeEvaluator) {
|
||||
|
|
@ -71,7 +66,6 @@ export const promisifyAction = (
|
|||
resolvedFunctions: dataTreeEvaluator.resolvedFunctions,
|
||||
isTriggerBased: true,
|
||||
context: {
|
||||
requestId: workerRequestId,
|
||||
eventType,
|
||||
},
|
||||
});
|
||||
|
|
@ -94,31 +88,3 @@ export const promisifyAction = (
|
|||
ctx.addEventListener("message", processResponse);
|
||||
});
|
||||
};
|
||||
// To indicate the main thread that the processing of the trigger is done
|
||||
// we send a finished message
|
||||
export const completePromise = (requestId: string, result: EvalResult) => {
|
||||
ctx.postMessage({
|
||||
type: EVAL_WORKER_ACTIONS.PROCESS_TRIGGER,
|
||||
responseData: {
|
||||
finished: true,
|
||||
result,
|
||||
},
|
||||
requestId,
|
||||
promisified: true,
|
||||
});
|
||||
};
|
||||
|
||||
export const confirmationPromise = function(
|
||||
requestId: string,
|
||||
func: any,
|
||||
name: string,
|
||||
...args: any[]
|
||||
) {
|
||||
const payload: ActionDescription = {
|
||||
type: ActionTriggerType.CONFIRMATION_MODAL,
|
||||
payload: {
|
||||
funName: name,
|
||||
},
|
||||
};
|
||||
return promisifyAction(requestId, payload).then(() => func(...args));
|
||||
};
|
||||
|
|
|
|||
16
app/client/src/workers/Evaluation/SetupDOM.ts
Normal file
16
app/client/src/workers/Evaluation/SetupDOM.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
//@ts-expect-error no types.
|
||||
import * as documentMock from "linkedom/worker";
|
||||
|
||||
export const DOM_APIS = Object.keys(documentMock).reduce((acc, key) => {
|
||||
acc[key] = true;
|
||||
return acc;
|
||||
}, {} as Record<string, true>);
|
||||
|
||||
export default function() {
|
||||
for (const [key, value] of Object.entries(documentMock)) {
|
||||
//@ts-expect-error no types
|
||||
self[key] = value;
|
||||
}
|
||||
const dom = documentMock.parseHTML(`<!DOCTYPE html><body></body>`);
|
||||
self.document = dom.window.document;
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import { createGlobalData } from "./evaluate";
|
||||
import { dataTreeEvaluator } from "./evaluation.worker";
|
||||
import { dataTreeEvaluator } from "./handlers/evalTree";
|
||||
|
||||
export const _internalSetTimeout = self.setTimeout;
|
||||
export const _internalClearTimeout = self.clearTimeout;
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@ import { LogObject, Methods, Severity } from "entities/AppsmithConsole";
|
|||
import { klona } from "klona/lite";
|
||||
import moment from "moment";
|
||||
import { TriggerMeta } from "sagas/ActionExecution/ActionExecutionSagas";
|
||||
import { sendMessage, MessageType } from "utils/MessageUtil";
|
||||
import { MAIN_THREAD_ACTION } from "./evalWorkerActions";
|
||||
import { _internalClearTimeout, _internalSetTimeout } from "./TimeoutOverride";
|
||||
|
||||
class UserLog {
|
||||
|
|
@ -11,13 +13,11 @@ class UserLog {
|
|||
private logs: LogObject[] = [];
|
||||
private flushLogTimerId: number | undefined;
|
||||
private requestInfo: {
|
||||
requestId?: string;
|
||||
eventType?: EventType;
|
||||
triggerMeta?: TriggerMeta;
|
||||
} | null = null;
|
||||
|
||||
public setCurrentRequestInfo(requestInfo: {
|
||||
requestId?: string;
|
||||
eventType?: EventType;
|
||||
triggerMeta?: TriggerMeta;
|
||||
}) {
|
||||
|
|
@ -28,14 +28,16 @@ class UserLog {
|
|||
if (this.flushLogTimerId) _internalClearTimeout(this.flushLogTimerId);
|
||||
this.flushLogTimerId = _internalSetTimeout(() => {
|
||||
const logs = this.flushLogs();
|
||||
self.postMessage({
|
||||
promisified: true,
|
||||
responseData: {
|
||||
sendMessage.call(self, {
|
||||
messageType: MessageType.DEFAULT,
|
||||
body: {
|
||||
data: {
|
||||
logs,
|
||||
eventType: this.requestInfo?.eventType,
|
||||
triggerMeta: this.requestInfo?.triggerMeta,
|
||||
},
|
||||
requestId: this.requestInfo?.requestId,
|
||||
method: MAIN_THREAD_ACTION.PROCESS_LOGS,
|
||||
},
|
||||
});
|
||||
}, this.flushLogsTimerDelay);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { DataTree, ENTITY_TYPE } from "entities/DataTree/dataTreeFactory";
|
|||
import { PluginType } from "entities/Action";
|
||||
import { createGlobalData } from "workers/Evaluation/evaluate";
|
||||
import uniqueId from "lodash/uniqueId";
|
||||
import { MessageType } from "utils/MessageUtil";
|
||||
jest.mock("lodash/uniqueId");
|
||||
|
||||
describe("Add functions", () => {
|
||||
|
|
@ -39,6 +40,12 @@ describe("Add functions", () => {
|
|||
},
|
||||
});
|
||||
|
||||
const messageCreator = (type: string, body: unknown) => ({
|
||||
messageId: expect.stringContaining(type),
|
||||
messageType: MessageType.REQUEST,
|
||||
body,
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
workerEventMock.mockReset();
|
||||
self.postMessage = workerEventMock;
|
||||
|
|
@ -119,13 +126,9 @@ describe("Add functions", () => {
|
|||
expect(
|
||||
dataTreeWithFunctions.action1.run(null, null, actionParams),
|
||||
).resolves.toBe({ a: "b" });
|
||||
expect(workerEventMock).lastCalledWith({
|
||||
type: "PROCESS_TRIGGER",
|
||||
requestId: "EVAL_TRIGGER",
|
||||
promisified: true,
|
||||
responseData: {
|
||||
errors: [],
|
||||
subRequestId: expect.stringContaining("EVAL_TRIGGER_"),
|
||||
expect(workerEventMock).lastCalledWith(
|
||||
messageCreator("RUN_PLUGIN_ACTION", {
|
||||
data: {
|
||||
trigger: {
|
||||
type: "RUN_PLUGIN_ACTION",
|
||||
payload: {
|
||||
|
|
@ -134,19 +137,17 @@ describe("Add functions", () => {
|
|||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
method: "PROCESS_TRIGGER",
|
||||
}),
|
||||
);
|
||||
|
||||
// Old syntax works with undefined values is treated as new syntax
|
||||
expect(
|
||||
dataTreeWithFunctions.action1.run(undefined, undefined, actionParams),
|
||||
).resolves.toBe({ a: "b" });
|
||||
expect(workerEventMock).lastCalledWith({
|
||||
type: "PROCESS_TRIGGER",
|
||||
requestId: "EVAL_TRIGGER",
|
||||
promisified: true,
|
||||
responseData: {
|
||||
errors: [],
|
||||
subRequestId: expect.stringContaining("EVAL_TRIGGER_"),
|
||||
expect(workerEventMock).lastCalledWith(
|
||||
messageCreator("RUN_PLUGIN_ACTION", {
|
||||
data: {
|
||||
trigger: {
|
||||
type: "RUN_PLUGIN_ACTION",
|
||||
payload: {
|
||||
|
|
@ -155,7 +156,9 @@ describe("Add functions", () => {
|
|||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
method: "PROCESS_TRIGGER",
|
||||
}),
|
||||
);
|
||||
|
||||
// new syntax works
|
||||
expect(
|
||||
|
|
@ -164,13 +167,9 @@ describe("Add functions", () => {
|
|||
.then(onSuccess)
|
||||
.catch(onError),
|
||||
).resolves.toBe({ a: "b" });
|
||||
expect(workerEventMock).lastCalledWith({
|
||||
type: "PROCESS_TRIGGER",
|
||||
requestId: "EVAL_TRIGGER",
|
||||
promisified: true,
|
||||
responseData: {
|
||||
errors: [],
|
||||
subRequestId: expect.stringContaining("EVAL_TRIGGER_"),
|
||||
expect(workerEventMock).lastCalledWith(
|
||||
messageCreator("RUN_PLUGIN_ACTION", {
|
||||
data: {
|
||||
trigger: {
|
||||
type: "RUN_PLUGIN_ACTION",
|
||||
payload: {
|
||||
|
|
@ -179,17 +178,15 @@ describe("Add functions", () => {
|
|||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
method: "PROCESS_TRIGGER",
|
||||
}),
|
||||
);
|
||||
// New syntax without params
|
||||
expect(dataTreeWithFunctions.action1.run()).resolves.toBe({ a: "b" });
|
||||
|
||||
expect(workerEventMock).lastCalledWith({
|
||||
type: "PROCESS_TRIGGER",
|
||||
requestId: "EVAL_TRIGGER",
|
||||
promisified: true,
|
||||
responseData: {
|
||||
errors: [],
|
||||
subRequestId: expect.stringContaining("EVAL_TRIGGER_"),
|
||||
expect(workerEventMock).lastCalledWith(
|
||||
messageCreator("RUN_PLUGIN_ACTION", {
|
||||
data: {
|
||||
trigger: {
|
||||
type: "RUN_PLUGIN_ACTION",
|
||||
payload: {
|
||||
|
|
@ -198,26 +195,27 @@ describe("Add functions", () => {
|
|||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
method: "PROCESS_TRIGGER",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("action.clear works", () => {
|
||||
expect(dataTreeWithFunctions.action1.clear()).resolves.toBe({});
|
||||
expect(workerEventMock).lastCalledWith({
|
||||
type: "PROCESS_TRIGGER",
|
||||
requestId: "EVAL_TRIGGER",
|
||||
promisified: true,
|
||||
responseData: {
|
||||
errors: [],
|
||||
subRequestId: expect.stringContaining("EVAL_TRIGGER_"),
|
||||
expect(workerEventMock).lastCalledWith(
|
||||
messageCreator("CLEAR_PLUGIN_ACTION", {
|
||||
data: {
|
||||
trigger: {
|
||||
type: "CLEAR_PLUGIN_ACTION",
|
||||
payload: {
|
||||
actionId: "123",
|
||||
},
|
||||
},
|
||||
eventType: undefined,
|
||||
},
|
||||
});
|
||||
method: "PROCESS_TRIGGER",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("navigateTo works", () => {
|
||||
|
|
@ -228,14 +226,9 @@ describe("Add functions", () => {
|
|||
expect(
|
||||
dataTreeWithFunctions.navigateTo(pageNameOrUrl, params, target),
|
||||
).resolves.toBe({});
|
||||
|
||||
expect(workerEventMock).lastCalledWith({
|
||||
type: "PROCESS_TRIGGER",
|
||||
requestId: "EVAL_TRIGGER",
|
||||
promisified: true,
|
||||
responseData: {
|
||||
errors: [],
|
||||
subRequestId: expect.stringContaining("EVAL_TRIGGER_"),
|
||||
expect(workerEventMock).lastCalledWith(
|
||||
messageCreator("NAVIGATE_TO", {
|
||||
data: {
|
||||
trigger: {
|
||||
type: "NAVIGATE_TO",
|
||||
payload: {
|
||||
|
|
@ -244,21 +237,20 @@ describe("Add functions", () => {
|
|||
target,
|
||||
},
|
||||
},
|
||||
eventType: undefined,
|
||||
},
|
||||
});
|
||||
method: "PROCESS_TRIGGER",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("showAlert works", () => {
|
||||
const message = "Alert message";
|
||||
const style = "info";
|
||||
expect(dataTreeWithFunctions.showAlert(message, style)).resolves.toBe({});
|
||||
expect(workerEventMock).lastCalledWith({
|
||||
type: "PROCESS_TRIGGER",
|
||||
requestId: "EVAL_TRIGGER",
|
||||
promisified: true,
|
||||
responseData: {
|
||||
errors: [],
|
||||
subRequestId: expect.stringContaining("EVAL_TRIGGER_"),
|
||||
expect(workerEventMock).lastCalledWith(
|
||||
messageCreator("SHOW_ALERT", {
|
||||
data: {
|
||||
trigger: {
|
||||
type: "SHOW_ALERT",
|
||||
payload: {
|
||||
|
|
@ -266,49 +258,50 @@ describe("Add functions", () => {
|
|||
style,
|
||||
},
|
||||
},
|
||||
eventType: undefined,
|
||||
},
|
||||
});
|
||||
method: "PROCESS_TRIGGER",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("showModal works", () => {
|
||||
const modalName = "Modal 1";
|
||||
|
||||
expect(dataTreeWithFunctions.showModal(modalName)).resolves.toBe({});
|
||||
expect(workerEventMock).lastCalledWith({
|
||||
type: "PROCESS_TRIGGER",
|
||||
requestId: "EVAL_TRIGGER",
|
||||
promisified: true,
|
||||
responseData: {
|
||||
errors: [],
|
||||
subRequestId: expect.stringContaining("EVAL_TRIGGER_"),
|
||||
expect(workerEventMock).lastCalledWith(
|
||||
messageCreator("SHOW_MODAL_BY_NAME", {
|
||||
data: {
|
||||
trigger: {
|
||||
type: "SHOW_MODAL_BY_NAME",
|
||||
payload: {
|
||||
modalName,
|
||||
},
|
||||
},
|
||||
eventType: undefined,
|
||||
},
|
||||
});
|
||||
method: "PROCESS_TRIGGER",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("closeModal works", () => {
|
||||
const modalName = "Modal 1";
|
||||
expect(dataTreeWithFunctions.closeModal(modalName)).resolves.toBe({});
|
||||
expect(workerEventMock).lastCalledWith({
|
||||
type: "PROCESS_TRIGGER",
|
||||
requestId: "EVAL_TRIGGER",
|
||||
promisified: true,
|
||||
responseData: {
|
||||
errors: [],
|
||||
subRequestId: expect.stringContaining("EVAL_TRIGGER_"),
|
||||
expect(workerEventMock).lastCalledWith(
|
||||
messageCreator("CLOSE_MODAL", {
|
||||
data: {
|
||||
trigger: {
|
||||
type: "CLOSE_MODAL",
|
||||
payload: {
|
||||
modalName,
|
||||
},
|
||||
},
|
||||
eventType: undefined,
|
||||
},
|
||||
});
|
||||
method: "PROCESS_TRIGGER",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("storeValue works", () => {
|
||||
|
|
@ -323,13 +316,9 @@ describe("Add functions", () => {
|
|||
expect(dataTreeWithFunctions.storeValue(key, value, persist)).resolves.toBe(
|
||||
{},
|
||||
);
|
||||
expect(workerEventMock).lastCalledWith({
|
||||
type: "PROCESS_TRIGGER",
|
||||
requestId: "EVAL_TRIGGER",
|
||||
promisified: true,
|
||||
responseData: {
|
||||
errors: [],
|
||||
subRequestId: expect.stringContaining("EVAL_TRIGGER_"),
|
||||
expect(workerEventMock).lastCalledWith(
|
||||
messageCreator("STORE_VALUE", {
|
||||
data: {
|
||||
trigger: {
|
||||
type: "STORE_VALUE",
|
||||
payload: {
|
||||
|
|
@ -339,46 +328,46 @@ describe("Add functions", () => {
|
|||
uniqueActionRequestId,
|
||||
},
|
||||
},
|
||||
eventType: undefined,
|
||||
},
|
||||
});
|
||||
method: "PROCESS_TRIGGER",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("removeValue works", () => {
|
||||
const key = "some";
|
||||
|
||||
expect(dataTreeWithFunctions.removeValue(key)).resolves.toBe({});
|
||||
expect(workerEventMock).lastCalledWith({
|
||||
type: "PROCESS_TRIGGER",
|
||||
requestId: "EVAL_TRIGGER",
|
||||
promisified: true,
|
||||
responseData: {
|
||||
errors: [],
|
||||
subRequestId: expect.stringContaining("EVAL_TRIGGER_"),
|
||||
expect(workerEventMock).lastCalledWith(
|
||||
messageCreator("REMOVE_VALUE", {
|
||||
data: {
|
||||
trigger: {
|
||||
type: "REMOVE_VALUE",
|
||||
payload: {
|
||||
key,
|
||||
},
|
||||
},
|
||||
eventType: undefined,
|
||||
},
|
||||
});
|
||||
method: "PROCESS_TRIGGER",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("clearStore works", () => {
|
||||
expect(dataTreeWithFunctions.clearStore()).resolves.toBe({});
|
||||
expect(workerEventMock).lastCalledWith({
|
||||
type: "PROCESS_TRIGGER",
|
||||
requestId: "EVAL_TRIGGER",
|
||||
promisified: true,
|
||||
responseData: {
|
||||
errors: [],
|
||||
subRequestId: expect.stringContaining("EVAL_TRIGGER_"),
|
||||
expect(workerEventMock).lastCalledWith(
|
||||
messageCreator("CLEAR_STORE", {
|
||||
data: {
|
||||
trigger: {
|
||||
type: "CLEAR_STORE",
|
||||
payload: null,
|
||||
},
|
||||
eventType: undefined,
|
||||
},
|
||||
});
|
||||
method: "PROCESS_TRIGGER",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("download works", () => {
|
||||
|
|
@ -387,13 +376,9 @@ describe("Add functions", () => {
|
|||
const type = "text";
|
||||
|
||||
expect(dataTreeWithFunctions.download(data, name, type)).resolves.toBe({});
|
||||
expect(workerEventMock).lastCalledWith({
|
||||
type: "PROCESS_TRIGGER",
|
||||
requestId: "EVAL_TRIGGER",
|
||||
promisified: true,
|
||||
responseData: {
|
||||
errors: [],
|
||||
subRequestId: expect.stringContaining("EVAL_TRIGGER_"),
|
||||
expect(workerEventMock).lastCalledWith(
|
||||
messageCreator("DOWNLOAD", {
|
||||
data: {
|
||||
trigger: {
|
||||
type: "DOWNLOAD",
|
||||
payload: {
|
||||
|
|
@ -402,20 +387,19 @@ describe("Add functions", () => {
|
|||
type,
|
||||
},
|
||||
},
|
||||
eventType: undefined,
|
||||
},
|
||||
});
|
||||
method: "PROCESS_TRIGGER",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("copyToClipboard works", () => {
|
||||
const data = "file";
|
||||
expect(dataTreeWithFunctions.copyToClipboard(data)).resolves.toBe({});
|
||||
expect(workerEventMock).lastCalledWith({
|
||||
type: "PROCESS_TRIGGER",
|
||||
requestId: "EVAL_TRIGGER",
|
||||
promisified: true,
|
||||
responseData: {
|
||||
errors: [],
|
||||
subRequestId: expect.stringContaining("EVAL_TRIGGER_"),
|
||||
expect(workerEventMock).lastCalledWith(
|
||||
messageCreator("COPY_TO_CLIPBOARD", {
|
||||
data: {
|
||||
trigger: {
|
||||
type: "COPY_TO_CLIPBOARD",
|
||||
payload: {
|
||||
|
|
@ -423,8 +407,11 @@ describe("Add functions", () => {
|
|||
options: { debug: undefined, format: undefined },
|
||||
},
|
||||
},
|
||||
eventType: undefined,
|
||||
},
|
||||
});
|
||||
method: "PROCESS_TRIGGER",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("resetWidget works", () => {
|
||||
|
|
@ -434,13 +421,9 @@ describe("Add functions", () => {
|
|||
expect(
|
||||
dataTreeWithFunctions.resetWidget(widgetName, resetChildren),
|
||||
).resolves.toBe({});
|
||||
expect(workerEventMock).lastCalledWith({
|
||||
type: "PROCESS_TRIGGER",
|
||||
requestId: "EVAL_TRIGGER",
|
||||
promisified: true,
|
||||
responseData: {
|
||||
errors: [],
|
||||
subRequestId: expect.stringContaining("EVAL_TRIGGER_"),
|
||||
expect(workerEventMock).lastCalledWith(
|
||||
messageCreator("RESET_WIDGET_META_RECURSIVE_BY_NAME", {
|
||||
data: {
|
||||
trigger: {
|
||||
type: "RESET_WIDGET_META_RECURSIVE_BY_NAME",
|
||||
payload: {
|
||||
|
|
@ -448,8 +431,11 @@ describe("Add functions", () => {
|
|||
resetChildren,
|
||||
},
|
||||
},
|
||||
eventType: undefined,
|
||||
},
|
||||
});
|
||||
method: "PROCESS_TRIGGER",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("setInterval works", () => {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { createGlobalData } from "workers/Evaluation/evaluate";
|
||||
import _ from "lodash";
|
||||
jest.mock("../evaluation.worker.ts", () => {
|
||||
import { MessageType } from "utils/MessageUtil";
|
||||
jest.mock("../handlers/evalTree", () => {
|
||||
return {
|
||||
dataTreeEvaluator: {
|
||||
evalTree: {},
|
||||
|
|
@ -19,6 +20,12 @@ describe("promise execution", () => {
|
|||
context: { requestId },
|
||||
});
|
||||
|
||||
const requestMessageCreator = (type: string, body: unknown) => ({
|
||||
messageId: expect.stringContaining(`${type}_`),
|
||||
messageType: MessageType.REQUEST,
|
||||
body,
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
self.ALLOW_ASYNC = true;
|
||||
self.postMessage = postMessageMock;
|
||||
|
|
@ -36,12 +43,9 @@ describe("promise execution", () => {
|
|||
});
|
||||
it("sends an event from the worker", () => {
|
||||
dataTreeWithFunctions.showAlert("test alert", "info");
|
||||
expect(postMessageMock).toBeCalledWith({
|
||||
requestId,
|
||||
type: "PROCESS_TRIGGER",
|
||||
promisified: true,
|
||||
responseData: expect.objectContaining({
|
||||
subRequestId: expect.stringContaining(`${requestId}_`),
|
||||
expect(postMessageMock).toBeCalledWith(
|
||||
requestMessageCreator("SHOW_ALERT", {
|
||||
data: {
|
||||
trigger: {
|
||||
type: "SHOW_ALERT",
|
||||
payload: {
|
||||
|
|
@ -49,8 +53,10 @@ describe("promise execution", () => {
|
|||
style: "info",
|
||||
},
|
||||
},
|
||||
},
|
||||
method: "PROCESS_TRIGGER",
|
||||
}),
|
||||
});
|
||||
);
|
||||
});
|
||||
it("returns a promise that resolves", async () => {
|
||||
postMessageMock.mockReset();
|
||||
|
|
@ -59,16 +65,24 @@ describe("promise execution", () => {
|
|||
"info",
|
||||
);
|
||||
const requestArgs = postMessageMock.mock.calls[0][0];
|
||||
const subRequestId = requestArgs.responseData.subRequestId;
|
||||
console.log(requestArgs);
|
||||
const messageId = requestArgs.messageId;
|
||||
|
||||
self.dispatchEvent(
|
||||
new MessageEvent("message", {
|
||||
data: {
|
||||
data: { resolve: ["123"], subRequestId },
|
||||
messageId,
|
||||
messageType: MessageType.RESPONSE,
|
||||
body: {
|
||||
data: {
|
||||
data: { resolve: ["123"] },
|
||||
method: "PROCESS_TRIGGER",
|
||||
requestId,
|
||||
success: true,
|
||||
},
|
||||
method: "PROCESS_TRIGGER",
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
|
|
@ -82,14 +96,15 @@ describe("promise execution", () => {
|
|||
"info",
|
||||
);
|
||||
const requestArgs = postMessageMock.mock.calls[0][0];
|
||||
const subRequestId = requestArgs.responseData.subRequestId;
|
||||
self.dispatchEvent(
|
||||
new MessageEvent("message", {
|
||||
data: {
|
||||
data: { reason: "testing", subRequestId },
|
||||
messageId: requestArgs.messageId,
|
||||
messageType: MessageType.RESPONSE,
|
||||
body: {
|
||||
data: { data: { reason: "testing" }, success: false },
|
||||
method: "PROCESS_TRIGGER",
|
||||
requestId,
|
||||
success: false,
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
|
@ -104,20 +119,22 @@ describe("promise execution", () => {
|
|||
);
|
||||
|
||||
const requestArgs = postMessageMock.mock.calls[0][0];
|
||||
const correctSubRequestId = requestArgs.responseData.subRequestId;
|
||||
const differentSubRequestId = "wrongRequestId";
|
||||
const correctId = requestArgs.messageId;
|
||||
|
||||
self.dispatchEvent(
|
||||
new MessageEvent("message", {
|
||||
data: {
|
||||
messageId: "wrongMessageId",
|
||||
messageType: MessageType.RESPONSE,
|
||||
body: {
|
||||
data: {
|
||||
data: {
|
||||
resolve: ["wrongRequest"],
|
||||
subRequestId: differentSubRequestId,
|
||||
},
|
||||
success: true,
|
||||
},
|
||||
method: "PROCESS_TRIGGER",
|
||||
requestId,
|
||||
success: true,
|
||||
promisified: true,
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
|
@ -125,11 +142,18 @@ describe("promise execution", () => {
|
|||
self.dispatchEvent(
|
||||
new MessageEvent("message", {
|
||||
data: {
|
||||
data: { resolve: ["testing"], subRequestId: correctSubRequestId },
|
||||
method: "PROCESS_TRIGGER",
|
||||
requestId,
|
||||
messageId: correctId,
|
||||
messageType: MessageType.RESPONSE,
|
||||
body: {
|
||||
data: {
|
||||
data: {
|
||||
resolve: ["testing"],
|
||||
},
|
||||
success: true,
|
||||
},
|
||||
method: "PROCESS_TRIGGER",
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
|
|
@ -143,19 +167,22 @@ describe("promise execution", () => {
|
|||
);
|
||||
|
||||
const requestArgs = postMessageMock.mock.calls[0][0];
|
||||
const subRequestId = requestArgs.responseData.subRequestId;
|
||||
const messageId = requestArgs.messageId;
|
||||
|
||||
self.dispatchEvent(
|
||||
new MessageEvent("message", {
|
||||
data: {
|
||||
messageId,
|
||||
messageType: MessageType.RESPONSE,
|
||||
body: {
|
||||
data: {
|
||||
data: {
|
||||
resolve: ["testing"],
|
||||
subRequestId,
|
||||
},
|
||||
success: true,
|
||||
},
|
||||
method: "PROCESS_TRIGGER",
|
||||
requestId,
|
||||
success: true,
|
||||
promisified: true,
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
|
@ -163,11 +190,12 @@ describe("promise execution", () => {
|
|||
self.dispatchEvent(
|
||||
new MessageEvent("message", {
|
||||
data: {
|
||||
data: { resolve: ["wrongRequest"], subRequestId },
|
||||
messageId,
|
||||
messageType: MessageType.RESPONSE,
|
||||
body: {
|
||||
data: { data: { resolve: ["wrongRequest"] }, success: true },
|
||||
method: "PROCESS_TRIGGER",
|
||||
requestId,
|
||||
success: false,
|
||||
promisified: true,
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import evaluate, {
|
||||
setupEvaluationEnvironment,
|
||||
evaluateAsync,
|
||||
isFunctionAsync,
|
||||
} from "workers/Evaluation/evaluate";
|
||||
|
|
@ -9,6 +8,7 @@ import {
|
|||
ENTITY_TYPE,
|
||||
} from "entities/DataTree/dataTreeFactory";
|
||||
import { RenderModes } from "constants/WidgetConstants";
|
||||
import setupEvalEnv from "../handlers/setupEvalEnv";
|
||||
|
||||
describe("evaluateSync", () => {
|
||||
const widget: DataTreeWidget = {
|
||||
|
|
@ -40,7 +40,7 @@ describe("evaluateSync", () => {
|
|||
Input1: widget,
|
||||
};
|
||||
beforeAll(() => {
|
||||
setupEvaluationEnvironment();
|
||||
setupEvalEnv();
|
||||
});
|
||||
it("unescapes string before evaluation", () => {
|
||||
const js = '\\"Hello!\\"';
|
||||
|
|
@ -192,33 +192,23 @@ describe("evaluateAsync", () => {
|
|||
it("runs and completes", async () => {
|
||||
const js = "(() => new Promise((resolve) => { resolve(123) }))()";
|
||||
self.postMessage = jest.fn();
|
||||
await evaluateAsync(js, {}, "TEST_REQUEST", {});
|
||||
expect(self.postMessage).toBeCalledWith({
|
||||
requestId: "TEST_REQUEST",
|
||||
promisified: true,
|
||||
responseData: {
|
||||
finished: true,
|
||||
result: { errors: [], logs: [], result: 123, triggers: [] },
|
||||
},
|
||||
type: "PROCESS_TRIGGER",
|
||||
const response = await evaluateAsync(js, {}, {}, {});
|
||||
expect(response).toStrictEqual({
|
||||
errors: [],
|
||||
logs: [],
|
||||
result: 123,
|
||||
triggers: [],
|
||||
});
|
||||
});
|
||||
it("runs and returns errors", async () => {
|
||||
jest.restoreAllMocks();
|
||||
const js = "(() => new Promise((resolve) => { randomKeyword }))()";
|
||||
self.postMessage = jest.fn();
|
||||
await evaluateAsync(js, {}, "TEST_REQUEST_1", {});
|
||||
expect(self.postMessage).toBeCalledWith({
|
||||
requestId: "TEST_REQUEST_1",
|
||||
promisified: true,
|
||||
responseData: {
|
||||
finished: true,
|
||||
result: {
|
||||
const result = await evaluateAsync(js, {}, {}, {});
|
||||
expect(result).toStrictEqual({
|
||||
errors: [
|
||||
{
|
||||
errorMessage: expect.stringContaining(
|
||||
"randomKeyword is not defined",
|
||||
),
|
||||
errorMessage: expect.stringContaining("randomKeyword is not defined"),
|
||||
errorType: "PARSE",
|
||||
originalBinding: expect.stringContaining("Promise"),
|
||||
raw: expect.stringContaining("Promise"),
|
||||
|
|
@ -228,9 +218,6 @@ describe("evaluateAsync", () => {
|
|||
triggers: [],
|
||||
result: undefined,
|
||||
logs: [],
|
||||
},
|
||||
},
|
||||
type: "PROCESS_TRIGGER",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import { PluginType } from "entities/Action";
|
||||
import { DataTree, ENTITY_TYPE } from "entities/DataTree/dataTreeFactory";
|
||||
import { createGlobalData } from "./evaluate";
|
||||
import "./TimeoutOverride";
|
||||
import overrideTimeout from "./TimeoutOverride";
|
||||
import { createGlobalData } from "../evaluate";
|
||||
import "../TimeoutOverride";
|
||||
import overrideTimeout from "../TimeoutOverride";
|
||||
|
||||
describe("Expects appsmith setTimeout to pass the following criteria", () => {
|
||||
overrideTimeout();
|
||||
33
app/client/src/workers/Evaluation/evalWorkerActions.ts
Normal file
33
app/client/src/workers/Evaluation/evalWorkerActions.ts
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
export enum EVAL_WORKER_SYNC_ACTION {
|
||||
SETUP = "SETUP",
|
||||
EVAL_TREE = "EVAL_TREE",
|
||||
EVAL_ACTION_BINDINGS = "EVAL_ACTION_BINDINGS",
|
||||
CLEAR_CACHE = "CLEAR_CACHE",
|
||||
VALIDATE_PROPERTY = "VALIDATE_PROPERTY",
|
||||
UNDO = "undo",
|
||||
REDO = "redo",
|
||||
UPDATE_REPLAY_OBJECT = "UPDATE_REPLAY_OBJECT",
|
||||
SET_EVALUATION_VERSION = "SET_EVALUATION_VERSION",
|
||||
INIT_FORM_EVAL = "INIT_FORM_EVAL",
|
||||
EXECUTE_SYNC_JS = "EXECUTE_SYNC_JS",
|
||||
INSTALL_LIBRARY = "INSTALL_LIBRARY",
|
||||
UNINSTALL_LIBRARY = "UNINSTALL_LIBRARY",
|
||||
LOAD_LIBRARIES = "LOAD_LIBRARIES",
|
||||
LINT_TREE = "LINT_TREE",
|
||||
}
|
||||
|
||||
export enum EVAL_WORKER_ASYNC_ACTION {
|
||||
EVAL_TRIGGER = "EVAL_TRIGGER",
|
||||
EVAL_EXPRESSION = "EVAL_EXPRESSION",
|
||||
}
|
||||
|
||||
export const EVAL_WORKER_ACTIONS = {
|
||||
...EVAL_WORKER_SYNC_ACTION,
|
||||
...EVAL_WORKER_ASYNC_ACTION,
|
||||
};
|
||||
|
||||
export const MAIN_THREAD_ACTION = {
|
||||
PROCESS_TRIGGER: "PROCESS_TRIGGER",
|
||||
PROCESS_LOGS: "PROCESS_LOGS",
|
||||
LINT_TREE: "LINT_TREE",
|
||||
};
|
||||
|
|
@ -2,22 +2,19 @@
|
|||
import { DataTree } from "entities/DataTree/dataTreeFactory";
|
||||
import {
|
||||
EvaluationError,
|
||||
extraLibraries,
|
||||
PropertyEvaluationErrorType,
|
||||
unsafeFunctionForEval,
|
||||
} from "utils/DynamicBindingUtils";
|
||||
import unescapeJS from "unescape-js";
|
||||
import { LogObject, Severity } from "entities/AppsmithConsole";
|
||||
import { enhanceDataTreeWithFunctions } from "./Actions";
|
||||
import { isEmpty } from "lodash";
|
||||
import { completePromise } from "workers/Evaluation/PromisifyAction";
|
||||
import { ActionDescription } from "entities/DataTree/actionTriggers";
|
||||
import userLogs from "./UserLog";
|
||||
import { EventType } from "constants/AppsmithActionConstants/ActionConstants";
|
||||
import overrideTimeout from "./TimeoutOverride";
|
||||
import { TriggerMeta } from "sagas/ActionExecution/ActionExecutionSagas";
|
||||
import interceptAndOverrideHttpRequest from "./HTTPRequestOverride";
|
||||
import indirectEval from "./indirectEval";
|
||||
import { DOM_APIS } from "./SetupDOM";
|
||||
import { JSLibraries, libraryReservedIdentifiers } from "../common/JSLibrary";
|
||||
|
||||
export type EvalResult = {
|
||||
result: any;
|
||||
|
|
@ -75,11 +72,19 @@ const topLevelWorkerAPIs = Object.keys(self).reduce((acc, key: string) => {
|
|||
|
||||
function resetWorkerGlobalScope() {
|
||||
for (const key of Object.keys(self)) {
|
||||
if (topLevelWorkerAPIs[key]) continue;
|
||||
if (key === "evaluationVersion") continue;
|
||||
if (extraLibraries.find((lib) => lib.accessor === key)) continue;
|
||||
if (topLevelWorkerAPIs[key] || DOM_APIS[key]) continue;
|
||||
//TODO: Remove this once we have a better way to handle this
|
||||
if (["evaluationVersion", "window", "document", "location"].includes(key))
|
||||
continue;
|
||||
if (JSLibraries.find((lib) => lib.accessor.includes(key))) continue;
|
||||
if (libraryReservedIdentifiers[key]) continue;
|
||||
try {
|
||||
// @ts-expect-error: Types are not available
|
||||
delete self[key];
|
||||
} catch (e) {
|
||||
// @ts-expect-error: Types are not available
|
||||
self[key] = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -98,6 +103,8 @@ export const getScriptType = (
|
|||
return scriptType;
|
||||
};
|
||||
|
||||
export const additionalLibrariesNames: string[] = [];
|
||||
|
||||
export const getScriptToEval = (
|
||||
userScript: string,
|
||||
type: EvaluationScriptType,
|
||||
|
|
@ -107,25 +114,7 @@ export const getScriptToEval = (
|
|||
return `${buffer[0]}${userScript}${buffer[1]}`;
|
||||
};
|
||||
|
||||
export function setupEvaluationEnvironment() {
|
||||
///// Adding extra libraries separately
|
||||
extraLibraries.forEach((library) => {
|
||||
// @ts-expect-error: Types are not available
|
||||
self[library.accessor] = library.lib;
|
||||
});
|
||||
|
||||
///// Remove all unsafe functions
|
||||
unsafeFunctionForEval.forEach((func) => {
|
||||
// @ts-expect-error: Types are not available
|
||||
self[func] = undefined;
|
||||
});
|
||||
userLogs.overrideConsoleAPI();
|
||||
overrideTimeout();
|
||||
interceptAndOverrideHttpRequest();
|
||||
}
|
||||
|
||||
const beginsWithLineBreakRegex = /^\s+|\s+$/;
|
||||
|
||||
export interface createGlobalDataArgs {
|
||||
dataTree: DataTree;
|
||||
resolvedFunctions: Record<string, any>;
|
||||
|
|
@ -165,7 +154,6 @@ export const createGlobalData = (args: createGlobalDataArgs) => {
|
|||
//// Add internal functions to dataTree;
|
||||
const dataTreeWithFunctions = enhanceDataTreeWithFunctions(
|
||||
dataTree,
|
||||
context?.requestId,
|
||||
skipEntityFunctions,
|
||||
context?.eventType,
|
||||
);
|
||||
|
|
@ -322,7 +310,6 @@ export default function evaluateSync(
|
|||
export async function evaluateAsync(
|
||||
userScript: string,
|
||||
dataTree: DataTree,
|
||||
requestId: string,
|
||||
resolvedFunctions: Record<string, any>,
|
||||
context?: EvaluateContext,
|
||||
evalArguments?: Array<any>,
|
||||
|
|
@ -335,7 +322,6 @@ export async function evaluateAsync(
|
|||
/**** Setting the eval context ****/
|
||||
userLogs.resetLogs();
|
||||
userLogs.setCurrentRequestInfo({
|
||||
requestId,
|
||||
eventType: context?.eventType,
|
||||
triggerMeta: context?.triggerMeta,
|
||||
});
|
||||
|
|
@ -343,7 +329,7 @@ export async function evaluateAsync(
|
|||
dataTree,
|
||||
resolvedFunctions,
|
||||
isTriggerBased: true,
|
||||
context: { ...context, requestId },
|
||||
context,
|
||||
evalArguments,
|
||||
});
|
||||
const { script } = getUserScriptToEvaluate(userScript, true, evalArguments);
|
||||
|
|
@ -375,21 +361,12 @@ export async function evaluateAsync(
|
|||
// Adding this extra try catch because there are cases when logs have child objects
|
||||
// like functions or promises that cause issue in complete promise action, thus
|
||||
// leading the app into a bad state.
|
||||
try {
|
||||
completePromise(requestId, {
|
||||
return {
|
||||
result,
|
||||
errors,
|
||||
logs,
|
||||
triggers: Array.from(self.TRIGGER_COLLECTOR),
|
||||
});
|
||||
} catch (error) {
|
||||
completePromise(requestId, {
|
||||
result,
|
||||
errors,
|
||||
logs: [userLogs.parseLogs("log", ["failed to parse logs"])],
|
||||
triggers: Array.from(self.TRIGGER_COLLECTOR),
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
})();
|
||||
}
|
||||
|
|
@ -419,21 +396,7 @@ export function isFunctionAsync(
|
|||
const dataTreeKey = GLOBAL_DATA[datum];
|
||||
if (dataTreeKey) {
|
||||
const data = dataTreeKey[key]?.data;
|
||||
//do not remove, we will be investigating this
|
||||
// const isAsync = dataTreeKey.meta[key]?.isAsync || false;
|
||||
// const confirmBeforeExecute =
|
||||
// dataTreeKey.meta[key]?.confirmBeforeExecute || false;
|
||||
dataTreeKey[key] = resolvedObject[key];
|
||||
// if (isAsync && confirmBeforeExecute) {
|
||||
// dataTreeKey[key] = confirmationPromise.bind(
|
||||
// {},
|
||||
// "",
|
||||
// resolvedObject[key],
|
||||
// key,
|
||||
// );
|
||||
// } else {
|
||||
// dataTreeKey[key] = resolvedObject[key];
|
||||
// }
|
||||
if (!!data) {
|
||||
dataTreeKey[key].data = data;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,427 +1,72 @@
|
|||
// Workers do not have access to log.error
|
||||
/* eslint-disable no-console */
|
||||
import { DataTree } from "entities/DataTree/dataTreeFactory";
|
||||
import {
|
||||
DependencyMap,
|
||||
EVAL_WORKER_ACTIONS,
|
||||
EvalError,
|
||||
EvalErrorTypes,
|
||||
} from "utils/DynamicBindingUtils";
|
||||
import {
|
||||
CrashingError,
|
||||
DataTreeDiff,
|
||||
getSafeToRenderDataTree,
|
||||
removeFunctions,
|
||||
} from "./evaluationUtils";
|
||||
import DataTreeEvaluator from "workers/common/DataTreeEvaluator";
|
||||
import ReplayEntity from "entities/Replay";
|
||||
import ReplayCanvas from "entities/Replay/ReplayEntity/ReplayCanvas";
|
||||
import ReplayEditor from "entities/Replay/ReplayEntity/ReplayEditor";
|
||||
import { isEmpty } from "lodash";
|
||||
import { UserLogObject } from "entities/AppsmithConsole";
|
||||
import { WorkerErrorTypes } from "workers/common/types";
|
||||
import {
|
||||
EvalTreeRequestData,
|
||||
EvalTreeResponseData,
|
||||
EvalWorkerRequest,
|
||||
EvalWorkerResponse,
|
||||
} from "./types";
|
||||
import { EvalMetaUpdates } from "workers/common/DataTreeEvaluator/types";
|
||||
import { setFormEvaluationSaga } from "workers/Evaluation/formEval";
|
||||
import evaluate, {
|
||||
evaluateAsync,
|
||||
setupEvaluationEnvironment,
|
||||
} from "./evaluate";
|
||||
import { JSUpdate } from "utils/JSPaneUtils";
|
||||
import { validateWidgetProperty } from "workers/common/DataTreeEvaluator/validationUtils";
|
||||
import { initiateLinting } from "workers/Linting/utils";
|
||||
import {
|
||||
createUnEvalTreeForEval,
|
||||
makeEntityConfigsAsObjProperties,
|
||||
} from "./dataTreeUtils";
|
||||
|
||||
const CANVAS = "canvas";
|
||||
|
||||
export let dataTreeEvaluator: DataTreeEvaluator | undefined;
|
||||
|
||||
let replayMap: Record<string, ReplayEntity<any>>;
|
||||
import { EvalWorkerASyncRequest, EvalWorkerSyncRequest } from "./types";
|
||||
import { syncHandlerMap, asyncHandlerMap } from "./handlers";
|
||||
import { TMessage, sendMessage, MessageType } from "utils/MessageUtil";
|
||||
|
||||
//TODO: Create a more complete RPC setup in the subtree-eval branch.
|
||||
function messageEventListener(fn: typeof eventRequestHandler) {
|
||||
return (e: MessageEvent<EvalWorkerRequest>) => {
|
||||
function syncRequestMessageListener(
|
||||
e: MessageEvent<TMessage<EvalWorkerSyncRequest>>,
|
||||
) {
|
||||
const { messageType } = e.data;
|
||||
if (messageType !== MessageType.REQUEST) return;
|
||||
const startTime = performance.now();
|
||||
const { method, requestData, requestId } = e.data;
|
||||
if (method) {
|
||||
const responseData = fn({ method, requestData, requestId });
|
||||
if (responseData) {
|
||||
const { body, messageId } = e.data;
|
||||
const { method } = body;
|
||||
if (!method) return;
|
||||
const messageHandler = syncHandlerMap[method];
|
||||
if (typeof messageHandler !== "function") return;
|
||||
const responseData = messageHandler(body);
|
||||
if (!responseData) return;
|
||||
const endTime = performance.now();
|
||||
respond(messageId, responseData, endTime - startTime);
|
||||
}
|
||||
|
||||
async function asyncRequestMessageListener(
|
||||
e: MessageEvent<TMessage<EvalWorkerASyncRequest>>,
|
||||
) {
|
||||
const { messageType } = e.data;
|
||||
if (messageType !== MessageType.REQUEST) return;
|
||||
const start = performance.now();
|
||||
const { body, messageId } = e.data;
|
||||
const { method } = body;
|
||||
if (!method) return;
|
||||
const messageHandler = asyncHandlerMap[method];
|
||||
if (typeof messageHandler !== "function") return;
|
||||
const data = await messageHandler(body);
|
||||
if (!data) return;
|
||||
const end = performance.now();
|
||||
respond(messageId, data, end - start);
|
||||
}
|
||||
|
||||
function respond(messageId: string, data: unknown, timeTaken: number) {
|
||||
try {
|
||||
self.postMessage({
|
||||
requestId,
|
||||
responseData,
|
||||
timeTaken: (endTime - startTime).toFixed(2),
|
||||
sendMessage.call(self, {
|
||||
messageId,
|
||||
messageType: MessageType.RESPONSE,
|
||||
body: { data, timeTaken },
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
// we dont want to log dataTree because it is huge.
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { dataTree, ...rest } = requestData;
|
||||
self.postMessage({
|
||||
requestId,
|
||||
responseData: {
|
||||
sendMessage.call(self, {
|
||||
messageId,
|
||||
messageType: MessageType.RESPONSE,
|
||||
body: {
|
||||
timeTaken: timeTaken.toFixed(2),
|
||||
data: {
|
||||
errors: [
|
||||
{
|
||||
type: WorkerErrorTypes.CLONE_ERROR,
|
||||
message: (e as Error)?.message,
|
||||
context: JSON.stringify(rest),
|
||||
context: JSON.stringify(data),
|
||||
},
|
||||
],
|
||||
},
|
||||
timeTaken: (endTime - startTime).toFixed(2),
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function eventRequestHandler({
|
||||
method,
|
||||
requestData,
|
||||
requestId,
|
||||
}: EvalWorkerRequest): EvalWorkerResponse {
|
||||
switch (method) {
|
||||
case EVAL_WORKER_ACTIONS.SETUP: {
|
||||
setupEvaluationEnvironment();
|
||||
return true;
|
||||
}
|
||||
case EVAL_WORKER_ACTIONS.EVAL_ACTION_BINDINGS: {
|
||||
const { bindings, executionParams } = requestData;
|
||||
if (!dataTreeEvaluator) {
|
||||
return { values: undefined, errors: [] };
|
||||
}
|
||||
|
||||
const values = dataTreeEvaluator.evaluateActionBindings(
|
||||
bindings,
|
||||
executionParams,
|
||||
);
|
||||
|
||||
const cleanValues = removeFunctions(values);
|
||||
|
||||
const errors = dataTreeEvaluator.errors;
|
||||
dataTreeEvaluator.clearErrors();
|
||||
return { values: cleanValues, errors };
|
||||
}
|
||||
case EVAL_WORKER_ACTIONS.EVAL_TRIGGER: {
|
||||
const {
|
||||
callbackData,
|
||||
dynamicTrigger,
|
||||
eventType,
|
||||
globalContext,
|
||||
triggerMeta,
|
||||
unEvalTree: __unEvalTree__,
|
||||
} = requestData;
|
||||
if (!dataTreeEvaluator) {
|
||||
return { triggers: [], errors: [] };
|
||||
}
|
||||
|
||||
const unEvalTree = createUnEvalTreeForEval(__unEvalTree__);
|
||||
|
||||
const {
|
||||
evalOrder,
|
||||
nonDynamicFieldValidationOrder,
|
||||
} = dataTreeEvaluator.setupUpdateTree(unEvalTree);
|
||||
dataTreeEvaluator.evalAndValidateSubTree(
|
||||
evalOrder,
|
||||
nonDynamicFieldValidationOrder,
|
||||
);
|
||||
const evalTree = dataTreeEvaluator.evalTree;
|
||||
const resolvedFunctions = dataTreeEvaluator.resolvedFunctions;
|
||||
|
||||
dataTreeEvaluator.evaluateTriggers(
|
||||
dynamicTrigger,
|
||||
evalTree,
|
||||
requestId,
|
||||
resolvedFunctions,
|
||||
callbackData,
|
||||
{
|
||||
globalContext,
|
||||
eventType,
|
||||
triggerMeta,
|
||||
},
|
||||
);
|
||||
|
||||
break;
|
||||
}
|
||||
case EVAL_WORKER_ACTIONS.PROCESS_TRIGGER:
|
||||
case EVAL_WORKER_ACTIONS.LINT_TREE:
|
||||
/**
|
||||
* These actions will not be processed here. They will be handled in the eval trigger sub steps
|
||||
* @link promisifyAction
|
||||
**/
|
||||
break;
|
||||
case EVAL_WORKER_ACTIONS.CLEAR_CACHE: {
|
||||
dataTreeEvaluator = undefined;
|
||||
return true;
|
||||
}
|
||||
case EVAL_WORKER_ACTIONS.VALIDATE_PROPERTY: {
|
||||
const { property, props, validation, value } = requestData;
|
||||
return removeFunctions(
|
||||
validateWidgetProperty(validation, value, props, property),
|
||||
);
|
||||
}
|
||||
case EVAL_WORKER_ACTIONS.UNDO: {
|
||||
const { entityId } = requestData;
|
||||
if (!replayMap[entityId || CANVAS]) return;
|
||||
const replayResult = replayMap[entityId || CANVAS].replay("UNDO");
|
||||
replayMap[entityId || CANVAS].clearLogs();
|
||||
return replayResult;
|
||||
}
|
||||
case EVAL_WORKER_ACTIONS.REDO: {
|
||||
const { entityId } = requestData;
|
||||
if (!replayMap[entityId ?? CANVAS]) return;
|
||||
const replayResult = replayMap[entityId ?? CANVAS].replay("REDO");
|
||||
replayMap[entityId ?? CANVAS].clearLogs();
|
||||
return replayResult;
|
||||
}
|
||||
case EVAL_WORKER_ACTIONS.EXECUTE_SYNC_JS: {
|
||||
const { functionCall } = requestData;
|
||||
|
||||
if (!dataTreeEvaluator) {
|
||||
return true;
|
||||
}
|
||||
const evalTree = dataTreeEvaluator.evalTree;
|
||||
const resolvedFunctions = dataTreeEvaluator.resolvedFunctions;
|
||||
const { errors, logs, result } = evaluate(
|
||||
functionCall,
|
||||
evalTree,
|
||||
resolvedFunctions,
|
||||
false,
|
||||
undefined,
|
||||
);
|
||||
return { errors, logs, result };
|
||||
}
|
||||
case EVAL_WORKER_ACTIONS.EVAL_EXPRESSION:
|
||||
const { expression, isTrigger } = requestData;
|
||||
const evalTree = dataTreeEvaluator?.evalTree;
|
||||
if (!evalTree) return {};
|
||||
// TODO find a way to do this for snippets
|
||||
return isTrigger
|
||||
? evaluateAsync(expression, evalTree, "SNIPPET", {})
|
||||
: evaluate(expression, evalTree, {}, false);
|
||||
case EVAL_WORKER_ACTIONS.UPDATE_REPLAY_OBJECT:
|
||||
const { entity, entityId, entityType } = requestData;
|
||||
const replayObject = replayMap[entityId];
|
||||
if (replayObject) {
|
||||
replayObject.update(entity);
|
||||
} else {
|
||||
replayMap[entityId] = new ReplayEditor(entity, entityType);
|
||||
}
|
||||
break;
|
||||
case EVAL_WORKER_ACTIONS.SET_EVALUATION_VERSION:
|
||||
const { version } = requestData;
|
||||
self.evaluationVersion = version || 1;
|
||||
break;
|
||||
case EVAL_WORKER_ACTIONS.INIT_FORM_EVAL:
|
||||
const { currentEvalState, payload, type } = requestData;
|
||||
const response = setFormEvaluationSaga(type, payload, currentEvalState);
|
||||
return response;
|
||||
case EVAL_WORKER_ACTIONS.EVAL_TREE: {
|
||||
let evalOrder: string[] = [];
|
||||
let lintOrder: string[] = [];
|
||||
let jsUpdates: Record<string, JSUpdate> = {};
|
||||
let unEvalUpdates: DataTreeDiff[] = [];
|
||||
let nonDynamicFieldValidationOrder: string[] = [];
|
||||
let isCreateFirstTree = false;
|
||||
let dataTree: DataTree = {};
|
||||
let errors: EvalError[] = [];
|
||||
let logs: any[] = [];
|
||||
let userLogs: UserLogObject[] = [];
|
||||
let dependencies: DependencyMap = {};
|
||||
let evalMetaUpdates: EvalMetaUpdates = [];
|
||||
|
||||
const {
|
||||
allActionValidationConfig,
|
||||
requiresLinting,
|
||||
shouldReplay,
|
||||
theme,
|
||||
unevalTree: __unevalTree__,
|
||||
widgets,
|
||||
widgetTypeConfigMap,
|
||||
} = requestData as EvalTreeRequestData;
|
||||
|
||||
const unevalTree = createUnEvalTreeForEval(__unevalTree__);
|
||||
|
||||
try {
|
||||
if (!dataTreeEvaluator) {
|
||||
isCreateFirstTree = true;
|
||||
replayMap = replayMap || {};
|
||||
replayMap[CANVAS] = new ReplayCanvas({ widgets, theme });
|
||||
dataTreeEvaluator = new DataTreeEvaluator(
|
||||
widgetTypeConfigMap,
|
||||
allActionValidationConfig,
|
||||
);
|
||||
|
||||
const setupFirstTreeResponse = dataTreeEvaluator.setupFirstTree(
|
||||
unevalTree,
|
||||
);
|
||||
evalOrder = setupFirstTreeResponse.evalOrder;
|
||||
lintOrder = setupFirstTreeResponse.lintOrder;
|
||||
jsUpdates = setupFirstTreeResponse.jsUpdates;
|
||||
|
||||
initiateLinting(
|
||||
lintOrder,
|
||||
makeEntityConfigsAsObjProperties(dataTreeEvaluator.oldUnEvalTree, {
|
||||
sanitizeDataTree: false,
|
||||
}),
|
||||
requiresLinting,
|
||||
);
|
||||
|
||||
const dataTreeResponse = dataTreeEvaluator.evalAndValidateFirstTree();
|
||||
dataTree = makeEntityConfigsAsObjProperties(
|
||||
dataTreeResponse.evalTree,
|
||||
{
|
||||
evalProps: dataTreeEvaluator.evalProps,
|
||||
},
|
||||
);
|
||||
} else if (dataTreeEvaluator.hasCyclicalDependency) {
|
||||
if (dataTreeEvaluator && !isEmpty(allActionValidationConfig)) {
|
||||
//allActionValidationConfigs may not be set in dataTreeEvaluatior. Therefore, set it explicitly via setter method
|
||||
dataTreeEvaluator.setAllActionValidationConfig(
|
||||
allActionValidationConfig,
|
||||
);
|
||||
}
|
||||
if (shouldReplay) {
|
||||
replayMap[CANVAS]?.update({ widgets, theme });
|
||||
}
|
||||
dataTreeEvaluator = new DataTreeEvaluator(
|
||||
widgetTypeConfigMap,
|
||||
allActionValidationConfig,
|
||||
);
|
||||
if (dataTreeEvaluator && !isEmpty(allActionValidationConfig)) {
|
||||
dataTreeEvaluator.setAllActionValidationConfig(
|
||||
allActionValidationConfig,
|
||||
);
|
||||
}
|
||||
const setupFirstTreeResponse = dataTreeEvaluator.setupFirstTree(
|
||||
unevalTree,
|
||||
);
|
||||
isCreateFirstTree = true;
|
||||
evalOrder = setupFirstTreeResponse.evalOrder;
|
||||
lintOrder = setupFirstTreeResponse.lintOrder;
|
||||
jsUpdates = setupFirstTreeResponse.jsUpdates;
|
||||
|
||||
initiateLinting(
|
||||
lintOrder,
|
||||
makeEntityConfigsAsObjProperties(dataTreeEvaluator.oldUnEvalTree, {
|
||||
sanitizeDataTree: false,
|
||||
}),
|
||||
requiresLinting,
|
||||
);
|
||||
|
||||
const dataTreeResponse = dataTreeEvaluator.evalAndValidateFirstTree();
|
||||
dataTree = makeEntityConfigsAsObjProperties(
|
||||
dataTreeResponse.evalTree,
|
||||
{
|
||||
evalProps: dataTreeEvaluator.evalProps,
|
||||
},
|
||||
);
|
||||
} else {
|
||||
if (dataTreeEvaluator && !isEmpty(allActionValidationConfig)) {
|
||||
dataTreeEvaluator.setAllActionValidationConfig(
|
||||
allActionValidationConfig,
|
||||
);
|
||||
}
|
||||
isCreateFirstTree = false;
|
||||
if (shouldReplay) {
|
||||
replayMap[CANVAS]?.update({ widgets, theme });
|
||||
}
|
||||
const setupUpdateTreeResponse = dataTreeEvaluator.setupUpdateTree(
|
||||
unevalTree,
|
||||
);
|
||||
evalOrder = setupUpdateTreeResponse.evalOrder;
|
||||
lintOrder = setupUpdateTreeResponse.lintOrder;
|
||||
jsUpdates = setupUpdateTreeResponse.jsUpdates;
|
||||
unEvalUpdates = setupUpdateTreeResponse.unEvalUpdates;
|
||||
|
||||
initiateLinting(
|
||||
lintOrder,
|
||||
makeEntityConfigsAsObjProperties(dataTreeEvaluator.oldUnEvalTree, {
|
||||
sanitizeDataTree: false,
|
||||
}),
|
||||
requiresLinting,
|
||||
);
|
||||
nonDynamicFieldValidationOrder =
|
||||
setupUpdateTreeResponse.nonDynamicFieldValidationOrder;
|
||||
const updateResponse = dataTreeEvaluator.evalAndValidateSubTree(
|
||||
evalOrder,
|
||||
nonDynamicFieldValidationOrder,
|
||||
);
|
||||
dataTree = makeEntityConfigsAsObjProperties(
|
||||
dataTreeEvaluator.evalTree,
|
||||
{
|
||||
evalProps: dataTreeEvaluator.evalProps,
|
||||
},
|
||||
);
|
||||
evalMetaUpdates = JSON.parse(
|
||||
JSON.stringify(updateResponse.evalMetaUpdates),
|
||||
);
|
||||
}
|
||||
dataTreeEvaluator = dataTreeEvaluator as DataTreeEvaluator;
|
||||
dependencies = dataTreeEvaluator.inverseDependencyMap;
|
||||
errors = dataTreeEvaluator.errors;
|
||||
dataTreeEvaluator.clearErrors();
|
||||
logs = dataTreeEvaluator.logs;
|
||||
userLogs = dataTreeEvaluator.userLogs;
|
||||
if (shouldReplay) {
|
||||
if (replayMap[CANVAS]?.logs)
|
||||
logs = logs.concat(replayMap[CANVAS]?.logs);
|
||||
replayMap[CANVAS]?.clearLogs();
|
||||
}
|
||||
|
||||
dataTreeEvaluator.clearLogs();
|
||||
} catch (error) {
|
||||
if (dataTreeEvaluator !== undefined) {
|
||||
errors = dataTreeEvaluator.errors;
|
||||
logs = dataTreeEvaluator.logs;
|
||||
userLogs = dataTreeEvaluator.userLogs;
|
||||
}
|
||||
if (!(error instanceof CrashingError)) {
|
||||
errors.push({
|
||||
type: EvalErrorTypes.UNKNOWN_ERROR,
|
||||
message: (error as Error).message,
|
||||
});
|
||||
console.error(error);
|
||||
}
|
||||
|
||||
dataTree = getSafeToRenderDataTree(
|
||||
makeEntityConfigsAsObjProperties(unevalTree, {
|
||||
sanitizeDataTree: false,
|
||||
evalProps: dataTreeEvaluator?.evalProps,
|
||||
}),
|
||||
widgetTypeConfigMap,
|
||||
);
|
||||
|
||||
unEvalUpdates = [];
|
||||
}
|
||||
|
||||
return {
|
||||
dataTree,
|
||||
dependencies,
|
||||
errors,
|
||||
evalMetaUpdates,
|
||||
evaluationOrder: evalOrder,
|
||||
jsUpdates,
|
||||
logs,
|
||||
userLogs,
|
||||
unEvalUpdates,
|
||||
isCreateFirstTree,
|
||||
} as EvalTreeResponseData;
|
||||
}
|
||||
default: {
|
||||
console.error("Action not registered on evalWorker", method);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.onmessage = messageEventListener(eventRequestHandler);
|
||||
self.addEventListener("message", syncRequestMessageListener);
|
||||
self.addEventListener("message", asyncRequestMessageListener);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,94 @@
|
|||
import { installLibrary, uninstallLibrary } from "../jsLibrary";
|
||||
import { EVAL_WORKER_SYNC_ACTION } from "workers/Evaluation/evalWorkerActions";
|
||||
import * as mod from "../../../common/JSLibrary/ternDefinitionGenerator";
|
||||
|
||||
jest.mock("../../../common/JSLibrary/ternDefinitionGenerator");
|
||||
|
||||
describe("Tests to assert install/uninstall flows", function() {
|
||||
beforeAll(() => {
|
||||
self.importScripts = jest.fn(() => {
|
||||
//@ts-expect-error importScripts is not defined in the test environment
|
||||
self.lodash = {};
|
||||
});
|
||||
|
||||
const mockTernDefsGenerator = jest.fn(() => ({}));
|
||||
|
||||
jest.mock("../../../common/JSLibrary/ternDefinitionGenerator.ts", () => {
|
||||
return {
|
||||
makeTernDefs: mockTernDefsGenerator,
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
it("should install a library", function() {
|
||||
const res = installLibrary({
|
||||
data: {
|
||||
url:
|
||||
"https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.21/lodash.min.js",
|
||||
takenAccessors: [],
|
||||
takenNamesMap: {},
|
||||
},
|
||||
method: EVAL_WORKER_SYNC_ACTION.INSTALL_LIBRARY,
|
||||
});
|
||||
//
|
||||
expect(self.importScripts).toHaveBeenCalled();
|
||||
expect(mod.makeTernDefs).toHaveBeenCalledWith({});
|
||||
|
||||
expect(res).toEqual({
|
||||
success: true,
|
||||
defs: {
|
||||
"!name": "LIB/lodash",
|
||||
lodash: undefined,
|
||||
},
|
||||
accessor: ["lodash"],
|
||||
});
|
||||
});
|
||||
|
||||
it("Reinstalling a different version of the same installed library should fail", function() {
|
||||
const res = installLibrary({
|
||||
data: {
|
||||
url:
|
||||
"https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.16.0/lodash.min.js",
|
||||
takenAccessors: ["lodash"],
|
||||
takenNamesMap: {},
|
||||
},
|
||||
method: EVAL_WORKER_SYNC_ACTION.INSTALL_LIBRARY,
|
||||
});
|
||||
expect(res).toEqual({
|
||||
success: false,
|
||||
defs: {},
|
||||
error: expect.any(Error),
|
||||
});
|
||||
});
|
||||
|
||||
it("Detects name space collision where there is another entity(api, widget or query) with the same name", function() {
|
||||
//@ts-expect-error ignore
|
||||
delete self.lodash;
|
||||
const res = installLibrary({
|
||||
data: {
|
||||
url:
|
||||
"https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.16.0/lodash.min.js",
|
||||
takenAccessors: [],
|
||||
takenNamesMap: { lodash: true },
|
||||
},
|
||||
method: EVAL_WORKER_SYNC_ACTION.INSTALL_LIBRARY,
|
||||
});
|
||||
expect(res).toEqual({
|
||||
success: false,
|
||||
defs: {},
|
||||
error: expect.any(Error),
|
||||
});
|
||||
});
|
||||
|
||||
it("Removes or set the accessors to undefined on the global object on uninstallation", function() {
|
||||
//@ts-expect-error ignore
|
||||
self.lodash = {};
|
||||
const res = uninstallLibrary({
|
||||
data: ["lodash"],
|
||||
method: EVAL_WORKER_SYNC_ACTION.UNINSTALL_LIBRARY,
|
||||
});
|
||||
expect(res).toEqual({ success: true });
|
||||
//@ts-expect-error ignore
|
||||
expect(self.lodash).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
import { dataTreeEvaluator } from "./evalTree";
|
||||
import { removeFunctions } from "../evaluationUtils";
|
||||
import { EvalWorkerSyncRequest } from "../types";
|
||||
|
||||
export default function(request: EvalWorkerSyncRequest) {
|
||||
const { data } = request;
|
||||
const { bindings, executionParams } = data;
|
||||
if (!dataTreeEvaluator) {
|
||||
return { values: undefined, errors: [] };
|
||||
}
|
||||
|
||||
const values = dataTreeEvaluator.evaluateActionBindings(
|
||||
bindings,
|
||||
executionParams,
|
||||
);
|
||||
|
||||
const cleanValues = removeFunctions(values);
|
||||
|
||||
const errors = dataTreeEvaluator.errors;
|
||||
dataTreeEvaluator.clearErrors();
|
||||
return { values: cleanValues, errors };
|
||||
}
|
||||
12
app/client/src/workers/Evaluation/handlers/evalExpression.ts
Normal file
12
app/client/src/workers/Evaluation/handlers/evalExpression.ts
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
import { evaluateAsync } from "../evaluate";
|
||||
import { EvalWorkerASyncRequest } from "../types";
|
||||
import { dataTreeEvaluator } from "./evalTree";
|
||||
|
||||
export default function(request: EvalWorkerASyncRequest) {
|
||||
const { data } = request;
|
||||
const { expression } = data;
|
||||
const evalTree = dataTreeEvaluator?.evalTree;
|
||||
const resolvedFunctions = dataTreeEvaluator?.resolvedFunctions || {};
|
||||
if (!evalTree) return {};
|
||||
return evaluateAsync(expression, evalTree, resolvedFunctions, {});
|
||||
}
|
||||
220
app/client/src/workers/Evaluation/handlers/evalTree.ts
Normal file
220
app/client/src/workers/Evaluation/handlers/evalTree.ts
Normal file
|
|
@ -0,0 +1,220 @@
|
|||
import { UserLogObject } from "entities/AppsmithConsole";
|
||||
import { DataTree } from "entities/DataTree/dataTreeFactory";
|
||||
import ReplayEntity from "entities/Replay";
|
||||
import ReplayCanvas from "entities/Replay/ReplayEntity/ReplayCanvas";
|
||||
import { isEmpty } from "lodash";
|
||||
import {
|
||||
DependencyMap,
|
||||
EvalError,
|
||||
EvalErrorTypes,
|
||||
} from "utils/DynamicBindingUtils";
|
||||
import { JSUpdate } from "utils/JSPaneUtils";
|
||||
import DataTreeEvaluator from "workers/common/DataTreeEvaluator";
|
||||
import { EvalMetaUpdates } from "workers/common/DataTreeEvaluator/types";
|
||||
import { initiateLinting } from "workers/Linting/utils";
|
||||
import {
|
||||
createUnEvalTreeForEval,
|
||||
makeEntityConfigsAsObjProperties,
|
||||
} from "../dataTreeUtils";
|
||||
import {
|
||||
CrashingError,
|
||||
DataTreeDiff,
|
||||
getSafeToRenderDataTree,
|
||||
} from "../evaluationUtils";
|
||||
import {
|
||||
EvalTreeRequestData,
|
||||
EvalTreeResponseData,
|
||||
EvalWorkerSyncRequest,
|
||||
} from "../types";
|
||||
export let replayMap: Record<string, ReplayEntity<any>>;
|
||||
export let dataTreeEvaluator: DataTreeEvaluator | undefined;
|
||||
export const CANVAS = "canvas";
|
||||
|
||||
export default function(request: EvalWorkerSyncRequest) {
|
||||
const { data } = request;
|
||||
let evalOrder: string[] = [];
|
||||
let lintOrder: string[] = [];
|
||||
let jsUpdates: Record<string, JSUpdate> = {};
|
||||
let unEvalUpdates: DataTreeDiff[] = [];
|
||||
let nonDynamicFieldValidationOrder: string[] = [];
|
||||
let isCreateFirstTree = false;
|
||||
let dataTree: DataTree = {};
|
||||
let errors: EvalError[] = [];
|
||||
let logs: any[] = [];
|
||||
let userLogs: UserLogObject[] = [];
|
||||
let dependencies: DependencyMap = {};
|
||||
let evalMetaUpdates: EvalMetaUpdates = [];
|
||||
|
||||
const {
|
||||
allActionValidationConfig,
|
||||
forceEvaluation,
|
||||
requiresLinting,
|
||||
shouldReplay,
|
||||
theme,
|
||||
unevalTree: __unevalTree__,
|
||||
widgets,
|
||||
widgetTypeConfigMap,
|
||||
} = data as EvalTreeRequestData;
|
||||
|
||||
const unevalTree = createUnEvalTreeForEval(__unevalTree__);
|
||||
|
||||
try {
|
||||
if (!dataTreeEvaluator) {
|
||||
isCreateFirstTree = true;
|
||||
replayMap = replayMap || {};
|
||||
replayMap[CANVAS] = new ReplayCanvas({ widgets, theme });
|
||||
dataTreeEvaluator = new DataTreeEvaluator(
|
||||
widgetTypeConfigMap,
|
||||
allActionValidationConfig,
|
||||
);
|
||||
const setupFirstTreeResponse = dataTreeEvaluator.setupFirstTree(
|
||||
unevalTree,
|
||||
);
|
||||
evalOrder = setupFirstTreeResponse.evalOrder;
|
||||
lintOrder = setupFirstTreeResponse.lintOrder;
|
||||
jsUpdates = setupFirstTreeResponse.jsUpdates;
|
||||
|
||||
initiateLinting(
|
||||
lintOrder,
|
||||
makeEntityConfigsAsObjProperties(dataTreeEvaluator.oldUnEvalTree, {
|
||||
sanitizeDataTree: false,
|
||||
}),
|
||||
requiresLinting,
|
||||
);
|
||||
|
||||
const dataTreeResponse = dataTreeEvaluator.evalAndValidateFirstTree();
|
||||
dataTree = makeEntityConfigsAsObjProperties(dataTreeResponse.evalTree, {
|
||||
evalProps: dataTreeEvaluator.evalProps,
|
||||
});
|
||||
} else if (dataTreeEvaluator.hasCyclicalDependency || forceEvaluation) {
|
||||
if (dataTreeEvaluator && !isEmpty(allActionValidationConfig)) {
|
||||
//allActionValidationConfigs may not be set in dataTreeEvaluatior. Therefore, set it explicitly via setter method
|
||||
dataTreeEvaluator.setAllActionValidationConfig(
|
||||
allActionValidationConfig,
|
||||
);
|
||||
}
|
||||
if (shouldReplay) {
|
||||
replayMap[CANVAS]?.update({ widgets, theme });
|
||||
}
|
||||
dataTreeEvaluator = new DataTreeEvaluator(
|
||||
widgetTypeConfigMap,
|
||||
allActionValidationConfig,
|
||||
);
|
||||
if (dataTreeEvaluator && !isEmpty(allActionValidationConfig)) {
|
||||
dataTreeEvaluator.setAllActionValidationConfig(
|
||||
allActionValidationConfig,
|
||||
);
|
||||
}
|
||||
const setupFirstTreeResponse = dataTreeEvaluator.setupFirstTree(
|
||||
unevalTree,
|
||||
);
|
||||
isCreateFirstTree = true;
|
||||
evalOrder = setupFirstTreeResponse.evalOrder;
|
||||
lintOrder = setupFirstTreeResponse.lintOrder;
|
||||
jsUpdates = setupFirstTreeResponse.jsUpdates;
|
||||
|
||||
initiateLinting(
|
||||
lintOrder,
|
||||
makeEntityConfigsAsObjProperties(dataTreeEvaluator.oldUnEvalTree, {
|
||||
sanitizeDataTree: false,
|
||||
}),
|
||||
requiresLinting,
|
||||
);
|
||||
|
||||
const dataTreeResponse = dataTreeEvaluator.evalAndValidateFirstTree();
|
||||
dataTree = makeEntityConfigsAsObjProperties(dataTreeResponse.evalTree, {
|
||||
evalProps: dataTreeEvaluator.evalProps,
|
||||
});
|
||||
} else {
|
||||
if (dataTreeEvaluator && !isEmpty(allActionValidationConfig)) {
|
||||
dataTreeEvaluator.setAllActionValidationConfig(
|
||||
allActionValidationConfig,
|
||||
);
|
||||
}
|
||||
isCreateFirstTree = false;
|
||||
if (shouldReplay) {
|
||||
replayMap[CANVAS]?.update({ widgets, theme });
|
||||
}
|
||||
const setupUpdateTreeResponse = dataTreeEvaluator.setupUpdateTree(
|
||||
unevalTree,
|
||||
);
|
||||
evalOrder = setupUpdateTreeResponse.evalOrder;
|
||||
lintOrder = setupUpdateTreeResponse.lintOrder;
|
||||
jsUpdates = setupUpdateTreeResponse.jsUpdates;
|
||||
unEvalUpdates = setupUpdateTreeResponse.unEvalUpdates;
|
||||
|
||||
initiateLinting(
|
||||
lintOrder,
|
||||
makeEntityConfigsAsObjProperties(dataTreeEvaluator.oldUnEvalTree, {
|
||||
sanitizeDataTree: false,
|
||||
}),
|
||||
requiresLinting,
|
||||
);
|
||||
|
||||
nonDynamicFieldValidationOrder =
|
||||
setupUpdateTreeResponse.nonDynamicFieldValidationOrder;
|
||||
const updateResponse = dataTreeEvaluator.evalAndValidateSubTree(
|
||||
evalOrder,
|
||||
nonDynamicFieldValidationOrder,
|
||||
);
|
||||
dataTree = makeEntityConfigsAsObjProperties(dataTreeEvaluator.evalTree, {
|
||||
evalProps: dataTreeEvaluator.evalProps,
|
||||
});
|
||||
evalMetaUpdates = JSON.parse(
|
||||
JSON.stringify(updateResponse.evalMetaUpdates),
|
||||
);
|
||||
}
|
||||
dataTreeEvaluator = dataTreeEvaluator as DataTreeEvaluator;
|
||||
dependencies = dataTreeEvaluator.inverseDependencyMap;
|
||||
errors = dataTreeEvaluator.errors;
|
||||
dataTreeEvaluator.clearErrors();
|
||||
logs = dataTreeEvaluator.logs;
|
||||
userLogs = dataTreeEvaluator.userLogs;
|
||||
if (shouldReplay) {
|
||||
if (replayMap[CANVAS]?.logs) logs = logs.concat(replayMap[CANVAS]?.logs);
|
||||
replayMap[CANVAS]?.clearLogs();
|
||||
}
|
||||
|
||||
dataTreeEvaluator.clearLogs();
|
||||
} catch (error) {
|
||||
if (dataTreeEvaluator !== undefined) {
|
||||
errors = dataTreeEvaluator.errors;
|
||||
logs = dataTreeEvaluator.logs;
|
||||
userLogs = dataTreeEvaluator.userLogs;
|
||||
}
|
||||
if (!(error instanceof CrashingError)) {
|
||||
errors.push({
|
||||
type: EvalErrorTypes.UNKNOWN_ERROR,
|
||||
message: (error as Error).message,
|
||||
});
|
||||
// eslint-disable-next-line
|
||||
console.error(error);
|
||||
}
|
||||
dataTree = getSafeToRenderDataTree(
|
||||
makeEntityConfigsAsObjProperties(unevalTree, {
|
||||
sanitizeDataTree: false,
|
||||
evalProps: dataTreeEvaluator?.evalProps,
|
||||
}),
|
||||
widgetTypeConfigMap,
|
||||
);
|
||||
unEvalUpdates = [];
|
||||
}
|
||||
|
||||
return {
|
||||
dataTree,
|
||||
dependencies,
|
||||
errors,
|
||||
evalMetaUpdates,
|
||||
evaluationOrder: evalOrder,
|
||||
jsUpdates,
|
||||
logs,
|
||||
userLogs,
|
||||
unEvalUpdates,
|
||||
isCreateFirstTree,
|
||||
} as EvalTreeResponseData;
|
||||
}
|
||||
|
||||
export function clearCache() {
|
||||
dataTreeEvaluator = undefined;
|
||||
return true;
|
||||
}
|
||||
41
app/client/src/workers/Evaluation/handlers/evalTrigger.ts
Normal file
41
app/client/src/workers/Evaluation/handlers/evalTrigger.ts
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
import { dataTreeEvaluator } from "./evalTree";
|
||||
import { EvalWorkerASyncRequest } from "../types";
|
||||
import { createUnEvalTreeForEval } from "../dataTreeUtils";
|
||||
|
||||
export default async function(request: EvalWorkerASyncRequest) {
|
||||
const { data } = request;
|
||||
const {
|
||||
callbackData,
|
||||
dynamicTrigger,
|
||||
eventType,
|
||||
globalContext,
|
||||
triggerMeta,
|
||||
unEvalTree: __unEvalTree__,
|
||||
} = data;
|
||||
if (!dataTreeEvaluator) {
|
||||
return { triggers: [], errors: [] };
|
||||
}
|
||||
const unEvalTree = createUnEvalTreeForEval(__unEvalTree__);
|
||||
const {
|
||||
evalOrder,
|
||||
nonDynamicFieldValidationOrder,
|
||||
} = dataTreeEvaluator.setupUpdateTree(unEvalTree);
|
||||
dataTreeEvaluator.evalAndValidateSubTree(
|
||||
evalOrder,
|
||||
nonDynamicFieldValidationOrder,
|
||||
);
|
||||
const evalTree = dataTreeEvaluator.evalTree;
|
||||
const resolvedFunctions = dataTreeEvaluator.resolvedFunctions;
|
||||
|
||||
return dataTreeEvaluator.evaluateTriggers(
|
||||
dynamicTrigger,
|
||||
evalTree,
|
||||
resolvedFunctions,
|
||||
callbackData,
|
||||
{
|
||||
globalContext,
|
||||
eventType,
|
||||
triggerMeta,
|
||||
},
|
||||
);
|
||||
}
|
||||
21
app/client/src/workers/Evaluation/handlers/executeSyncJS.ts
Normal file
21
app/client/src/workers/Evaluation/handlers/executeSyncJS.ts
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import evaluateSync from "../evaluate";
|
||||
import { dataTreeEvaluator } from "./evalTree";
|
||||
import { EvalWorkerSyncRequest } from "../types";
|
||||
|
||||
export default function(request: EvalWorkerSyncRequest) {
|
||||
const { data } = request;
|
||||
const { functionCall } = data;
|
||||
|
||||
if (!dataTreeEvaluator) {
|
||||
return true;
|
||||
}
|
||||
const evalTree = dataTreeEvaluator.evalTree;
|
||||
const resolvedFunctions = dataTreeEvaluator.resolvedFunctions;
|
||||
return evaluateSync(
|
||||
functionCall,
|
||||
evalTree,
|
||||
resolvedFunctions,
|
||||
false,
|
||||
undefined,
|
||||
);
|
||||
}
|
||||
50
app/client/src/workers/Evaluation/handlers/index.ts
Normal file
50
app/client/src/workers/Evaluation/handlers/index.ts
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
import noop from "lodash/noop";
|
||||
import {
|
||||
EVAL_WORKER_ACTIONS,
|
||||
EVAL_WORKER_ASYNC_ACTION,
|
||||
EVAL_WORKER_SYNC_ACTION,
|
||||
} from "workers/Evaluation/evalWorkerActions";
|
||||
import { EvalWorkerSyncRequest, EvalWorkerASyncRequest } from "../types";
|
||||
import evalActionBindings from "./evalActionBindings";
|
||||
import evalExpression from "./evalExpression";
|
||||
import evalTree, { clearCache } from "./evalTree";
|
||||
import evalTrigger from "./evalTrigger";
|
||||
import executeSyncJS from "./executeSyncJS";
|
||||
import initFormEval from "./initFormEval";
|
||||
import { installLibrary, loadLibraries, uninstallLibrary } from "./jsLibrary";
|
||||
import { redo, undo, updateReplayObject } from "./replay";
|
||||
import setupEvaluationEnvironment, {
|
||||
setEvaluationVersion,
|
||||
} from "./setupEvalEnv";
|
||||
import validateProperty from "./validateProperty";
|
||||
|
||||
const syncHandlerMap: Record<
|
||||
EVAL_WORKER_SYNC_ACTION,
|
||||
(req: EvalWorkerSyncRequest) => any
|
||||
> = {
|
||||
[EVAL_WORKER_ACTIONS.EVAL_ACTION_BINDINGS]: evalActionBindings,
|
||||
[EVAL_WORKER_ACTIONS.EVAL_TREE]: evalTree,
|
||||
[EVAL_WORKER_ACTIONS.EXECUTE_SYNC_JS]: executeSyncJS,
|
||||
[EVAL_WORKER_ACTIONS.UNDO]: undo,
|
||||
[EVAL_WORKER_ACTIONS.REDO]: redo,
|
||||
[EVAL_WORKER_ACTIONS.UPDATE_REPLAY_OBJECT]: updateReplayObject,
|
||||
[EVAL_WORKER_ACTIONS.VALIDATE_PROPERTY]: validateProperty,
|
||||
[EVAL_WORKER_ACTIONS.INSTALL_LIBRARY]: installLibrary,
|
||||
[EVAL_WORKER_ACTIONS.UNINSTALL_LIBRARY]: uninstallLibrary,
|
||||
[EVAL_WORKER_ACTIONS.LOAD_LIBRARIES]: loadLibraries,
|
||||
[EVAL_WORKER_ACTIONS.LINT_TREE]: noop,
|
||||
[EVAL_WORKER_ACTIONS.SETUP]: setupEvaluationEnvironment,
|
||||
[EVAL_WORKER_ACTIONS.CLEAR_CACHE]: clearCache,
|
||||
[EVAL_WORKER_ACTIONS.SET_EVALUATION_VERSION]: setEvaluationVersion,
|
||||
[EVAL_WORKER_ACTIONS.INIT_FORM_EVAL]: initFormEval,
|
||||
};
|
||||
|
||||
const asyncHandlerMap: Record<
|
||||
EVAL_WORKER_ASYNC_ACTION,
|
||||
(req: EvalWorkerASyncRequest) => any
|
||||
> = {
|
||||
[EVAL_WORKER_ACTIONS.EVAL_TRIGGER]: evalTrigger,
|
||||
[EVAL_WORKER_ACTIONS.EVAL_EXPRESSION]: evalExpression,
|
||||
};
|
||||
|
||||
export { syncHandlerMap, asyncHandlerMap };
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
import { setFormEvaluationSaga } from "../formEval";
|
||||
import { EvalWorkerSyncRequest } from "../types";
|
||||
|
||||
export default function(request: EvalWorkerSyncRequest) {
|
||||
const { data } = request;
|
||||
const { currentEvalState, payload, type } = data;
|
||||
const response = setFormEvaluationSaga(type, payload, currentEvalState);
|
||||
return response;
|
||||
}
|
||||
202
app/client/src/workers/Evaluation/handlers/jsLibrary.ts
Normal file
202
app/client/src/workers/Evaluation/handlers/jsLibrary.ts
Normal file
|
|
@ -0,0 +1,202 @@
|
|||
import {
|
||||
createMessage,
|
||||
customJSLibraryMessages,
|
||||
} from "@appsmith/constants/messages";
|
||||
import difference from "lodash/difference";
|
||||
import { Def } from "tern";
|
||||
import {
|
||||
JSLibraries,
|
||||
libraryReservedIdentifiers,
|
||||
} from "../../common/JSLibrary";
|
||||
import { makeTernDefs } from "../../common/JSLibrary/ternDefinitionGenerator";
|
||||
import { EvalWorkerSyncRequest } from "../types";
|
||||
|
||||
enum LibraryInstallError {
|
||||
NameCollisionError,
|
||||
ImportError,
|
||||
TernDefinitionError,
|
||||
LibraryOverrideError,
|
||||
}
|
||||
|
||||
class NameCollisionError extends Error {
|
||||
code = LibraryInstallError.NameCollisionError;
|
||||
constructor(accessors: string) {
|
||||
super(
|
||||
createMessage(customJSLibraryMessages.NAME_COLLISION_ERROR, accessors),
|
||||
);
|
||||
this.name = "NameCollisionError";
|
||||
}
|
||||
}
|
||||
|
||||
class ImportError extends Error {
|
||||
code = LibraryInstallError.ImportError;
|
||||
constructor(url: string) {
|
||||
super(createMessage(customJSLibraryMessages.IMPORT_URL_ERROR, url));
|
||||
this.name = "ImportError";
|
||||
}
|
||||
}
|
||||
|
||||
class TernDefinitionError extends Error {
|
||||
code = LibraryInstallError.TernDefinitionError;
|
||||
constructor(name: string) {
|
||||
super(createMessage(customJSLibraryMessages.DEFS_FAILED_ERROR, name));
|
||||
this.name = "TernDefinitionError";
|
||||
}
|
||||
}
|
||||
|
||||
class LibraryOverrideError extends Error {
|
||||
code = LibraryInstallError.LibraryOverrideError;
|
||||
data: any;
|
||||
constructor(name: string, data: any) {
|
||||
super(createMessage(customJSLibraryMessages.LIB_OVERRIDE_ERROR, name));
|
||||
this.name = "LibraryOverrideError";
|
||||
this.data = data;
|
||||
}
|
||||
}
|
||||
|
||||
export function installLibrary(request: EvalWorkerSyncRequest) {
|
||||
const { data } = request;
|
||||
const { takenAccessors, takenNamesMap, url } = data;
|
||||
const defs: Def = {};
|
||||
try {
|
||||
const currentEnvKeys = Object.keys(self);
|
||||
|
||||
//@ts-expect-error Find libraries that were uninstalled.
|
||||
const unsetKeys = currentEnvKeys.filter((key) => self[key] === undefined);
|
||||
|
||||
const existingLibraries: Record<string, any> = {};
|
||||
|
||||
for (const acc of takenAccessors) {
|
||||
existingLibraries[acc] = self[acc];
|
||||
}
|
||||
|
||||
try {
|
||||
self.importScripts(url);
|
||||
} catch (e) {
|
||||
throw new ImportError(url);
|
||||
}
|
||||
|
||||
// Find keys add that were installed to the global scope.
|
||||
const accessor = difference(Object.keys(self), currentEnvKeys) as Array<
|
||||
string
|
||||
>;
|
||||
|
||||
checkForNameCollision(accessor, takenNamesMap);
|
||||
|
||||
checkIfUninstalledEarlier(accessor, unsetKeys);
|
||||
|
||||
checkForOverrides(url, accessor, takenAccessors, existingLibraries);
|
||||
|
||||
if (accessor.length === 0) return { status: false, defs, accessor };
|
||||
|
||||
//Reserves accessor names.
|
||||
const name = accessor[accessor.length - 1];
|
||||
|
||||
defs["!name"] = `LIB/${name}`;
|
||||
try {
|
||||
for (const key of accessor) {
|
||||
//@ts-expect-error no types
|
||||
defs[key] = makeTernDefs(self[key]);
|
||||
}
|
||||
} catch (e) {
|
||||
for (const acc of accessor) {
|
||||
//@ts-expect-error no types
|
||||
self[acc] = undefined;
|
||||
}
|
||||
throw new TernDefinitionError(
|
||||
`Failed to generate autocomplete definitions: ${name}`,
|
||||
);
|
||||
}
|
||||
|
||||
//Reserve accessor names.
|
||||
for (const acc of accessor) {
|
||||
libraryReservedIdentifiers[acc] = true;
|
||||
}
|
||||
|
||||
return { success: true, defs, accessor };
|
||||
} catch (error) {
|
||||
return { success: false, defs, error };
|
||||
}
|
||||
}
|
||||
|
||||
export function uninstallLibrary(request: EvalWorkerSyncRequest) {
|
||||
const { data } = request;
|
||||
const accessor = data;
|
||||
try {
|
||||
for (const key of accessor) {
|
||||
try {
|
||||
delete self[key];
|
||||
} catch (e) {
|
||||
//@ts-expect-error ignore
|
||||
self[key] = undefined;
|
||||
}
|
||||
delete libraryReservedIdentifiers[key];
|
||||
}
|
||||
return { success: true };
|
||||
} catch (e) {
|
||||
return { success: false };
|
||||
}
|
||||
}
|
||||
|
||||
export function loadLibraries(request: EvalWorkerSyncRequest) {
|
||||
//Add types
|
||||
const { data } = request;
|
||||
const urls = data.map((lib: any) => lib.url);
|
||||
const keysBefore = Object.keys(self);
|
||||
let message = "";
|
||||
|
||||
try {
|
||||
self.importScripts(...urls);
|
||||
} catch (e) {
|
||||
message = (e as Error).message;
|
||||
}
|
||||
const keysAfter = Object.keys(self);
|
||||
const newKeys = difference(keysAfter, keysBefore);
|
||||
for (const key of newKeys) {
|
||||
libraryReservedIdentifiers[key] = true;
|
||||
}
|
||||
JSLibraries.push(...data);
|
||||
return { success: !message, message };
|
||||
}
|
||||
|
||||
function checkForNameCollision(
|
||||
accessor: string[],
|
||||
takenNamesMap: Record<string, any>,
|
||||
) {
|
||||
const collidingNames = accessor.filter((key: string) => takenNamesMap[key]);
|
||||
if (collidingNames.length) {
|
||||
for (const acc of accessor) {
|
||||
//@ts-expect-error no types
|
||||
self[acc] = undefined;
|
||||
}
|
||||
throw new NameCollisionError(collidingNames.join(", "));
|
||||
}
|
||||
}
|
||||
|
||||
function checkIfUninstalledEarlier(accessor: string[], unsetKeys: string[]) {
|
||||
if (accessor.length > 0) return;
|
||||
for (const key of unsetKeys) {
|
||||
//@ts-expect-error no types
|
||||
if (!self[key]) continue;
|
||||
accessor.push(key);
|
||||
}
|
||||
}
|
||||
|
||||
function checkForOverrides(
|
||||
url: string,
|
||||
accessor: string[],
|
||||
takenAccessors: string[],
|
||||
existingLibraries: Record<string, any>,
|
||||
) {
|
||||
if (accessor.length > 0) return;
|
||||
const overriddenAccessors: Array<string> = [];
|
||||
for (const acc of takenAccessors) {
|
||||
//@ts-expect-error no types
|
||||
if (existingLibraries[acc] === self[acc]) continue;
|
||||
//@ts-expect-error no types
|
||||
self[acc] = existingLibraries[acc];
|
||||
overriddenAccessors.push(acc);
|
||||
}
|
||||
if (overriddenAccessors.length === 0) return;
|
||||
throw new LibraryOverrideError(url, overriddenAccessors);
|
||||
}
|
||||
33
app/client/src/workers/Evaluation/handlers/replay.ts
Normal file
33
app/client/src/workers/Evaluation/handlers/replay.ts
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
import ReplayEditor from "entities/Replay/ReplayEntity/ReplayEditor";
|
||||
import { EvalWorkerSyncRequest } from "../types";
|
||||
import { CANVAS, replayMap } from "./evalTree";
|
||||
|
||||
export function undo(request: EvalWorkerSyncRequest) {
|
||||
const { data } = request;
|
||||
const { entityId } = data;
|
||||
if (!replayMap[entityId || CANVAS]) return;
|
||||
const replayResult = replayMap[entityId || CANVAS].replay("UNDO");
|
||||
replayMap[entityId || CANVAS].clearLogs();
|
||||
return replayResult;
|
||||
}
|
||||
|
||||
export function redo(request: EvalWorkerSyncRequest) {
|
||||
const { data } = request;
|
||||
const { entityId } = data;
|
||||
if (!replayMap[entityId ?? CANVAS]) return;
|
||||
const replayResult = replayMap[entityId ?? CANVAS].replay("REDO");
|
||||
replayMap[entityId ?? CANVAS].clearLogs();
|
||||
return replayResult;
|
||||
}
|
||||
|
||||
export function updateReplayObject(request: EvalWorkerSyncRequest) {
|
||||
const { data } = request;
|
||||
const { entity, entityId, entityType } = data;
|
||||
const replayObject = replayMap[entityId];
|
||||
if (replayObject) {
|
||||
replayObject.update(entity);
|
||||
} else {
|
||||
replayMap[entityId] = new ReplayEditor(entity, entityType);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
37
app/client/src/workers/Evaluation/handlers/setupEvalEnv.ts
Normal file
37
app/client/src/workers/Evaluation/handlers/setupEvalEnv.ts
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
import { unsafeFunctionForEval } from "utils/DynamicBindingUtils";
|
||||
import interceptAndOverrideHttpRequest from "../HTTPRequestOverride";
|
||||
import { resetJSLibraries } from "../../common/JSLibrary";
|
||||
import setupDOM from "../SetupDOM";
|
||||
import overrideTimeout from "../TimeoutOverride";
|
||||
import { EvalWorkerSyncRequest } from "../types";
|
||||
import userLogs from "../UserLog";
|
||||
|
||||
export default function() {
|
||||
const libraries = resetJSLibraries();
|
||||
///// Adding extra libraries separately
|
||||
libraries.forEach((library) => {
|
||||
// @ts-expect-error: Types are not available
|
||||
self[library.accessor] = library.lib;
|
||||
});
|
||||
|
||||
///// Remove all unsafe functions
|
||||
unsafeFunctionForEval.forEach((func) => {
|
||||
// @ts-expect-error: Types are not available
|
||||
self[func] = undefined;
|
||||
});
|
||||
self.window = self;
|
||||
userLogs.overrideConsoleAPI();
|
||||
overrideTimeout();
|
||||
interceptAndOverrideHttpRequest();
|
||||
setupDOM();
|
||||
return true;
|
||||
}
|
||||
|
||||
export function setEvaluationVersion(request: EvalWorkerSyncRequest) {
|
||||
const { data } = request;
|
||||
const { version } = data;
|
||||
self.evaluationVersion = version || 1;
|
||||
// TODO: Move this to setup
|
||||
resetJSLibraries();
|
||||
return true;
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
import { validateWidgetProperty } from "workers/common/DataTreeEvaluator/validationUtils";
|
||||
import { removeFunctions } from "../evaluationUtils";
|
||||
import { EvalWorkerSyncRequest } from "../types";
|
||||
|
||||
export default function(request: EvalWorkerSyncRequest) {
|
||||
const { data } = request;
|
||||
const { property, props, validation, value } = data;
|
||||
return removeFunctions(
|
||||
validateWidgetProperty(validation, value, props, property),
|
||||
);
|
||||
}
|
||||
|
|
@ -4,18 +4,22 @@ import { AppTheme } from "entities/AppTheming";
|
|||
import { DataTree, UnEvalTree } from "entities/DataTree/dataTreeFactory";
|
||||
import { CanvasWidgetsReduxState } from "reducers/entityReducers/canvasWidgetsReducer";
|
||||
|
||||
import { DependencyMap, EvalError } from "utils/DynamicBindingUtils";
|
||||
import {
|
||||
DependencyMap,
|
||||
EvalError,
|
||||
EVAL_WORKER_ACTIONS,
|
||||
} from "utils/DynamicBindingUtils";
|
||||
EVAL_WORKER_ASYNC_ACTION,
|
||||
EVAL_WORKER_SYNC_ACTION,
|
||||
} from "workers/Evaluation/evalWorkerActions";
|
||||
import { JSUpdate } from "utils/JSPaneUtils";
|
||||
import { WidgetTypeConfigMap } from "utils/WidgetFactory";
|
||||
import { EvalMetaUpdates } from "workers/common/DataTreeEvaluator/types";
|
||||
import { WorkerRequest } from "workers/common/types";
|
||||
import { DataTreeDiff } from "./evaluationUtils";
|
||||
|
||||
export type EvalWorkerRequest = WorkerRequest<any, EVAL_WORKER_ACTIONS>;
|
||||
export type EvalWorkerSyncRequest = WorkerRequest<any, EVAL_WORKER_SYNC_ACTION>;
|
||||
export type EvalWorkerASyncRequest = WorkerRequest<
|
||||
any,
|
||||
EVAL_WORKER_ASYNC_ACTION
|
||||
>;
|
||||
export type EvalWorkerResponse = EvalTreeResponseData | boolean | unknown;
|
||||
|
||||
export interface EvalTreeRequestData {
|
||||
|
|
@ -28,6 +32,7 @@ export interface EvalTreeRequestData {
|
|||
[actionId: string]: ActionValidationConfigMap;
|
||||
};
|
||||
requiresLinting: boolean;
|
||||
forceEvaluation: boolean;
|
||||
}
|
||||
export interface EvalTreeResponseData {
|
||||
dataTree: DataTree;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
import { isEqual } from "lodash";
|
||||
import { WorkerErrorTypes } from "workers/common/types";
|
||||
import { JSLibraries, resetJSLibraries } from "workers/common/JSLibrary";
|
||||
import {
|
||||
LintWorkerRequest,
|
||||
LintTreeResponse,
|
||||
|
|
@ -6,29 +8,38 @@ import {
|
|||
LintTreeRequest,
|
||||
} from "./types";
|
||||
import { getlintErrorsFromTree } from "./utils";
|
||||
import { TMessage, MessageType, sendMessage } from "utils/MessageUtil";
|
||||
|
||||
function messageEventListener(fn: typeof eventRequestHandler) {
|
||||
return (event: MessageEvent<LintWorkerRequest>) => {
|
||||
const { method, requestId } = event.data;
|
||||
return (event: MessageEvent<TMessage<LintWorkerRequest>>) => {
|
||||
const { messageType } = event.data;
|
||||
if (messageType !== MessageType.REQUEST) return;
|
||||
const { body, messageId } = event.data;
|
||||
const { data, method } = body;
|
||||
if (!method) return;
|
||||
|
||||
const startTime = performance.now();
|
||||
const responseData = fn(event.data);
|
||||
const responseData = fn({ method, requestData: data });
|
||||
const endTime = performance.now();
|
||||
if (!responseData) return;
|
||||
|
||||
try {
|
||||
self.postMessage({
|
||||
requestId,
|
||||
responseData,
|
||||
sendMessage.call(self, {
|
||||
messageId,
|
||||
messageType: MessageType.RESPONSE,
|
||||
body: {
|
||||
data: responseData,
|
||||
timeTaken: (endTime - startTime).toFixed(2),
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(e);
|
||||
self.postMessage({
|
||||
requestId,
|
||||
responseData: {
|
||||
sendMessage.call(self, {
|
||||
messageId,
|
||||
messageType: MessageType.RESPONSE,
|
||||
body: {
|
||||
data: {
|
||||
errors: [
|
||||
{
|
||||
type: WorkerErrorTypes.CLONE_ERROR,
|
||||
|
|
@ -37,6 +48,7 @@ function messageEventListener(fn: typeof eventRequestHandler) {
|
|||
],
|
||||
},
|
||||
timeTaken: (endTime - startTime).toFixed(2),
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
@ -45,7 +57,10 @@ function messageEventListener(fn: typeof eventRequestHandler) {
|
|||
function eventRequestHandler({
|
||||
method,
|
||||
requestData,
|
||||
}: LintWorkerRequest): LintTreeResponse | unknown {
|
||||
}: {
|
||||
method: LINT_WORKER_ACTIONS;
|
||||
requestData: any;
|
||||
}): LintTreeResponse | unknown {
|
||||
switch (method) {
|
||||
case LINT_WORKER_ACTIONS.LINT_TREE: {
|
||||
const lintTreeResponse: LintTreeResponse = { errors: {} };
|
||||
|
|
@ -56,7 +71,24 @@ function eventRequestHandler({
|
|||
} catch (e) {}
|
||||
return lintTreeResponse;
|
||||
}
|
||||
|
||||
case LINT_WORKER_ACTIONS.UPDATE_LINT_GLOBALS: {
|
||||
const { add, libs } = requestData;
|
||||
if (add) {
|
||||
JSLibraries.push(...libs);
|
||||
} else if (add === false) {
|
||||
for (const lib of libs) {
|
||||
const idx = JSLibraries.findIndex((l) =>
|
||||
isEqual(l.accessor.sort(), lib.accessor.sort()),
|
||||
);
|
||||
if (idx === -1) return;
|
||||
JSLibraries.splice(idx, 1);
|
||||
}
|
||||
} else {
|
||||
resetJSLibraries();
|
||||
JSLibraries.push(...libs);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
default: {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error("Action not registered on lintWorker ", method);
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { WorkerRequest } from "workers/common/types";
|
|||
|
||||
export enum LINT_WORKER_ACTIONS {
|
||||
LINT_TREE = "LINT_TREE",
|
||||
UPDATE_LINT_GLOBALS = "UPDATE_LINT_GLOBALS",
|
||||
}
|
||||
|
||||
export interface LintTreeResponse {
|
||||
|
|
|
|||
|
|
@ -2,13 +2,12 @@ import { DataTree, DataTreeEntity } from "entities/DataTree/dataTreeFactory";
|
|||
|
||||
import { Position } from "codemirror";
|
||||
import {
|
||||
EVAL_WORKER_ACTIONS,
|
||||
extraLibraries,
|
||||
isDynamicValue,
|
||||
isPathADynamicBinding,
|
||||
LintError,
|
||||
PropertyEvaluationErrorType,
|
||||
} from "utils/DynamicBindingUtils";
|
||||
import { MAIN_THREAD_ACTION } from "workers/Evaluation/evalWorkerActions";
|
||||
import {
|
||||
JSHINT as jshint,
|
||||
LintError as JSHintError,
|
||||
|
|
@ -52,6 +51,8 @@ import {
|
|||
} from "workers/Evaluation/evaluationUtils";
|
||||
import { LintErrors } from "reducers/lintingReducers/lintErrorsReducers";
|
||||
import { Severity } from "entities/AppsmithConsole";
|
||||
import { JSLibraries } from "workers/common/JSLibrary";
|
||||
import { MessageType, sendMessage } from "utils/MessageUtil";
|
||||
|
||||
export function getlintErrorsFromTree(
|
||||
pathsToLint: string[],
|
||||
|
|
@ -302,7 +303,11 @@ export function getLintingErrors(
|
|||
globalData[dataKey] = true;
|
||||
}
|
||||
// Jshint shouldn't throw errors for additional libraries
|
||||
extraLibraries.forEach((lib) => (globalData[lib.accessor] = true));
|
||||
const libAccessors = ([] as string[]).concat(
|
||||
...JSLibraries.map((lib) => lib.accessor),
|
||||
);
|
||||
libAccessors.forEach((accessor) => (globalData[accessor] = true));
|
||||
|
||||
// JSHint shouldn't throw errors for supported web apis
|
||||
Object.keys(SUPPORTED_WEB_APIS).forEach(
|
||||
(apiName) => (globalData[apiName] = true),
|
||||
|
|
@ -464,12 +469,15 @@ export function initiateLinting(
|
|||
requiresLinting: boolean,
|
||||
) {
|
||||
if (!requiresLinting) return;
|
||||
postMessage({
|
||||
promisified: true,
|
||||
responseData: {
|
||||
sendMessage.call(self, {
|
||||
messageId: "",
|
||||
messageType: MessageType.REQUEST,
|
||||
body: {
|
||||
data: {
|
||||
lintOrder,
|
||||
unevalTree,
|
||||
type: EVAL_WORKER_ACTIONS.LINT_TREE,
|
||||
},
|
||||
method: MAIN_THREAD_ACTION.LINT_TREE,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,7 +38,6 @@ function getFile(file: string, c: CallbackFn) {
|
|||
}
|
||||
|
||||
function startServer(defs: Def[], plugins = {}, scripts?: string[]) {
|
||||
//@ts-expect-error test
|
||||
if (scripts) self.importScripts.apply(null, scripts);
|
||||
|
||||
server = new tern.Server({
|
||||
|
|
|
|||
|
|
@ -1013,7 +1013,6 @@ export default class DataTreeEvaluator {
|
|||
async evaluateTriggers(
|
||||
userScript: string,
|
||||
dataTree: DataTree,
|
||||
requestId: string,
|
||||
resolvedFunctions: Record<string, any>,
|
||||
callbackData: Array<unknown>,
|
||||
context?: EvaluateContext,
|
||||
|
|
@ -1022,7 +1021,6 @@ export default class DataTreeEvaluator {
|
|||
return evaluateAsync(
|
||||
jsSnippets[0] || userScript,
|
||||
dataTree,
|
||||
requestId,
|
||||
resolvedFunctions,
|
||||
context,
|
||||
callbackData,
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import {
|
|||
EvalError,
|
||||
DependencyMap,
|
||||
getDynamicBindings,
|
||||
extraLibrariesNames,
|
||||
getEntityDynamicBindingPathList,
|
||||
} from "utils/DynamicBindingUtils";
|
||||
import { extractIdentifierInfoFromCode } from "@shared/ast";
|
||||
|
|
@ -26,6 +25,7 @@ import {
|
|||
JAVASCRIPT_KEYWORDS,
|
||||
} from "constants/WidgetValidation";
|
||||
import { APPSMITH_GLOBAL_FUNCTIONS } from "components/editorComponents/ActionCreator/constants";
|
||||
import { libraryReservedIdentifiers } from "workers/common/JSLibrary";
|
||||
|
||||
/** This function extracts validReferences and invalidReferences from a binding {{}}
|
||||
* @param script
|
||||
|
|
@ -45,7 +45,7 @@ export const extractInfoFromBinding = (
|
|||
const { references } = extractIdentifierInfoFromCode(
|
||||
script,
|
||||
self.evaluationVersion,
|
||||
invalidEntityIdentifiers,
|
||||
{ ...invalidEntityIdentifiers, ...libraryReservedIdentifiers },
|
||||
);
|
||||
return extractInfoFromReferences(references, allPaths);
|
||||
};
|
||||
|
|
@ -207,7 +207,6 @@ const invalidEntityIdentifiers: Record<string, unknown> = {
|
|||
...JAVASCRIPT_KEYWORDS,
|
||||
...APPSMITH_GLOBAL_FUNCTIONS,
|
||||
...DEDICATED_WORKER_GLOBAL_SCOPE_IDENTIFIERS,
|
||||
...extraLibrariesNames,
|
||||
};
|
||||
|
||||
export function listEntityDependencies(
|
||||
|
|
|
|||
|
|
@ -0,0 +1,62 @@
|
|||
import { makeTernDefs } from "../ternDefinitionGenerator";
|
||||
|
||||
describe("Tests tern definition generator", () => {
|
||||
const obj = {
|
||||
var1: "myVar1",
|
||||
var2: 2,
|
||||
var3: true,
|
||||
var4: null,
|
||||
var5: undefined,
|
||||
var6: { a: 1, b: 2 },
|
||||
var7: () => {
|
||||
return "there!";
|
||||
},
|
||||
var8: function() {
|
||||
return "hey, ";
|
||||
},
|
||||
var9: new Date(),
|
||||
};
|
||||
const proto = {
|
||||
sayHello() {
|
||||
return "Hello";
|
||||
},
|
||||
};
|
||||
it("Correctly determines tern def types based", () => {
|
||||
const expected = {
|
||||
var1: { "!type": "string" },
|
||||
var2: { "!type": "number" },
|
||||
var3: { "!type": "bool" },
|
||||
var4: { "!type": "?" },
|
||||
var5: { "!type": "?" },
|
||||
var6: { a: { "!type": "number" }, b: { "!type": "number" } },
|
||||
var7: { "!type": "fn()" },
|
||||
};
|
||||
const defs = makeTernDefs(obj);
|
||||
expect(defs).toMatchObject(expected);
|
||||
});
|
||||
it("should look up the prototype chain on objects", () => {
|
||||
Object.setPrototypeOf(obj, proto);
|
||||
const expected = {
|
||||
sayHello: { "!type": "fn()" },
|
||||
};
|
||||
const defs = makeTernDefs(proto);
|
||||
expect(defs).toMatchObject(expected);
|
||||
});
|
||||
it("should look up the prototype property on functions", () => {
|
||||
obj.var8.prototype = {
|
||||
sayWorld() {
|
||||
return "World";
|
||||
},
|
||||
};
|
||||
const expected = {
|
||||
var8: {
|
||||
"!type": "fn()",
|
||||
prototype: {
|
||||
sayWorld: { "!type": "fn()" },
|
||||
},
|
||||
},
|
||||
};
|
||||
const defs = makeTernDefs(obj);
|
||||
expect(defs).toMatchObject(expected);
|
||||
});
|
||||
});
|
||||
74
app/client/src/workers/common/JSLibrary/index.ts
Normal file
74
app/client/src/workers/common/JSLibrary/index.ts
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
import _, { VERSION as lodashVersion } from "lodash";
|
||||
import moment from "moment-timezone";
|
||||
import parser from "fast-xml-parser";
|
||||
import forge from "node-forge";
|
||||
|
||||
export type TJSLibrary = {
|
||||
version?: string;
|
||||
docsURL: string;
|
||||
name: string;
|
||||
accessor: string[];
|
||||
lib?: any;
|
||||
url?: string;
|
||||
};
|
||||
export const defaultLibraries: TJSLibrary[] = [
|
||||
{
|
||||
accessor: ["_"],
|
||||
lib: _,
|
||||
version: lodashVersion,
|
||||
docsURL: `https://lodash.com/docs/${lodashVersion}`,
|
||||
name: "lodash",
|
||||
},
|
||||
{
|
||||
accessor: ["moment"],
|
||||
lib: moment,
|
||||
version: moment.version,
|
||||
docsURL: `https://momentjs.com/docs/`,
|
||||
name: "moment",
|
||||
},
|
||||
{
|
||||
accessor: ["xmlParser"],
|
||||
lib: parser,
|
||||
version: "3.17.5",
|
||||
docsURL: "https://github.com/NaturalIntelligence/fast-xml-parser",
|
||||
name: "xmlParser",
|
||||
},
|
||||
{
|
||||
accessor: ["forge"],
|
||||
// We are removing some functionalities of node-forge because they wont
|
||||
// work in the worker thread
|
||||
lib: _.omit(forge, ["tls", "http", "xhr", "socket", "task"]),
|
||||
version: "1.3.0",
|
||||
docsURL: "https://github.com/digitalbazaar/forge",
|
||||
name: "forge",
|
||||
},
|
||||
];
|
||||
|
||||
export const JSLibraries = [...defaultLibraries];
|
||||
export const libraryReservedIdentifiers = defaultLibraries.reduce(
|
||||
(acc, lib) => {
|
||||
lib.accessor.forEach((a) => (acc[a] = true));
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, boolean>,
|
||||
);
|
||||
|
||||
export function resetJSLibraries() {
|
||||
JSLibraries.length = 0;
|
||||
JSLibraries.push(...defaultLibraries);
|
||||
const defaultLibraryAccessors = defaultLibraries.map(
|
||||
(lib) => lib.accessor[0],
|
||||
);
|
||||
for (const key of Object.keys(libraryReservedIdentifiers)) {
|
||||
if (defaultLibraryAccessors.includes(key)) continue;
|
||||
try {
|
||||
// @ts-expect-error: Types are not available
|
||||
delete self[key];
|
||||
} catch (e) {
|
||||
// @ts-expect-error: Types are not available
|
||||
self[key] = undefined;
|
||||
}
|
||||
delete libraryReservedIdentifiers[key];
|
||||
}
|
||||
return JSLibraries;
|
||||
}
|
||||
|
|
@ -0,0 +1,84 @@
|
|||
import log from "loglevel";
|
||||
import { Def } from "tern";
|
||||
|
||||
function getTernDocType(obj: any) {
|
||||
const type = typeof obj;
|
||||
switch (type) {
|
||||
case "string":
|
||||
return "string";
|
||||
case "number":
|
||||
return "number";
|
||||
case "boolean":
|
||||
return "bool";
|
||||
case "undefined":
|
||||
return "?";
|
||||
case "function":
|
||||
return "fn()";
|
||||
default:
|
||||
return "?";
|
||||
}
|
||||
}
|
||||
|
||||
const ignoredKeys = [
|
||||
"constructor",
|
||||
"WINDOW",
|
||||
"window",
|
||||
"self",
|
||||
"arguments",
|
||||
"caller",
|
||||
"length",
|
||||
"name",
|
||||
];
|
||||
|
||||
export function makeTernDefs(obj: any) {
|
||||
const defs: Def = {};
|
||||
const cachedDefs: any = [];
|
||||
const visitedReferences: any = [];
|
||||
const MAX_ITERATIONS = 5000;
|
||||
let iteration_count = 1;
|
||||
const baseObjPrototype = Object.getPrototypeOf({});
|
||||
|
||||
const queue = [[obj, defs]];
|
||||
|
||||
try {
|
||||
while (queue.length && iteration_count < MAX_ITERATIONS) {
|
||||
const [src, target] = queue.shift() as any;
|
||||
if (visitedReferences.includes(src)) {
|
||||
target["!type"] = cachedDefs[visitedReferences.indexOf(src)]["!type"];
|
||||
continue;
|
||||
}
|
||||
const type = typeof src;
|
||||
if (!src || (type !== "object" && type !== "function")) {
|
||||
target["!type"] = getTernDocType(src);
|
||||
continue;
|
||||
} else if (type === "function") {
|
||||
target["!type"] = "fn()";
|
||||
}
|
||||
queue.push(
|
||||
...Object.getOwnPropertyNames(src)
|
||||
.filter((key) => !ignoredKeys.includes(key))
|
||||
.map((key) => {
|
||||
target[key] = {};
|
||||
return [src[key], target[key]];
|
||||
}),
|
||||
);
|
||||
if (type === "object") {
|
||||
const prototype = Object.getPrototypeOf(src);
|
||||
if (prototype !== baseObjPrototype) {
|
||||
queue.push(
|
||||
...Object.getOwnPropertyNames(prototype)
|
||||
.filter((key) => !ignoredKeys.includes(key))
|
||||
.map((key) => {
|
||||
target[key] = {};
|
||||
return [src[key], target[key]];
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
iteration_count++;
|
||||
}
|
||||
} catch (e) {
|
||||
log.debug("Unknown depth", e);
|
||||
}
|
||||
return defs;
|
||||
}
|
||||
|
|
@ -9,6 +9,5 @@ export enum WorkerErrorTypes {
|
|||
|
||||
export interface WorkerRequest<TData, TActions> {
|
||||
method: TActions;
|
||||
requestData: TData;
|
||||
requestId: string;
|
||||
data: TData;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,6 +28,8 @@ import { watchJSActionSagas } from "sagas/JSActionSagas";
|
|||
import selectionCanvasSagas from "../src/sagas/CanvasSagas/SelectionCanvasSagas";
|
||||
import draggingCanvasSagas from "../src/sagas/CanvasSagas/DraggingCanvasSagas";
|
||||
import formEvaluationChangeListener from "../src/sagas/FormEvaluationSaga";
|
||||
import LintingSaga from "../src/sagas/LintingSagas";
|
||||
import JSLibrarySaga from "../src/sagas/JSLibrarySaga";
|
||||
|
||||
export const sagasToRunForTests = [
|
||||
initSagas,
|
||||
|
|
@ -60,4 +62,6 @@ export const sagasToRunForTests = [
|
|||
watchJSActionSagas,
|
||||
selectionCanvasSagas,
|
||||
draggingCanvasSagas,
|
||||
LintingSaga,
|
||||
JSLibrarySaga,
|
||||
];
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
"extends": "./tsconfig.path.json",
|
||||
"compilerOptions": {
|
||||
"target": "ES6",
|
||||
"lib": ["DOM", "ES6", "DOM.Iterable", "ScriptHost", "ES2016.Array.Include", "es2020.string", "esnext"],
|
||||
"lib": ["DOM", "ES6", "DOM.Iterable", "ScriptHost", "ES2016.Array.Include", "es2020.string", "esnext", "WebWorker"],
|
||||
"strict": true,
|
||||
"outDir": "./out/js/src",
|
||||
"allowJs": true,
|
||||
|
|
|
|||
|
|
@ -5767,7 +5767,7 @@ css-select-base-adapter@^0.1.1:
|
|||
version "0.1.1"
|
||||
resolved "https://registry.npmjs.org/css-select-base-adapter/-/css-select-base-adapter-0.1.1.tgz"
|
||||
|
||||
css-select@4.1.3, css-select@^2.0.0, css-select@^4.1.3:
|
||||
css-select@4.1.3, css-select@^2.0.0, css-select@^4.1.3, css-select@^5.1.0:
|
||||
version "4.1.3"
|
||||
resolved "https://registry.yarnpkg.com/css-select/-/css-select-4.1.3.tgz#a70440f70317f2669118ad74ff105e65849c7067"
|
||||
integrity sha512-gT3wBNd9Nj49rAbmtFHj1cljIAOLYSX1nZ8CB7TBO3INYckygm5B7LISU/szY//YmdiSLbJvDLOx9VnMVpMBxA==
|
||||
|
|
@ -5904,6 +5904,11 @@ cssom@^0.4.4:
|
|||
version "0.4.4"
|
||||
resolved "https://registry.npmjs.org/cssom/-/cssom-0.4.4.tgz"
|
||||
|
||||
cssom@^0.5.0:
|
||||
version "0.5.0"
|
||||
resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.5.0.tgz#d254fa92cd8b6fbd83811b9fbaed34663cc17c36"
|
||||
integrity sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw==
|
||||
|
||||
cssom@~0.3.6:
|
||||
version "0.3.8"
|
||||
resolved "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz"
|
||||
|
|
@ -6365,6 +6370,15 @@ dom-serializer@^1.0.1:
|
|||
domhandler "^4.2.0"
|
||||
entities "^2.0.0"
|
||||
|
||||
dom-serializer@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-2.0.0.tgz#e41b802e1eedf9f6cae183ce5e622d789d7d8e53"
|
||||
integrity sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==
|
||||
dependencies:
|
||||
domelementtype "^2.3.0"
|
||||
domhandler "^5.0.2"
|
||||
entities "^4.2.0"
|
||||
|
||||
dom4@^2.1.5:
|
||||
version "2.1.5"
|
||||
resolved "https://registry.npmjs.org/dom4/-/dom4-2.1.5.tgz"
|
||||
|
|
@ -6377,7 +6391,7 @@ domelementtype@^2.0.1:
|
|||
version "2.0.2"
|
||||
resolved "https://registry.npmjs.org/domelementtype/-/domelementtype-2.0.2.tgz"
|
||||
|
||||
domelementtype@^2.2.0:
|
||||
domelementtype@^2.2.0, domelementtype@^2.3.0:
|
||||
version "2.3.0"
|
||||
resolved "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz"
|
||||
integrity sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==
|
||||
|
|
@ -6401,6 +6415,13 @@ domhandler@^4.0.0, domhandler@^4.2.0:
|
|||
dependencies:
|
||||
domelementtype "^2.2.0"
|
||||
|
||||
domhandler@^5.0.1, domhandler@^5.0.2:
|
||||
version "5.0.3"
|
||||
resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-5.0.3.tgz#cc385f7f751f1d1fc650c21374804254538c7d31"
|
||||
integrity sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==
|
||||
dependencies:
|
||||
domelementtype "^2.3.0"
|
||||
|
||||
domutils@1.5:
|
||||
version "1.5.1"
|
||||
resolved "https://registry.npmjs.org/domutils/-/domutils-1.5.1.tgz"
|
||||
|
|
@ -6417,6 +6438,15 @@ domutils@^2.5.2, domutils@^2.6.0:
|
|||
domelementtype "^2.2.0"
|
||||
domhandler "^4.2.0"
|
||||
|
||||
domutils@^3.0.1:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/domutils/-/domutils-3.0.1.tgz#696b3875238338cb186b6c0612bd4901c89a4f1c"
|
||||
integrity sha512-z08c1l761iKhDFtfXO04C7kTdPBLi41zwOZl00WS8b5eiaebNpY00HKbztwBq+e3vyqWNwWF3mP9YLUeqIrF+Q==
|
||||
dependencies:
|
||||
dom-serializer "^2.0.0"
|
||||
domelementtype "^2.3.0"
|
||||
domhandler "^5.0.1"
|
||||
|
||||
dot-case@^3.0.4:
|
||||
version "3.0.4"
|
||||
resolved "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz"
|
||||
|
|
@ -6611,6 +6641,11 @@ entities@^2.0.0:
|
|||
version "2.0.3"
|
||||
resolved "https://registry.npmjs.org/entities/-/entities-2.0.3.tgz"
|
||||
|
||||
entities@^4.2.0, entities@^4.3.0:
|
||||
version "4.4.0"
|
||||
resolved "https://registry.yarnpkg.com/entities/-/entities-4.4.0.tgz#97bdaba170339446495e653cfd2db78962900174"
|
||||
integrity sha512-oYp7156SP8LkeGD0GF85ad1X9Ai79WtRsZ2gxJqtBuzH+98YUV6jkHEKlZkMbcrjJjIVJNIDP/3WL9wQkoPbWA==
|
||||
|
||||
errno@^0.1.3:
|
||||
version "0.1.7"
|
||||
resolved "https://registry.npmjs.org/errno/-/errno-0.1.7.tgz"
|
||||
|
|
@ -8255,6 +8290,11 @@ html-escaper@^2.0.0:
|
|||
version "2.0.2"
|
||||
resolved "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz"
|
||||
|
||||
html-escaper@^3.0.3:
|
||||
version "3.0.3"
|
||||
resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-3.0.3.tgz#4d336674652beb1dcbc29ef6b6ba7f6be6fdfed6"
|
||||
integrity sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ==
|
||||
|
||||
html-minifier-terser@^6.0.2:
|
||||
version "6.1.0"
|
||||
resolved "https://registry.npmjs.org/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz"
|
||||
|
|
@ -8299,6 +8339,16 @@ htmlparser2@^6.1.0:
|
|||
domutils "^2.5.2"
|
||||
entities "^2.0.0"
|
||||
|
||||
htmlparser2@^8.0.1:
|
||||
version "8.0.1"
|
||||
resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-8.0.1.tgz#abaa985474fcefe269bc761a779b544d7196d010"
|
||||
integrity sha512-4lVbmc1diZC7GUJQtRQ5yBAeUCL1exyMwmForWkRLnwyzWBFxN633SALPMGYaWZvKe9j1pRZJpauvmxENSp/EA==
|
||||
dependencies:
|
||||
domelementtype "^2.3.0"
|
||||
domhandler "^5.0.2"
|
||||
domutils "^3.0.1"
|
||||
entities "^4.3.0"
|
||||
|
||||
http-deceiver@^1.2.7:
|
||||
version "1.2.7"
|
||||
resolved "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz"
|
||||
|
|
@ -9860,6 +9910,17 @@ lines-and-columns@^1.1.6:
|
|||
version "1.1.6"
|
||||
resolved "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.1.6.tgz"
|
||||
|
||||
linkedom@^0.14.20:
|
||||
version "0.14.20"
|
||||
resolved "https://registry.yarnpkg.com/linkedom/-/linkedom-0.14.20.tgz#4dd4418941507326ef7cc325e8e049853481dbb9"
|
||||
integrity sha512-H7BX22kn4Ul4Mfr5/Jz039TgfsYce/YCvQ6272LEIlIJ1sYmU3R6yFNSYZU6iDX2aoF76wX+qjcSZEaLwumcAw==
|
||||
dependencies:
|
||||
css-select "^5.1.0"
|
||||
cssom "^0.5.0"
|
||||
html-escaper "^3.0.3"
|
||||
htmlparser2 "^8.0.1"
|
||||
uhyphen "^0.1.0"
|
||||
|
||||
lint-staged@^13.0.3:
|
||||
version "13.0.3"
|
||||
resolved "https://registry.yarnpkg.com/lint-staged/-/lint-staged-13.0.3.tgz#d7cdf03a3830b327a2b63c6aec953d71d9dc48c6"
|
||||
|
|
@ -14806,6 +14867,11 @@ uglify-js@^3.1.4:
|
|||
version "3.13.4"
|
||||
resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.13.4.tgz#592588bb9f47ae03b24916e2471218d914955574"
|
||||
|
||||
uhyphen@^0.1.0:
|
||||
version "0.1.0"
|
||||
resolved "https://registry.yarnpkg.com/uhyphen/-/uhyphen-0.1.0.tgz#3cc22afa790daa802b9f6789f3583108d5b4a08c"
|
||||
integrity sha512-o0QVGuFg24FK765Qdd5kk0zU/U4dEsCtN/GSiwNI9i8xsSVtjIAOdTaVhLwZ1nrbWxFVMxNDDl+9fednsOMsBw==
|
||||
|
||||
unbox-primitive@^1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.2.tgz#29032021057d5e6cdbd08c5129c226dff8ed6f9e"
|
||||
|
|
|
|||
|
|
@ -5,14 +5,14 @@
|
|||
$ cd app/client
|
||||
$ yarn install myLibrary
|
||||
```
|
||||
2. In the file `app/client/src/utils/DynamicBindingUtils.ts` find the const `extraLibraries` and add details about your library in the codebase
|
||||
2. In the file `app/client/src/worker/common/JSLibrary/index.ts` find the const `defaultLibraries` and add details about your library in the codebase
|
||||
```
|
||||
import myLibrary from "myLibrary";
|
||||
|
||||
...
|
||||
...
|
||||
|
||||
const extraLibraries = [
|
||||
const defaultLibraries = [
|
||||
...
|
||||
{
|
||||
accessor: "myLibrary",// The namespace for access
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user