Merge pull request #8124 from ToolJet/merge/appDef-to-develop

Merge main back to develop (v2.24.0)
This commit is contained in:
Kavin Venkatachalam 2023-11-08 12:02:48 +05:30 committed by GitHub
commit 019ac15a22
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
117 changed files with 8568 additions and 3595 deletions

View file

@ -1 +1 @@
2.23.0
2.24.0

View file

@ -19,9 +19,9 @@ module.exports = defineConfig({
trashAssetsBeforeRuns: true,
e2e: {
setupNodeEvents(on, config) {
setupNodeEvents (on, config) {
on("task", {
readPdf(pathToPdf) {
readPdf (pathToPdf) {
return new Promise((resolve) => {
const pdfPath = path.resolve(pathToPdf);
let dataBuffer = fs.readFileSync(pdfPath);
@ -33,7 +33,7 @@ module.exports = defineConfig({
});
on("task", {
readXlsx(filePath) {
readXlsx (filePath) {
return new Promise((resolve, reject) => {
try {
let dataBuffer = fs.readFileSync(filePath);
@ -48,7 +48,7 @@ module.exports = defineConfig({
});
on("task", {
deleteFolder(folderName) {
deleteFolder (folderName) {
return new Promise((resolve, reject) => {
if (fs.existsSync(folderName)) {
rmdir(folderName, { maxRetries: 10, recursive: true }, (err) => {
@ -66,7 +66,7 @@ module.exports = defineConfig({
});
on("task", {
updateId({ dbconfig, sql }) {
updateId ({ dbconfig, sql }) {
const client = new pg.Pool(dbconfig);
return client.query(sql);
},
@ -80,7 +80,7 @@ module.exports = defineConfig({
specPattern: "cypress/e2e/**/*.cy.js",
downloadsFolder: "cypress/downloads",
numTestsKeptInMemory: 0,
redirectionLimit: 7,
redirectionLimit: 10,
experimentalRunAllSpecs: true,
trashAssetsBeforeRuns: true,
experimentalMemoryManagement: true,

View file

@ -163,3 +163,21 @@ Cypress.Commands.add("apiCreateWorkspace", (workspaceName, workspaceSlug) => {
});
});
});
Cypress.Commands.add("logoutApi", () => {
cy.getCookie("tj_auth_token").then((cookie) => {
cy.request(
{
method: "GET",
url: "http://localhost:3000/api/logout",
headers: {
"Tj-Workspace-Id": Cypress.env("workspaceId"),
Cookie: `tj_auth_token=${cookie.value}`,
},
},
{ log: false }
).then((response) => {
expect(response.status).to.equal(200);
});
});
});

View file

@ -108,18 +108,23 @@ Cypress.Commands.add(
.find("pre.CodeMirror-line")
.invoke("text")
.then((text) => {
cy.wrap(subject).type(createBackspaceText(text), { delay: 0 }),
cy
.wrap(subject)
.last()
.click()
.type(createBackspaceText(text), { delay: 0 }),
{
delay: 0,
};
});
if (!Array.isArray(value)) {
cy.wrap(subject).type(value, {
cy.wrap(subject).last().type(value, {
parseSpecialCharSequences: false,
delay: 0,
});
} else {
cy.wrap(subject)
.last()
.type(value[1], {
parseSpecialCharSequences: false,
delay: 0,

View file

@ -3,7 +3,7 @@ export const workspaceConstantsText = {
"To resolve a Workspace constant use {{constants.access_token}}",
emptyStateHeader: "No Workspace constants yet",
emptyStateText:
"Use Workspace constants seamlessly in both the app builder and global data source connections across ToolJet.",
"Use workspace constants seamlessly in both the app builder and data source connections across ToolJet.",
addNewConstantButton: "Create new constant",
addConstatntText: "Add new constant in production ",
constantCreatedToast: "Constant has been created",

View file

@ -0,0 +1,39 @@
import { commonSelectors, commonWidgetSelector } from "Selectors/common";
import { fake } from "Fixtures/fake";
import { logout, navigateToAppEditor, verifyTooltip, releaseApp } from "Support/utils/common";
import { commonText } from "Texts/common";
import { addNewUserMW } from "Support/utils/userPermissions";
import { userSignUp } from "Support/utils/onboarding";
describe("App share functionality", () => {
const data = {};
data.appName = `${fake.companyName} App`;
data.firstName = fake.firstName;
data.lastName = fake.lastName.replaceAll("[^A-Za-z]", "");
data.email = fake.email.toLowerCase();
const slug = data.appName.toLowerCase().replace(/\s+/g, "-");
const firstUserEmail = data.email
const envVar = Cypress.env("environment");
// beforeEach(() => {
// cy.appUILogin();
// });
before(() => {
cy.apiLogin();
cy.apiCreateApp(data.appName);
// cy.visit('/')
// logout();
})
it("", () => {
cy.openApp(data.appName);
cy.get('[data-cy="left-sidebar-settings-button"]').click();
cy.get('[data-cy="app-slug-label"]').verifyVisibleElement("have.text", "Unique app slug");
cy.get('[data-cy="app-slug-input-field"]').verifyVisibleElement("have.value", Cypress.env("appId"));
cy.get('[data-cy="app-slug-info-label"]').verifyVisibleElement("have.text", "URL-friendly 'slug' consists of lowercase letters, numbers, and hyphens");
cy.get('[data-cy="app-link-label"]').verifyVisibleElement("have.text", "App link");
cy.get('[data-cy="app-link-field"]').verifyVisibleElement("have.text", `http://localhost:8082/my-workspace/apps/${Cypress.env("appId")}`)
})
});

View file

@ -37,12 +37,18 @@ describe("App Version Functionality", () => {
let currentVersion = "";
let newVersion = [];
let versionFrom = "";
beforeEach(() => {
cy.appUILogin();
before(() => {
cy.apiLogin();
cy.apiCreateApp(data.appName);
cy.logoutApi();
});
beforeEach(() => {
cy.apiLogin();
cy.visit('/my-workspace')
})
it("Verify the elements of the version module", () => {
cy.createApp(data.appName);
navigateToAppEditor(data.appName);
cy.get(appVersionSelectors.appVersionLabel).should("be.visible");
cy.get(commonSelectors.appNameInput).verifyVisibleElement(
"have.value",
@ -103,6 +109,6 @@ describe("App Version Functionality", () => {
createNewVersion((newVersion = ["v6"]), (versionFrom = "v3"));
verifyVersionAfterPreview((currentVersion = "v6"));
cy.go("back");
});
});

View file

@ -77,27 +77,37 @@ describe("Editor- Inspector", () => {
cy.get(multipageSelector.sidebarPageButton).click();
addNewPage("test_page");
cy.dragAndDropWidget("Button", 100, 200);
cy.dragAndDropWidget("Button", 500, 500);
selectEvent("On click", "Switch page");
cy.get('[data-cy="switch-page-label-and-input"] > .select-search')
.click()
.type("home{enter}");
cy.get('[data-cy="button-add-query-param"]').click();
cy.wait(1000);
cy.get('[data-cy="button-add-query-param"]').click();
addSupportCSAData("query-param-key", "key");
addSupportCSAData("query-param-value", "value");
cy.get('[data-cy="switch-page-label-and-input"] > .select-search')
.click()
.type("home{enter}");
cy.get('[data-cy="real-canvas"]').click("topRight", { force: true });
cy.dragAndDropWidget("Button", 100, 300);
cy.dragAndDropWidget("Button", 500, 300);
selectEvent("On click", "Set variable");
addSupportCSAData("key", "globalVar");
addSupportCSAData("variable", "globalVar");
cy.forceClickOnCanvas();
cy.waitForAutoSave();
cy.get(commonWidgetSelector.draggableWidget("button2")).click();
cy.get('[data-cy="real-canvas"]').click("topRight", { force: true });
cy.dragAndDropWidget("Button", 100, 400);
cy.dragAndDropWidget("Button", 500, 400);
selectEvent("On click", "Set page variable");
addSupportCSAData("key", "pageVar");
addSupportCSAData("variable", "pageVar");
cy.forceClickOnCanvas();
cy.waitForAutoSave();
cy.get(commonWidgetSelector.draggableWidget("button3")).click();
cy.get(commonWidgetSelector.sidebarinspector).click();
@ -147,7 +157,7 @@ describe("Editor- Inspector", () => {
});
});
cy.dragAndDropWidget("Button", 100, 300);
cy.dragAndDropWidget("Button", 500, 300);
cy.get(commonWidgetSelector.sidebarinspector).click();
openNode("components");
cy.get(`[data-cy="inspector-node-button1"] > .mx-1`).realHover();

View file

@ -34,7 +34,7 @@ import {
describe("Editor- Test Button widget", () => {
beforeEach(() => {
cy.apiLogin();
cy.apiCreateApp();
cy.apiCreateApp(`${fake.companyName}-App`);
cy.openApp();
cy.dragAndDropWidget(buttonText.defaultWidgetText, 500, 500);
});
@ -76,6 +76,8 @@ describe("Editor- Test Button widget", () => {
openEditorSidebar(data.widgetName);
openAccordion(commonWidgetText.accordionEvents);
addDefaultEventHandler(data.alertMessage);
cy.forceClickOnCanvas();
cy.waitForAutoSave();
cy.get(commonWidgetSelector.draggableWidget(data.widgetName)).click();
cy.verifyToastMessage(commonSelectors.toastMessage, data.alertMessage);
@ -341,6 +343,7 @@ describe("Editor- Test Button widget", () => {
});
it("Should verify csa", () => {
cy.get('[data-tooltip-content="Hide query panel"]').click();
// cy.dragAndDropWidget(buttonText.defaultWidgetText);
selectEvent("On click", "Show alert");
@ -359,7 +362,8 @@ describe("Editor- Test Button widget", () => {
cy.dragAndDropWidget(buttonText.defaultWidgetText, 500, 150);
selectEvent("On click", "Control Component");
selectCSA("button1", "Disable");
cy.get('[data-cy="Value-toggle-button"]').click();
cy.get('[data-cy="Value-fx-button"]').realClick();
cy.get('[data-cy="Value-input-field"]').clearAndTypeOnCodeMirror(`{{true`);
cy.get('[data-cy="real-canvas"]').click("topRight", { force: true });
cy.dragAndDropWidget(buttonText.defaultWidgetText, 500, 200);
@ -370,7 +374,9 @@ describe("Editor- Test Button widget", () => {
cy.dragAndDropWidget(buttonText.defaultWidgetText, 500, 250);
selectEvent("On click", "Control Component");
selectCSA("button1", "Loading");
cy.get('[data-cy="Value-toggle-button"]').click();
cy.wait(500);
cy.get('[data-cy="Value-fx-button"]').realClick();
cy.get('[data-cy="Value-input-field"]').clearAndTypeOnCodeMirror(`{{true`);
cy.get(commonWidgetSelector.draggableWidget("textinput1")).type("testBtn");
cy.wait(500);

View file

@ -21,6 +21,7 @@ describe("Editor- CSA", () => {
cy.apiLogin();
cy.apiCreateApp(appName1);
cy.openApp();
cy.get('[data-tooltip-content="Hide query panel"]').click();
});
afterEach(() => {
@ -38,6 +39,8 @@ describe("Editor- CSA", () => {
selectEvent("On click", "Control Component");
selectCSA("tabs1", "Set current tab");
addSupportCSAData("Id", "2");
cy.forceClickOnCanvas();
cy.waitForAutoSave();
cy.get(commonWidgetSelector.draggableWidget("button1")).click();
cy.get(".nav-link").eq(0).verifyVisibleElement("not.have.class", "active");
@ -69,7 +72,14 @@ describe("Editor- CSA", () => {
cy.get('[data-cy="draggable-widget-numberinput1"]')
.click()
.type(`{selectAll}{backspace}30{enter}`);
cy.wait(200);
cy.forceClickOnCanvas();
cy.waitForAutoSave();
cy.get(commonWidgetSelector.draggableWidget("button2")).click();
cy.wait(200);
cy.get(commonWidgetSelector.draggableWidget("button2")).click();
cy.verifyToastMessage(
commonSelectors.toastMessage,
"Form submitted successfully"
@ -79,6 +89,8 @@ describe("Editor- CSA", () => {
cy.get('[data-cy="draggable-widget-numberinput1"]')
.click()
.type(`{selectAll}{backspace}20{enter}`);
cy.forceClickOnCanvas();
cy.waitForAutoSave();
cy.get(commonWidgetSelector.draggableWidget("button3")).click();
cy.get('[data-cy="draggable-widget-numberinput1"]').should(
"have.value",
@ -98,7 +110,8 @@ describe("Editor- CSA", () => {
selectEvent("On click", "Control Component");
selectCSA("dropdown1", "Select option");
addSupportCSAData("Select", "{{3");
cy.forceClickOnCanvas();
cy.waitForAutoSave();
cy.get(commonWidgetSelector.draggableWidget("button1")).click();
cy.get(
'[data-cy="draggable-widget-dropdown1"] .css-1qrxvr1-singleValue'
@ -130,6 +143,8 @@ describe("Editor- CSA", () => {
cy.get(commonWidgetSelector.draggableWidget("textarea1"))
.should("be.visible")
.and("have.text", "New Text");
cy.forceClickOnCanvas();
cy.waitForAutoSave();
cy.get(commonWidgetSelector.draggableWidget("button2")).click();
cy.get(commonWidgetSelector.draggableWidget("textarea1"))
@ -182,10 +197,13 @@ describe("Editor- CSA", () => {
cy.dragAndDropWidget("Button", 500, 300);
selectEvent("On click", "Control Component");
selectCSA("icon1", "Set Visibility");
cy.get('[data-cy="Value-toggle-button"]').click();
cy.get('[data-cy="Value-toggle-button"]')
.should("be.visible")
.and("not.be.checked");
cy.get('[data-cy="Value-fx-button"]').click();
cy.get('[data-cy="Value-input-field"]').clearAndTypeOnCodeMirror("{{false");
// cy.get('[data-cy="Value-toggle-button"]')
// .should("be.visible")
// .and("not.be.checked");
cy.forceClickOnCanvas();
cy.waitForAutoSave();
cy.get(commonWidgetSelector.draggableWidget("button1")).click();
cy.verifyToastMessage(
@ -200,7 +218,7 @@ describe("Editor- CSA", () => {
cy.get('[data-cy="draggable-widget-icon1"]').should("not.be.visible");
});
it("Should verify Kanban CSA", () => {
it.only("Should verify Kanban CSA", () => {
cy.viewport(1400, 1900);
cy.dragAndDropWidget("Kanban", 50, 400);

View file

@ -35,7 +35,7 @@ import {
describe("List view widget", () => {
beforeEach(() => {
cy.apiLogin();
cy.apiCreateApp();
cy.apiCreateApp(`${fake.companyName}-App`);
cy.openApp();
cy.viewport(1200, 1200);
cy.dragAndDropWidget("List View", 50, 500);
@ -164,6 +164,8 @@ describe("List view widget", () => {
)
);
cy.get(commonWidgetSelector.buttonCloseEditorSideBar).click();
cy.forceClickOnCanvas();
cy.waitForAutoSave();
cy.get(`[data-cy=${data.widgetName.toLowerCase()}-row-1]`).click();
cy.verifyToastMessage(commonSelectors.toastMessage, data.marks[1]);

View file

@ -275,6 +275,10 @@ describe("Table", () => {
"have.text",
"Button Position"
); // dropdown_type
cy.forceClickOnCanvas();
cy.waitForAutoSave();
openEditorSidebar(data.widgetName);
cy.get('[data-cy="pages-name-fakename1"]').click();
cy.get('[data-cy="rightActions-cell-2"]')
.eq(0)
@ -291,6 +295,9 @@ describe("Table", () => {
);
cy.get('[data-cy="add-event-handler"]').eq(1).click();
cy.waitForAutoSave();
openEditorSidebar(data.widgetName);
cy.get('[data-cy="pages-name-fakename1"]').click();
cy.get('[data-cy="leftActions-cell-0"]').eq(0).find("button").click();
cy.verifyToastMessage(commonSelectors.toastMessage, "Hello world!");
openEditorSidebar(data.widgetName);
@ -1098,7 +1105,7 @@ describe("Table", () => {
verifyNodeData(tableText.defaultWidgetName, "Object", "22 entries ");
cy.wait(1000);
openNode(tableText.defaultWidgetName, 0, 1);
openNode(tableText.defaultWidgetName, 0, 1);
// openNode(tableText.defaultWidgetName, 0, 1);
verifyNodeData("newRows", "Array", "1 item ");
openNode("newRows");
verifyNodeData("0", "Object", "3 entries ");

View file

@ -50,15 +50,20 @@ describe("App Import Functionality", () => {
);
}
});
cy.get(importSelectors.importOptionInput).selectFile(toolJetImage, {
cy.get(importSelectors.importOptionInput).eq(0).selectFile(toolJetImage, {
force: true,
});
cy.verifyToastMessage(
commonSelectors.toastMessage,
importText.couldNotImportAppToastMessage
);
cy.get(importSelectors.importOptionInput).selectFile(appFile, {
cy.reload();
cy.get(importSelectors.dropDownMenu).should("be.visible").click();
cy.get(importSelectors.importOptionLabel).verifyVisibleElement(
"have.text",
importText.importOption
);
cy.get(importSelectors.importOptionInput).eq(0).selectFile(appFile, {
force: true,
});
cy.get('[data-cy="import-app-title"]').should("be.visible");
@ -72,6 +77,7 @@ describe("App Import Functionality", () => {
"contain.value",
appData.name.toLowerCase()
);
cy.skipEditorPopover();
cy.modifyCanvasSize(900, 600);
cy.dragAndDropWidget(buttonText.defaultWidgetText);
cy.get(appVersionSelectors.appVersionLabel).should("be.visible");

View file

@ -57,7 +57,9 @@ describe("App share functionality", () => {
cy.get(commonSelectors.editorPageLogo).click();
logout();
cy.wait(2500);
cy.visit(`/applications/${slug}`);
cy.wait(2500);
cy.get(commonSelectors.loginButton).should("be.visible");
@ -76,21 +78,34 @@ describe("App share functionality", () => {
cy.get(commonSelectors.editorPageLogo).click();
logout();
cy.wait(2500);
cy.visit(`/applications/${slug}`);
cy.wait(500);
cy.wait(2500);
cy.get('[data-cy="draggable-widget-table1"]').should("be.visible");
});
it("Verify app private and public app visibility for the same workspace user", () => {
addNewUserMW(data.firstName, data.email);
navigateToAppEditor(data.appName);
cy.wait(2000);
cy.get(commonWidgetSelector.shareAppButton).click();
cy.get("body").then(($el) => {
if (!$el.text().includes("Embedded app link", { timeout: 2000 })) {
cy.get(commonWidgetSelector.makePublicAppToggle).check();
}
});
cy.get(commonWidgetSelector.modalCloseButton).click();
cy.get(commonSelectors.editorPageLogo).click();
addNewUserMW(data.firstName, data.email);
logout();
cy.visit(`/applications/${slug}`);
cy.get('[data-cy="draggable-widget-table1"]').should("be.visible");
cy.appUILogin();
navigateToAppEditor(data.appName);
cy.skipEditorPopover()
cy.wait(2000);
cy.skipEditorPopover();
cy.get(commonWidgetSelector.shareAppButton).click();
cy.get(commonWidgetSelector.makePublicAppToggle).uncheck();
cy.get(commonWidgetSelector.modalCloseButton).click();
@ -119,6 +134,10 @@ describe("App share functionality", () => {
cy.clearAndType(commonSelectors.passwordInputField, "password");
cy.get(commonSelectors.signInButton).click();
cy.wait(1000);
cy.get(`[data-cy="workspace-sign-in-sub-header"]`).verifyVisibleElement(
"have.text",
"Sign in to your workspace - My workspace"
);
cy.visit("/");
cy.wait(2000);
@ -126,6 +145,7 @@ describe("App share functionality", () => {
cy.appUILogin();
navigateToAppEditor(data.appName);
cy.wait(2000);
cy.skipEditorPopover();
cy.get(commonWidgetSelector.shareAppButton).click();
cy.get(commonWidgetSelector.makePublicAppToggle).check();

View file

@ -21,21 +21,21 @@ describe("User permissions", () => {
cy.intercept("GET", "/api/apps?page=1&folder=&searchKey=").as("homePage");
cy.apiLogin();
cy.apiCreateApp(data.appName);
cy.visit('/')
cy.visit('/my-workspace')
permissions.reset();
cy.get(commonSelectors.homePageLogo).click();
cy.wait("@homePage");
permissions.addNewUserMW(data.firstName, data.email);
common.logout();
cy.logoutApi();
});
beforeEach(() => {
cy.appUILogin();
cy.visitTheWorkspace("My workspace");
cy.apiLogin();
cy.visit("/my-workspace");
});
it("Should verify the create new app permission", () => {
common.logout();
cy.login(data.email, usersText.password);
cy.logoutApi();
cy.apiLogin(data.email, usersText.password);
cy.get("body").then(($title) => {
if ($title.text().includes(dashboardText.emptyPageDescription)) {
cy.get(commonSelectors.dashboardAppCreateButton).should('be.disabled');
@ -43,7 +43,7 @@ describe("User permissions", () => {
cy.contains(dashboardText.createAppButton).should("not.exist");
}
});
common.logout();
cy.logoutApi();
});
it("Should verify the View and Edit permission", () => {
@ -59,7 +59,9 @@ describe("User permissions", () => {
});
common.logout();
cy.login(data.email, usersText.password);
cy.apiLogin(data.email, usersText.password);
cy.visit("/my-workspace");
cy.wait(500)
cy.contains(data.appName).should("exist");
cy.get(commonSelectors.appCard(data.appName)).should(
"contain.text",
@ -74,8 +76,11 @@ describe("User permissions", () => {
"tj-disabled-btn"
);
});
common.logout();
permissions.adminLogin();
cy.apiLogin();
cy.visit("/my-workspace");
common.navigateToManageGroups();
cy.contains("tr", data.appName)
.parent()
.within(() => {
@ -87,7 +92,9 @@ describe("User permissions", () => {
);
common.logout();
cy.login(data.email, usersText.password);
cy.apiLogin(data.email, usersText.password);
cy.visit("/my-workspace");
cy.wait(500)
cy.get(commonSelectors.appCard(data.appName)).should(
"contain.text",
data.appName
@ -114,9 +121,10 @@ describe("User permissions", () => {
it("Should verify the Create and Delete app permission", () => {
data.appName = `${fake.companyName}-App`;
cy.createApp(data.appName);
cy.get(commonSelectors.editorPageLogo).click();
cy.wait(1000);
cy.apiCreateApp(data.appName);
cy.visit('/my-workspace')
cy.wait(500);
common.navigateToManageGroups();
cy.get(groupsSelector.appSearchBox).click();
cy.get(groupsSelector.searchBoxOptions).contains(data.appName).click();
@ -133,18 +141,24 @@ describe("User permissions", () => {
cy.get(groupsSelector.permissionsLink).click();
cy.get(groupsSelector.appsDeleteCheck).check();
common.logout();
cy.login(data.email, usersText.password);
cy.logoutApi();
cy.apiLogin(data.email, usersText.password);
cy.visit('/my-workspace');
cy.get(commonSelectors.appCreateButton).should("exist");
common.viewAppCardOptions(data.appName);
cy.contains("Delete app").should("exist");
permissions.adminLogin();
common.logout();
cy.apiLogin();
cy.visit("/my-workspace");
common.navigateToManageGroups();
cy.get(groupsSelector.permissionsLink).click();
cy.get(groupsSelector.appsDeleteCheck).uncheck();
common.logout();
cy.login(data.email, usersText.password);
cy.logoutApi();
cy.apiLogin(data.email, usersText.password);
cy.visit("/my-workspace");
cy.wait(1000)
common.viewAppCardOptions(data.appName);
cy.contains("Delete app").should("not.exist");
@ -157,12 +171,17 @@ describe("User permissions", () => {
cy.get(commonSelectors.appCardOptions(commonText.deleteAppOption)).click();
cy.get(commonSelectors.buttonSelector("Yes")).click();
permissions.adminLogin();
common.logout
cy.apiLogin();
cy.visit("/my-workspace");
common.navigateToManageGroups();
cy.get(groupsSelector.permissionsLink).click();
cy.get(groupsSelector.appsCreateCheck).uncheck();
common.logout();
cy.login(data.email, usersText.password);
cy.logoutApi();
cy.apiLogin(data.email, usersText.password);
cy.visit("/my-workspace");
cy.wait(1000)
cy.contains("Create new application").should("not.exist");
});
@ -171,8 +190,10 @@ describe("User permissions", () => {
cy.get(groupsSelector.permissionsLink).click();
cy.get(groupsSelector.foldersCreateCheck).check();
common.logout();
cy.login(data.email, usersText.password);
cy.logoutApi();
cy.apiLogin(data.email, usersText.password);
cy.visit("/my-workspace");
cy.wait(500)
cy.get(commonSelectors.createNewFolderButton).click();
cy.clearAndType(commonSelectors.folderNameInput, data.folderName);
@ -188,15 +209,22 @@ describe("User permissions", () => {
});
cy.get(commonSelectors.deleteFolderOption(data.folderName)).click();
cy.get(commonSelectors.buttonSelector("Yes")).click();
common.logout();
permissions.adminLogin();
cy.apiLogin();
cy.visit("/my-workspace");
common.navigateToManageGroups();
cy.get(groupsSelector.permissionsLink).click();
cy.get(groupsSelector.foldersCreateCheck).uncheck();
common.logout();
cy.login(data.email, usersText.password);
cy.apiLogin(data.email, usersText.password);
cy.visit("/my-workspace");
cy.wait(500)
permissions.adminLogin();
cy.apiLogin();
cy.visit("/my-workspace");
common.navigateToManageGroups();
cy.contains("td", data.appName)
.parent()
.within(() => {
@ -204,7 +232,9 @@ describe("User permissions", () => {
});
common.logout();
cy.login(data.email, usersText.password);
cy.apiLogin(data.email, usersText.password);
cy.visit("/my-workspace");
cy.wait(500)
cy.contains(data.appName).should("not.exist");
common.logout();
@ -223,7 +253,9 @@ describe("User permissions", () => {
).verifyVisibleElement("have.text", "Go to workspace constants");
common.logout();
cy.login(data.email, usersText.password);
cy.apiLogin(data.email, usersText.password);
cy.visit("/my-workspace");
cy.wait(500)
common.navigateToWorkspaceVariable();
cy.get('[data-cy="alert-info-text"]>>.text-muted').verifyVisibleElement(
"have.text",

View file

@ -47,7 +47,9 @@ describe("Workspace constants", () => {
.click();
cy.get(commonSelectors.breadcrumbTitle).should(($el) => {
expect($el.contents().first().text().trim()).to.eq("Workspace settings");
expect($el.contents().first().text().trim()).to.eq(
"Workspace settings"
);
});
cy.get(commonSelectors.breadcrumbPageTitle).verifyVisibleElement(
"have.text",
@ -67,7 +69,9 @@ describe("Workspace constants", () => {
);
cy.get("body").then(($body) => {
if ($body.find(workspaceConstantsSelectors.emptyStateImage).length > 0) {
if (
$body.find(workspaceConstantsSelectors.emptyStateImage).length > 0
) {
cy.get(workspaceConstantsSelectors.emptyStateImage).should(
"be.visible"
);
@ -77,7 +81,9 @@ describe("Workspace constants", () => {
"have.text",
workspaceConstantsText.emptyStateHeader
);
cy.get(workspaceConstantsSelectors.emptyStateText).verifyVisibleElement(
cy.get(
workspaceConstantsSelectors.emptyStateText
).verifyVisibleElement(
"have.text",
workspaceConstantsText.emptyStateText
);
@ -94,7 +100,10 @@ describe("Workspace constants", () => {
"have.text",
workspaceConstantsText.addConstatntText
);
cy.get(commonSelectors.nameLabel).verifyVisibleElement("have.text", "Name");
cy.get(commonSelectors.nameLabel).verifyVisibleElement(
"have.text",
"Name"
);
cy.get(commonSelectors.nameInputField)
.invoke("attr", "placeholder")
.should("eq", "Enter Constant Name");
@ -111,11 +120,12 @@ describe("Workspace constants", () => {
"have.text",
"Cancel"
);
cy.get(workspaceConstantsSelectors.addConstantButton).verifyVisibleElement(
"have.text",
"Add constant"
cy.get(
workspaceConstantsSelectors.addConstantButton
).verifyVisibleElement("have.text", "Add constant");
cy.get(workspaceConstantsSelectors.addConstantButton).should(
"be.disabled"
);
cy.get(workspaceConstantsSelectors.addConstantButton).should("be.disabled");
contantsNameValidation(" ", commonText.constantsNameError);
contantsNameValidation("9", commonText.constantsNameError);
@ -134,13 +144,17 @@ describe("Workspace constants", () => {
"have.text",
commonText.constantsValueError
);
cy.get(workspaceConstantsSelectors.addConstantButton).should("be.disabled");
cy.get(workspaceConstantsSelectors.addConstantButton).should(
"be.disabled"
);
cy.get(commonSelectors.cancelButton).click();
cy.get(workspaceConstantsSelectors.addNewConstantButton).click();
cy.clearAndType(commonSelectors.nameInputField, data.constName);
cy.clearAndType(commonSelectors.valueInputField, data.constName);
cy.get(workspaceConstantsSelectors.addConstantButton).should("be.enabled");
cy.get(workspaceConstantsSelectors.addConstantButton).should(
"be.enabled"
);
cy.get(commonSelectors.cancelButton).click();
cy.get(workspaceConstantsSelectors.constantName(data.constName)).should(
"not.exist"
@ -189,14 +203,22 @@ describe("Workspace constants", () => {
).verifyVisibleElement("have.text", "Delete");
cy.get(commonSelectors.pagination).should("be.visible");
cy.get(workspaceConstantsSelectors.constEditButton(data.constName)).click();
cy.get(
workspaceConstantsSelectors.constEditButton(data.constName)
).click();
cy.get(workspaceConstantsSelectors.contantFormTitle).verifyVisibleElement(
"have.text",
"Update constant in production "
);
cy.get(commonSelectors.nameLabel).verifyVisibleElement("have.text", "Name");
cy.get(commonSelectors.nameInputField).should("have.value", data.constName);
cy.get(commonSelectors.nameLabel).verifyVisibleElement(
"have.text",
"Name"
);
cy.get(commonSelectors.nameInputField).should(
"have.value",
data.constName
);
cy.get(commonSelectors.nameInputField)
.should("be.visible")
.and("be.disabled");
@ -211,20 +233,25 @@ describe("Workspace constants", () => {
"have.text",
"Cancel"
);
cy.get(workspaceConstantsSelectors.addConstantButton).verifyVisibleElement(
"have.text",
"Update"
cy.get(
workspaceConstantsSelectors.addConstantButton
).verifyVisibleElement("have.text", "Update");
cy.get(workspaceConstantsSelectors.addConstantButton).should(
"be.disabled"
);
cy.get(workspaceConstantsSelectors.addConstantButton).should("be.disabled");
cy.clearAndType(commonSelectors.valueInputField, data.newConstvalue);
cy.get(workspaceConstantsSelectors.addConstantButton).should("be.enabled");
cy.get(workspaceConstantsSelectors.addConstantButton).should(
"be.enabled"
);
cy.get(commonSelectors.cancelButton).click();
cy.get(
workspaceConstantsSelectors.constantValue(data.constName)
).verifyVisibleElement("have.text", data.constName);
cy.get(workspaceConstantsSelectors.constEditButton(data.constName)).click();
cy.get(
workspaceConstantsSelectors.constEditButton(data.constName)
).click();
cy.clearAndType(commonSelectors.valueInputField, data.newConstvalue);
cy.get(workspaceConstantsSelectors.addConstantButton).click();
cy.verifyToastMessage(
@ -246,7 +273,10 @@ describe("Workspace constants", () => {
"have.text",
"Cancel"
);
cy.get(commonSelectors.yesButton).verifyVisibleElement("have.text", "Yes");
cy.get(commonSelectors.yesButton).verifyVisibleElement(
"have.text",
"Yes"
);
cy.get(commonSelectors.cancelButton).click();
cy.get(
workspaceConstantsSelectors.constantValue(data.constName)
@ -312,47 +342,31 @@ describe("Workspace constants", () => {
`[data-cy="inspector-node-${data.constantsName}"] > .mx-2`
).verifyVisibleElement("have.text", `"dJ_8Q~BcaMPd"`);
cy.get('[data-cy="button-release"]').click();
cy.get('[data-cy="yes-button"]').click();
cy.verifyToastMessage(commonSelectors.toastMessage, "Version v1 released");
if (envVar === "Community") {
cy.get('[data-cy="button-release"]').click();
cy.get('[data-cy="yes-button"]').click();
cy.verifyToastMessage(
commonSelectors.toastMessage,
"Version v1 released"
);
cy.get(commonWidgetSelector.shareAppButton).click();
cy.clearAndType(commonWidgetSelector.appNameSlugInput, `${data.slug}`);
cy.wait(1500);
cy.get(commonWidgetSelector.modalCloseButton).click();
cy.forceClickOnCanvas();
cy.waitForAutoSave();
cy.wait(500)
cy.openInCurrentTab(commonWidgetSelector.previewButton);
cy.wait(4000);
cy.get(commonWidgetSelector.shareAppButton).click();
cy.clearAndType(commonWidgetSelector.appNameSlugInput, `${data.slug}`);
cy.get(commonWidgetSelector.modalCloseButton).click();
cy.forceClickOnCanvas();
cy.waitForAutoSave();
cy.openInCurrentTab(commonWidgetSelector.previewButton);
cy.wait(4000);
cy.get(
commonWidgetSelector.draggableWidget(data.constantsName)
).verifyVisibleElement("have.text", "dJ_8Q~BcaMPd");
cy.get(
commonWidgetSelector.draggableWidget(data.constantsName)
).verifyVisibleElement("have.text", "dJ_8Q~BcaMPd");
cy.get('[data-cy="viewer-page-logo"]').click();
cy.wait("@homePage");
cy.visit(`/applications/${data.slug}`);
cy.wait(4000);
cy.get('[data-cy="viewer-page-logo"]').click();
cy.wait("@homePage");
cy.visit(`/applications/${data.slug}`);
cy.wait(4000);
cy.get(
commonWidgetSelector.draggableWidget(data.constantsName)
).verifyVisibleElement("have.text", "dJ_8Q~BcaMPd");
} else {
cy.forceClickOnCanvas();
cy.waitForAutoSave();
cy.openInCurrentTab(commonWidgetSelector.previewButton);
cy.wait(4000);
cy.get(
commonWidgetSelector.draggableWidget(data.constantsName)
).verifyVisibleElement("have.text", "dJ_8Q~BcaMPd");
cy.get('[data-cy="viewer-page-logo"]').click();
cy.wait("@homePage");
}
cy.get(
commonWidgetSelector.draggableWidget(data.constantsName)
).verifyVisibleElement("have.text", "dJ_8Q~BcaMPd");
});
});
});

View file

@ -22,7 +22,12 @@ export const verifyControlComponentAction = (widgetName, value) => {
cy.get(commonWidgetSelector.componentTextInput)
.find('[data-cy*="-input-field"]')
.clearAndTypeOnCodeMirror(value);
cy.forceClickOnCanvas();
cy.waitForAutoSave();
cy.get(commonWidgetSelector.draggableWidget(widgetName)).click();
cy.get(commonWidgetSelector.draggableWidget('textinput1')).should("have.value", value);
cy.get(commonWidgetSelector.draggableWidget("textinput1")).should(
"have.value",
value
);
};

View file

@ -151,5 +151,7 @@ export const verifyVersionAfterPreview = (currentVersion) => {
.click();
cy.url().should("include", "/home");
verifyComponent("button1");
cy.go("back");
cy.waitForAppLoad()
cy.contains(currentVersion);
};

1905
docs/package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -18,12 +18,15 @@
"@docusaurus/plugin-google-gtag": "^2.4.3",
"@docusaurus/plugin-sitemap": "^2.4.3",
"@docusaurus/preset-classic": "^2.4.3",
"@mdx-js/react": "^1.6.22",
"clsx": "^1.2.1",
"plugin-image-zoom": "github:flexanalytics/plugin-image-zoom",
"react": "^17.0.2",
"react-dom": "^17.0.2"
},
"overrides": {
"got": "^12.1.0",
"trim": "^0.0.3"
},
"browserslist": {
"production": [
">0.5%",

View file

@ -1 +1 @@
2.23.0
2.24.0

View file

@ -1,11 +1,41 @@
import React from 'react';
import React, { useEffect } from 'react';
import { withTranslation } from 'react-i18next';
import { Editor } from '../Editor/Editor';
import { RealtimeEditor } from '@/Editor/RealtimeEditor';
import config from 'config';
import { appService } from '@/_services';
import { useAppDataActions } from '@/_stores/appDataStore';
const AppLoaderComponent = (props) => {
return config.ENABLE_MULTIPLAYER_EDITING ? <RealtimeEditor {...props} /> : <Editor {...props} />;
};
const AppLoaderComponent = React.memo((props) => {
const [shouldLoadApp, setShouldLoadApp] = React.useState(false);
const { updateState } = useAppDataActions();
useEffect(() => {
props?.id && props?.slug && loadAppDetails(props?.id);
return () => {
setShouldLoadApp(false);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const loadAppDetails = (appId) => {
appService.fetchApp(appId, 'edit').then((data) => {
setShouldLoadApp(true);
updateState({
app: data,
appId: data.id,
});
});
};
if (!shouldLoadApp) return <></>;
return config.ENABLE_MULTIPLAYER_EDITING ? (
<RealtimeEditor {...props} shouldLoadApp={shouldLoadApp} />
) : (
<Editor {...props} />
);
});
export const AppLoader = withTranslation()(AppLoaderComponent);

View file

@ -17,6 +17,7 @@ export const CreateVersion = ({
}) => {
const [isCreatingVersion, setIsCreatingVersion] = useState(false);
const [versionName, setVersionName] = useState('');
const { t } = useTranslation();
const { editingVersion } = useAppVersionStore(
(state) => ({
@ -25,6 +26,14 @@ export const CreateVersion = ({
shallow
);
const options = appVersions.map((version) => {
return { label: version.name, value: version };
});
const [selectedVersion, setSelectedVersion] = useState(
() => options.find((option) => option?.value?.id === editingVersion?.id)?.value
);
const createVersion = () => {
if (versionName.trim().length > 25) {
toast.error('Version name should not be longer than 25 characters');
@ -36,18 +45,26 @@ export const CreateVersion = ({
}
setIsCreatingVersion(true);
appVersionService
.create(appId, versionName, editingVersion.id)
.then(() => {
.create(appId, versionName, selectedVersion.id)
.then((data) => {
toast.success('Version Created');
appVersionService.getAll(appId).then((data) => {
setVersionName('');
setIsCreatingVersion(false);
setAppVersions(data.versions);
const latestVersion = data.versions.at(0);
setAppDefinitionFromVersion(latestVersion);
setShowCreateAppVersion(false);
});
appVersionService
.getAppVersionData(appId, data.id)
.then((data) => {
setAppDefinitionFromVersion(data);
})
.catch((error) => {
toast.error(error);
});
})
.catch((error) => {
toast.error(error?.error);
@ -55,10 +72,6 @@ export const CreateVersion = ({
});
};
const options = appVersions.map((version) => {
return { label: version.name, value: version };
});
return (
<AlertDialog
show={showCreateAppVersion}
@ -101,9 +114,9 @@ export const CreateVersion = ({
<div className="ts-control" data-cy="create-version-from-input-field">
<Select
options={options}
defaultValue={options.find((option) => option?.value?.id === editingVersion?.id)}
value={selectedVersion}
onChange={(version) => {
setAppDefinitionFromVersion(version, false);
setSelectedVersion(version);
}}
useMenuPortal={false}
width="100%"

View file

@ -114,11 +114,14 @@ export const CustomSelect = ({ ...props }) => {
return (
<>
<CreateVersion
{...props}
showCreateAppVersion={showCreateAppVersion}
setShowCreateAppVersion={setShowCreateAppVersion}
/>
{showCreateAppVersion && (
<CreateVersion
{...props}
showCreateAppVersion={showCreateAppVersion}
setShowCreateAppVersion={setShowCreateAppVersion}
/>
)}
<EditVersion {...props} showEditAppVersion={showEditAppVersion} setShowEditAppVersion={setShowEditAppVersion} />
{/* When we merge this code to EE update the defaultAppEnvironments object with rest of default environments (then delete this comment)*/}
<ConfirmDialog

View file

@ -1,53 +1,53 @@
import React, { useState, useEffect } from 'react';
import React, { useEffect, useState } from 'react';
import cx from 'classnames';
import { appVersionService, appEnvironmentService } from '@/_services';
import { appVersionService } from '@/_services';
import { CustomSelect } from './CustomSelect';
import { toast } from 'react-hot-toast';
import { shallow } from 'zustand/shallow';
import { useAppVersionStore } from '@/_stores/appVersionStore';
export const AppVersionsManager = function ({
appId,
releasedVersionId,
setAppDefinitionFromVersion,
onVersionDelete,
}) {
const [appVersions, setAppVersions] = useState([]);
const [appVersionStatus, setGetAppVersionStatus] = useState('');
const appVersionLoadingStatus = Object.freeze({
loading: 'loading',
loaded: 'loaded',
error: 'error',
});
export const AppVersionsManager = function ({ appId, setAppDefinitionFromVersion, onVersionDelete }) {
const [appVersionStatus, setGetAppVersionStatus] = useState(appVersionLoadingStatus.loading);
const [deleteVersion, setDeleteVersion] = useState({
versionId: '',
versionName: '',
showModal: false,
});
const { editingVersion } = useAppVersionStore(
const { releasedVersionId, editingVersion, appVersions, setAppVersions } = useAppVersionStore(
(state) => ({
editingVersion: state.editingVersion,
appVersions: state.appVersions,
setAppVersions: state.actions?.setAppVersions,
releasedVersionId: state.releasedVersionId,
}),
shallow
);
const darkMode = localStorage.getItem('darkMode') === 'true';
useEffect(() => {
setGetAppVersionStatus('loading');
appEnvironmentService
.getVersionsByEnvironment(appId)
.then((data) => {
setAppVersions(data.appVersions);
setGetAppVersionStatus('success');
})
.catch((error) => {
toast.error(error);
setGetAppVersionStatus('failure');
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
if (appVersions && appVersions.length > 0) {
setGetAppVersionStatus(appVersionLoadingStatus.loaded);
}
return () => {
setGetAppVersionStatus(appVersionLoadingStatus.loading);
};
}, [appVersions]);
const darkMode = localStorage.getItem('darkMode') === 'true';
const selectVersion = (id) => {
appVersionService
.getOne(appId, id)
.getAppVersionData(appId, id)
.then((data) => {
setAppDefinitionFromVersion(data, true);
const isCurrentVersionReleased = data.currentVersionId ? true : false;
setAppDefinitionFromVersion(data, isCurrentVersionReleased);
})
.catch((error) => {
toast.error(error);
@ -67,18 +67,22 @@ export const AppVersionsManager = function ({
appVersionService
.del(appId, versionId)
.then(() => {
onVersionDelete();
toast.dismiss(deleteingToastId);
toast.success(`Version - ${versionName} Deleted`);
resetDeleteModal();
appVersionService.getAll(appId).then((data) => {
setAppVersions(data.versions);
});
setGetAppVersionStatus(appVersionLoadingStatus.loading);
})
.catch((error) => {
toast.dismiss(deleteingToastId);
toast.error(error?.error ?? 'Oops, something went wrong');
setGetAppVersionStatus(appVersionLoadingStatus.error);
resetDeleteModal();
})
.finally(() => {
appVersionService.getAll(appId, true).then((data) => {
setAppVersions(data.versions);
onVersionDelete();
});
});
};

View file

@ -67,6 +67,7 @@ import _ from 'lodash';
import { EditorContext } from '@/Editor/Context/EditorContextWrapper';
import { useTranslation } from 'react-i18next';
import { useCurrentState } from '@/_stores/currentStateStore';
import { useAppInfo } from '@/_stores/appDataStore';
import WidgetIcon from '@/../assets/images/icons/widgets';
const AllComponents = {
@ -164,6 +165,8 @@ export const Box = memo(
};
}
const { events } = useAppInfo();
const componentMeta = useMemo(() => {
return componentTypes.find((comp) => component.component === comp.component);
}, [component]);
@ -265,7 +268,10 @@ export const Box = memo(
if (mode === 'edit' && eventName === 'onClick') {
onComponentClick(id, component);
}
onEvent(eventName, { ...options, customVariables: { ...customResolvables }, component });
const componentEvents = events.filter((event) => event.sourceId === id);
onEvent(eventName, componentEvents, { ...options, customVariables: { ...customResolvables } });
};
const validate = (value) =>
validateWidget({

View file

@ -93,7 +93,7 @@ export function CodeHinter({
};
const currentState = useCurrentState();
const [realState, setRealState] = useState(currentState);
const [currentValue, setCurrentValue] = useState(initialValue);
const [currentValue, setCurrentValue] = useState('');
const [prevCurrentValue, setPrevCurrentValue] = useState(null);
const [resolvedValue, setResolvedValue] = useState(null);
@ -120,6 +120,17 @@ export function CodeHinter({
const { variablesExposedForPreview } = useContext(EditorContext);
const prevCountRef = useRef(false);
useEffect(() => {
setCurrentValue(initialValue);
return () => {
setPrevCurrentValue(null);
setResolvedValue(null);
setResolvingError(null);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
if (_currentState) {
setRealState(_currentState);
@ -127,7 +138,7 @@ export function CodeHinter({
setRealState(currentState);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [currentState.components, _currentState]);
}, [JSON.stringify({ currentState, _currentState })]);
useEffect(() => {
const handleClickOutside = (event) => {
@ -149,7 +160,7 @@ export function CodeHinter({
}, [wrapperRef, isFocused, isPreviewFocused, currentValue, prevCountRef, isOpen]);
useEffect(() => {
if (JSON.stringify(currentValue) !== JSON.stringify(prevCurrentValue)) {
if (enablePreview && isFocused && JSON.stringify(currentValue) !== JSON.stringify(prevCurrentValue)) {
const customResolvables = getCustomResolvables();
const [preview, error] = resolveReferences(currentValue, realState, null, customResolvables, true, true);
setPrevCurrentValue(currentValue);
@ -162,13 +173,8 @@ export function CodeHinter({
setResolvedValue(preview);
}
}
return () => {
setPrevCurrentValue(null);
setResolvedValue(null);
setResolvingError(null);
};
}, [JSON.stringify({ currentValue, realState })]);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [JSON.stringify({ currentValue, realState, isFocused })]);
function valueChanged(editor, onChange, ignoreBraces) {
if (editor.getValue()?.trim() !== currentValue) {

View file

@ -2,7 +2,7 @@ import ToggleGroup from '@/ToolJetUI/SwitchGroup/ToggleGroup';
import ToggleGroupItem from '@/ToolJetUI/SwitchGroup/ToggleGroupItem';
import React from 'react';
const ClientServerSwitch = ({ value, onChange, cyLabel, meta, paramName }) => {
const ClientServerSwitch = ({ value, onChange, meta }) => {
const options = meta?.options;
const defaultValue = value ? 'serverSide' : 'clientSide';
const handleChange = (_value) => {

View file

@ -81,7 +81,7 @@ export const Calendar = function ({
action,
};
fireEvent('onCalendarSlotSelect', { selectedSlots });
fireEvent('onCalendarSlotSelect', { component, selectedSlots });
};
function popoverClosed() {
@ -153,7 +153,7 @@ export const Calendar = function ({
min={startTime}
max={endTime}
onSelectEvent={(calendarEvent, e) => {
fireEvent('onCalendarEventSelect', { calendarEvent });
fireEvent('onCalendarEventSelect', { component, calendarEvent });
if (properties.showPopOverOnEventClick)
setEventPopoverOptions({
...eventPopoverOptions,

View file

@ -4,6 +4,7 @@ import { resolveWidgetFieldValue } from '@/_helpers/utils';
import { toast } from 'react-hot-toast';
import * as XLSX from 'xlsx/xlsx.mjs';
import { useCurrentState } from '@/_stores/currentStateStore';
import { useAppInfo } from '@/_stores/appDataStore';
export const FilePicker = ({
id,
@ -54,6 +55,10 @@ export const FilePicker = ({
const parsedWidgetVisibility =
typeof widgetVisibility !== 'boolean' ? resolveWidgetFieldValue(widgetVisibility, currentState) : widgetVisibility;
const { events: allAppEvents } = useAppInfo();
const filePickerEvents = allAppEvents.filter((event) => event.target === 'component' && event.sourceId === id);
const bgThemeColor = darkMode ? '#232E3C' : '#fff';
const baseStyle = {
@ -235,7 +240,7 @@ export const FilePicker = ({
onComponentOptionChanged(component, 'file', [], id);
}
if (acceptedFiles.length !== 0) {
if (acceptedFiles.length !== 0 && onEvent) {
const fileData = parsedEnableMultiple ? [...selectedFiles] : [];
if (parseContent) {
onComponentOptionChanged(component, 'isParsing', true, id);
@ -250,7 +255,8 @@ export const FilePicker = ({
});
setSelectedFiles(fileData);
onComponentOptionChanged(component, 'file', fileData, id);
onEvent('onFileSelected', { component })
onEvent('onFileSelected', filePickerEvents, { component })
.then(() => {
setAccepted(true);
// eslint-disable-next-line no-unused-vars
@ -263,7 +269,7 @@ export const FilePicker = ({
}, 600);
});
})
.then(() => onEvent('onFileLoaded', { component }));
.then(() => onEvent('onFileLoaded', filePickerEvents, { component }));
}
if (fileRejections.length > 0) {
@ -286,7 +292,7 @@ export const FilePicker = ({
copy.splice(index, 1);
return copy;
});
onEvent('onFileDeselected', { component });
onEvent('onFileDeselected', filePickerEvents);
};
useEffect(() => {

View file

@ -8,6 +8,7 @@ import { Box } from '@/Editor/Box';
import { generateUIComponents } from './FormUtils';
import { useMounted } from '@/_hooks/use-mount';
import { removeFunctionObjects } from '@/_helpers/appUtils';
import { useAppInfo } from '@/_stores/appDataStore';
export const Form = function Form(props) {
const {
id,
@ -28,6 +29,10 @@ export const Form = function Form(props) {
dataCy,
paramUpdated,
} = props;
const { events: allAppEvents } = useAppInfo();
const formEvents = allAppEvents.filter((event) => event.target === 'component' && event.sourceId === id);
const { visibility, disabledState, borderRadius, borderColor, boxShadow } = styles;
const { buttonToSubmit, loadingState, advanced, JSONSchema } = properties;
const backgroundColor =
@ -57,7 +62,7 @@ export const Form = function Form(props) {
});
setExposedVariable('submitForm', async function () {
if (isValid) {
onEvent('onSubmit', { component }).then(() => resetComponent());
onEvent('onSubmit', formEvents).then(() => resetComponent());
} else {
fireEvent('onInvalid');
}
@ -167,7 +172,7 @@ export const Form = function Form(props) {
};
const fireSubmissionEvent = () => {
if (isValid) {
onEvent('onSubmit', { component }).then(() => resetComponent());
onEvent('onSubmit', formEvents).then(() => resetComponent());
} else {
fireEvent('onInvalid');
}

View file

@ -26,11 +26,12 @@ export const Item = React.memo(
isFirstItem = false,
setShowModal = () => {},
cardDataAsObj = {},
setLastSelectedCard,
...props
},
ref
) => {
const { id, component, containerProps, fireEvent, setExposedVariable, darkMode } = kanbanProps;
const { id, component, containerProps, fireEvent, darkMode, setExposedVariable } = kanbanProps;
useEffect(() => {
if (!dragOverlay) {
return;
@ -61,6 +62,7 @@ export const Item = React.memo(
)
return;
setExposedVariable('lastSelectedCard', cardDataAsObj[value]);
setLastSelectedCard(cardDataAsObj[value]);
setShowModal(true);
fireEvent('onCardSelected');
}}

View file

@ -37,11 +37,10 @@ const dropAnimation = {
const TRASH_ID = 'void';
export function KanbanBoard({ widgetHeight, kanbanProps, parentRef }) {
const { properties, fireEvent, setExposedVariable, setExposedVariables, exposedVariables, styles } = kanbanProps;
const { lastSelectedCard = {} } = exposedVariables;
const { properties, fireEvent, setExposedVariable, setExposedVariables, styles } = kanbanProps;
const { columnData, cardData, cardWidth, cardHeight, showDeleteButton, enableAddCard } = properties;
const { accentColor } = styles;
const [lastSelectedCard, setLastSelectedCard] = useState({});
// eslint-disable-next-line react-hooks/exhaustive-deps
const columnDataAsObj = useMemo(() => convertArrayToObj(columnData), [JSON.stringify(columnData)]);
@ -85,7 +84,6 @@ export function KanbanBoard({ widgetHeight, kanbanProps, parentRef }) {
useEffect(() => {
droppableItemsColumnId.current = containers.find((container) => items[container]?.length > 0);
}, [items, containers]);
useEffect(() => {
setExposedVariable('updateCardData', async function (cardId, value) {
if (cardDataAsObj[cardId] === undefined) return toast.error('Card not found');
@ -95,6 +93,7 @@ export function KanbanBoard({ widgetHeight, kanbanProps, parentRef }) {
if (lastSelectedCard?.id === cardId) {
setExposedVariables({
lastSelectedCard: cardDataAsObj[cardId],
lastUpdatedCard: cardDataAsObj[cardId],
lastCardUpdate: diffKeys.map((key) => {
return {
@ -104,9 +103,10 @@ export function KanbanBoard({ widgetHeight, kanbanProps, parentRef }) {
updatedCardData: getData(cardDataAsObj),
});
fireEvent('onUpdate');
} else {
setExposedVariable('updatedCardData', getData(cardDataAsObj));
fireEvent('onUpdate');
}
setExposedVariable('updatedCardData', getData(cardDataAsObj));
fireEvent('onUpdate');
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [lastSelectedCard, JSON.stringify(cardDataAsObj)]);
@ -148,7 +148,6 @@ export function KanbanBoard({ widgetHeight, kanbanProps, parentRef }) {
...items,
[columnId]: [...items[columnId], cardDetails.id],
}));
setExposedVariables({ lastAddedCard: { ...cardDetails }, updatedCardData: getData(cardDataAsObj) });
fireEvent('onCardAdded');
});
@ -370,6 +369,7 @@ export function KanbanBoard({ widgetHeight, kanbanProps, parentRef }) {
isFirstItem={index === 0 && droppableItemsColumnId.current === columnId}
setShowModal={setShowModal}
cardDataAsObj={cardDataAsObj}
setLastSelectedCard={setLastSelectedCard}
/>
);
})}
@ -426,6 +426,7 @@ function SortableItem({
isFirstItem,
setShowModal,
cardDataAsObj,
setLastSelectedCard,
}) {
const { setNodeRef, setActivatorNodeRef, listeners, isDragging, isSorting, transform, transition } = useSortable({
id,
@ -450,6 +451,7 @@ function SortableItem({
isFirstItem={isFirstItem}
setShowModal={setShowModal}
cardDataAsObj={cardDataAsObj}
setLastSelectedCard={setLastSelectedCard}
/>
);
}

View file

@ -6,6 +6,7 @@ import { useCurrentState } from '@/_stores/currentStateStore';
export const BoardContext = React.createContext({});
// This one is deprecated and not deleted to support backward compatibility
export const KanbanBoard = ({
id,
height,

View file

@ -113,7 +113,7 @@ export const Listview = function Listview({
key={index}
data-cy={`${String(component.name).toLowerCase()}-row-${index}`}
onClick={(event) => {
event.stopPropagation();
event.preventDefault();
onRecordClicked(index);
onRowClicked(index);
}}

View file

@ -18,6 +18,7 @@ export function AddNewRowComponent({
columns,
addNewRowsDetails,
utilityForNestedNewRow,
tableEvents,
}) {
const getNewRowObject = () => {
return allColumns.reduce((accumulator, column) => {
@ -160,11 +161,10 @@ export function AddNewRowComponent({
<ButtonSolid
variant="primary"
className={`tj-text-xsm`}
onClick={() => {
onEvent('onNewRowsAdded', { component }).then(() => {
mergeToAddNewRowsDetails({ newRowsDataUpdates: {}, newRowsChangeSet: {}, addingNewRows: false });
setNewRowsState([]);
});
onClick={async () => {
await onEvent('onNewRowsAdded', tableEvents, { component });
mergeToAddNewRowsDetails({ newRowsDataUpdates: {}, newRowsChangeSet: {}, addingNewRows: false });
setNewRowsState([]);
}}
size="sm"
customStyles={{ padding: '10px 20px' }}

View file

@ -10,13 +10,14 @@ export const GlobalFilter = ({
onEvent,
// eslint-disable-next-line no-unused-vars
darkMode,
tableEvents,
}) => {
const [value, setValue] = React.useState(globalFilter);
const onChange = useAsyncDebounce((filterValue) => {
setValue(filterValue);
setGlobalFilter(filterValue || undefined);
onComponentOptionChanged(component, 'searchText', filterValue).then(() => {
onEvent('onSearch', { component, data: {} });
onEvent('onSearch', tableEvents, { component, data: {} });
});
}, 500);

View file

@ -46,10 +46,13 @@ import GenerateEachCellValue from './GenerateEachCellValue';
import { toast } from 'react-hot-toast';
import { Tooltip } from 'react-tooltip';
import { AddNewRowComponent } from './AddNewRowComponent';
import { useAppInfo } from '@/_stores/appDataStore';
import { ButtonSolid } from '@/_ui/AppButton/AppButton';
import SolidIcon from '@/_ui/Icon/SolidIcons';
import Skeleton, { SkeletonTheme } from 'react-loading-skeleton';
import { OverlayTriggerComponent } from './OverlayTriggerComponent';
// eslint-disable-next-line import/no-unresolved
import { diff } from 'deep-object-diff';
// utilityForNestedNewRow function is used to construct nested object while adding or updating new row when '.' is present in column key for adding new row
const utilityForNestedNewRow = (row) => {
@ -92,7 +95,7 @@ export function Table({
properties,
variablesExposedForPreview,
exposeToCodeHinter,
events,
// events,
setProperty,
mode,
exposedVariables,
@ -132,6 +135,11 @@ export function Table({
const updatedDataReference = useRef([]);
const preSelectRow = useRef(false);
const { events: allAppEvents } = useAppInfo();
const tableEvents = allAppEvents.filter((event) => event.target === 'component' && event.sourceId === id);
const tableColumnEvents = allAppEvents.filter((event) => event.target === 'table_column' && event.sourceId === id);
const tableActionEvents = allAppEvents.filter((event) => event.target === 'table_action' && event.sourceId === id);
const getItemStyle = ({ isDragging, isDropAnimating }, draggableStyle) => ({
...draggableStyle,
@ -179,13 +187,14 @@ export function Table({
);
useEffect(() => {
const hoverEvent = component?.definition?.events?.find((event) => {
const hoverEvent = tableEvents?.find(({ event }) => {
return event?.eventId == 'onRowHovered';
});
if (hoverEvent?.eventId) {
if (hoverEvent?.event?.eventId) {
setHoverAdded(true);
}
}, [JSON.stringify(component.definition.events)]);
}, [JSON.stringify(tableEvents)]);
function showFilters() {
mergeToFilterDetails({ filtersVisible: true });
@ -281,7 +290,7 @@ export function Table({
function getExportFileBlob({ columns, fileType, fileName }) {
let headers = columns.map((column) => {
return { exportValue: String(column.exportValue), key: column.key ? String(column.key) : column.key };
return { exportValue: String(column?.exportValue), key: column.key ? String(column.key) : column?.key };
});
let data = globalFilteredRows.map((row) => {
return headers.reduce((accumulator, header) => {
@ -399,12 +408,13 @@ export function Table({
tableRef,
t,
darkMode,
tableColumnEvents: tableColumnEvents,
});
columnData = useMemo(
() =>
columnData.filter((column) => {
if (resolveReferences(column.columnVisibility, currentState)) {
if (resolveReferences(column?.columnVisibility, currentState)) {
return column;
}
}),
@ -437,8 +447,9 @@ export function Table({
defaultColumn,
fireEvent,
setExposedVariables,
tableActionEvents,
}),
[JSON.stringify(actions)]
[JSON.stringify(actions), tableActionEvents]
);
const textWrapActions = (id) => {
@ -448,7 +459,7 @@ export function Table({
return wrapOption?.textWrap;
};
const optionsData = columnData.map((column) => column.columnOptions?.selectOptions);
const optionsData = columnData.map((column) => column?.columnOptions?.selectOptions);
const columns = useMemo(
() => {
return [...leftActionsCellData, ...columnData, ...rightActionsCellData];
@ -467,6 +478,8 @@ export function Table({
darkMode,
allowSelection,
highlightSelectedRow,
JSON.stringify(tableActionEvents),
JSON.stringify(tableColumnEvents),
] // Hack: need to fix
);
@ -777,11 +790,19 @@ export function Table({
useEffect(() => {
const newColumnSizes = { ...columnSizes, ...state.columnResizing.columnWidths };
if (!state.columnResizing.isResizingColumn && !_.isEmpty(newColumnSizes)) {
const isColumnSizeChanged = !_.isEmpty(diff(columnSizes, newColumnSizes));
if (isColumnSizeChanged && !state.columnResizing.isResizingColumn && !_.isEmpty(newColumnSizes)) {
changeCanDrag(true);
paramUpdated(id, 'columnSizes', {
value: newColumnSizes,
});
paramUpdated(
id,
'columnSizes',
{
value: newColumnSizes,
},
{ componentDefinitionChanged: true }
);
} else {
changeCanDrag(false);
}
@ -908,7 +929,7 @@ export function Table({
</div>
{allColumns.map(
(column) =>
typeof column.Header === 'string' && (
typeof column?.Header === 'string' && (
<div key={column.id}>
<div>
<label className="dropdown-item d-flex cursor-pointer">
@ -1040,6 +1061,7 @@ export function Table({
component={component}
onEvent={onEvent}
darkMode={darkMode}
tableEvents={tableEvents}
/>
)}
</div>
@ -1497,7 +1519,7 @@ export function Table({
variant="primary"
className={`tj-text-xsm`}
onClick={() => {
onEvent('onBulkUpdate', { component }).then(() => {
onEvent('onBulkUpdate', tableEvents, { component }).then(() => {
handleChangesSaved();
});
}}
@ -1676,6 +1698,7 @@ export function Table({
columns={columnsForAddNewRow}
addNewRowsDetails={tableDetails.addNewRowsDetails}
utilityForNestedNewRow={utilityForNestedNewRow}
tableEvents={tableEvents}
/>
)}
</div>

View file

@ -1,6 +1,13 @@
import React from 'react';
const generateActionsData = ({ actions: actionItems, columnSizes, defaultColumn, fireEvent, setExposedVariables }) => {
const generateActionsData = ({
actions: actionItems,
columnSizes,
defaultColumn,
fireEvent,
setExposedVariables,
tableActionEvents,
}) => {
const leftActions = (actions = actionItems) => actions.filter((action) => action.position === 'left');
const rightActions = (actions = actionItems) =>
actions.filter((action) => [undefined, 'right'].includes(action.position));
@ -32,6 +39,7 @@ const generateActionsData = ({ actions: actionItems, columnSizes, defaultColumn,
data: cell.row.original,
rowId: cell.row.id,
action,
tableActionEvents,
});
});
}}
@ -72,6 +80,7 @@ const generateActionsData = ({ actions: actionItems, columnSizes, defaultColumn,
data: cell.row.original,
rowId: cell.row.id,
action,
tableActionEvents,
});
});
}}

View file

@ -9,6 +9,7 @@ import { Toggle } from '../Toggle';
import { Datepicker } from '../Datepicker';
import { Link } from '../Link';
import moment from 'moment';
export default function generateColumnsData({
columnProperties,
columnSizes,
@ -25,10 +26,13 @@ export default function generateColumnsData({
tableRef,
t,
darkMode,
tableColumnEvents,
}) {
return columnProperties.map((column) => {
const columnSize = columnSizes[column.id] || columnSizes[column.name];
const columnType = column.columnType;
if (!column) return;
const columnSize = columnSizes[column?.id] || columnSizes[column?.name];
const columnType = column?.columnType;
let sortType = 'alphanumeric';
const columnOptions = {};
@ -429,6 +433,7 @@ export default function generateColumnsData({
column: column,
rowId: cell.row.id,
row: cell.row.original,
tableColumnEvents,
});
}
);

View file

@ -32,7 +32,6 @@ export const ConfigHandle = function ConfigHandle({
style={{ display: 'flex', alignItems: 'center' }}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
setSelectedComponent(id, component, e.shiftKey);
}}
role="button"
@ -56,7 +55,7 @@ export const ConfigHandle = function ConfigHandle({
role="button"
height="12"
draggable="false"
onClick={() => removeComponent({ id })}
onClick={() => removeComponent(id)}
data-cy={`${component.name.toLowerCase()}-delete-button`}
className="delete-icon"
/>

View file

@ -17,8 +17,11 @@ import { addComponents, addNewWidgetToTheEditor } from '@/_helpers/appUtils';
import { useCurrentState } from '@/_stores/currentStateStore';
import { useAppVersionStore } from '@/_stores/appVersionStore';
import { useEditorStore } from '@/_stores/editorStore';
import { useAppDataStore } from '@/_stores/appDataStore';
import { useAppInfo } from '@/_stores/appDataStore';
import { shallow } from 'zustand/shallow';
import _ from 'lodash';
// eslint-disable-next-line import/no-unresolved
import { diff } from 'deep-object-diff';
const NO_OF_GRIDS = 43;
@ -44,15 +47,21 @@ export const Container = ({
sideBarDebugger,
currentPageId,
}) => {
const gridWidth = canvasWidth / NO_OF_GRIDS;
const styles = {
width: currentLayout === 'mobile' ? deviceWindowWidth : '100%',
maxWidth: `${canvasWidth}px`,
backgroundSize: `${gridWidth}px 10px`,
};
// Dont update first time to skip
// redundant save on app definition load
const firstUpdate = useRef(true);
const { showComments, currentLayout, selectedComponents } = useEditorStore(
(state) => ({
showComments: state?.showComments,
currentLayout: state?.currentLayout,
selectedComponents: state?.selectedComponents,
}),
shallow
);
const { appId } = useAppInfo();
// eslint-disable-next-line react-hooks/exhaustive-deps
const components = appDefinition.pages[currentPageId]?.components ?? {};
const currentState = useCurrentState();
const { appVersionsId, enableReleasedVersionPopupState, isVersionReleased } = useAppVersionStore(
(state) => ({
@ -62,55 +71,66 @@ export const Container = ({
}),
shallow
);
const { showComments, currentLayout, selectedComponents } = useEditorStore(
(state) => ({
showComments: state?.showComments,
currentLayout: state?.currentLayout,
selectedComponents: state?.selectedComponents,
}),
shallow
);
const { appId } = useAppDataStore(
(state) => ({
appId: state?.appId,
}),
shallow
const gridWidth = canvasWidth / NO_OF_GRIDS;
const styles = {
width: currentLayout === 'mobile' ? deviceWindowWidth : '100%',
maxWidth: `${canvasWidth}px`,
backgroundSize: `${gridWidth}px 10px`,
};
const components = useMemo(
() => appDefinition.pages[currentPageId]?.components ?? {},
// eslint-disable-next-line react-hooks/exhaustive-deps
[JSON.stringify(appDefinition), currentPageId]
);
const [boxes, setBoxes] = useState(components);
const [boxes, setBoxes] = useState([]);
const [isDragging, setIsDragging] = useState(false);
const [isResizing, setIsResizing] = useState(false);
const [commentsPreviewList, setCommentsPreviewList] = useState([]);
const [newThread, addNewThread] = useState({});
const [isContainerFocused, setContainerFocus] = useState(false);
const [canvasHeight, setCanvasHeight] = useState(null);
const paramUpdatesOptsRef = useRef({});
const canvasRef = useRef(null);
const focusedParentIdRef = useRef(undefined);
useHotkeys('meta+z, control+z', () => handleUndo());
useHotkeys('meta+shift+z, control+shift+z', () => handleRedo());
useHotkeys(
'meta+v, control+v',
() => {
async () => {
if (isContainerFocused && !isVersionReleased) {
navigator.clipboard.readText().then((cliptext) => {
// Check if the clipboard API is available
if (navigator.clipboard && typeof navigator.clipboard.readText === 'function') {
try {
const cliptext = await navigator.clipboard.readText();
addComponents(
currentPageId,
appDefinition,
appDefinitionChanged,
focusedParentIdRef.current,
JSON.parse(cliptext)
JSON.parse(cliptext),
true
);
} catch (err) {
console.log(err);
}
});
} else {
console.log('Clipboard API is not available in this browser.');
}
}
enableReleasedVersionPopupState();
},
[isContainerFocused, appDefinition, focusedParentIdRef]
[isContainerFocused, appDefinition, focusedParentIdRef.current]
);
useEffect(() => {
setBoxes(components);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [JSON.stringify(components)]);
useEffect(() => {
const handleClick = (e) => {
if (canvasRef.current.contains(e.target) || document.getElementById('modal-container')?.contains(e.target)) {
@ -132,11 +152,6 @@ export const Container = ({
return () => document.removeEventListener('click', handleClick);
}, [isContainerFocused, canvasRef]);
useEffect(() => {
setBoxes(components);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [JSON.stringify(components)]);
//listening to no of component change to handle addition/deletion of widgets
const noOfBoxs = Object.values(boxes || []).length;
useEffect(() => {
@ -157,9 +172,6 @@ export const Container = ({
[boxes]
);
// Dont update first time to skip
// redundant save on app definition load
const firstUpdate = useRef(true);
useEffect(() => {
if (firstUpdate.current) {
firstUpdate.current = false;
@ -177,7 +189,26 @@ export const Container = ({
},
};
appDefinitionChanged(newDefinition);
//need to check if a new component is added or deleted
const oldComponents = appDefinition.pages[currentPageId]?.components ?? {};
const newComponents = boxes;
const componendAdded = Object.keys(newComponents).length > Object.keys(oldComponents).length;
const opts = _.isEmpty(paramUpdatesOptsRef.current) ? { containerChanges: true } : paramUpdatesOptsRef.current;
paramUpdatesOptsRef.current = {};
if (componendAdded) {
opts.componentAdded = true;
}
const shouldUpdate = !_.isEmpty(diff(appDefinition, newDefinition));
if (shouldUpdate) {
appDefinitionChanged(newDefinition, opts);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [boxes]);
@ -385,18 +416,18 @@ export const Container = ({
);
const paramUpdated = useCallback(
(id, param, value) => {
if (Object.keys(value).length > 0) {
(id, param, value, opts = {}) => {
if (Object.keys(value)?.length > 0) {
setBoxes((boxes) =>
update(boxes, {
[id]: {
$merge: {
component: {
...boxes[id].component,
...boxes[id]?.component,
definition: {
...boxes[id].component.definition,
...boxes[id]?.component?.definition,
properties: {
...boxes[id].component.definition.properties,
...boxes?.[id]?.component?.definition?.properties,
[param]: value,
},
},
@ -405,9 +436,12 @@ export const Container = ({
},
})
);
if (!_.isEmpty(opts)) {
paramUpdatesOptsRef.current = opts;
}
}
},
[setBoxes]
[boxes, setBoxes]
);
const handleAddThread = async (e) => {
@ -504,7 +538,7 @@ export const Container = ({
const componentWithChildren = {};
Object.keys(components).forEach((key) => {
const component = components[key];
const { parent } = component;
const parent = component?.component?.parent;
if (parent) {
componentWithChildren[parent] = {
...componentWithChildren[parent],
@ -620,7 +654,8 @@ export const Container = ({
const canShowInCurrentLayout =
box.component.definition.others[currentLayout === 'mobile' ? 'showOnMobile' : 'showOnDesktop'].value;
const addDefaultChildren = box.withDefaultChildren;
if (!box.parent && resolveReferences(canShowInCurrentLayout, currentState)) {
if (!box.component.parent && resolveReferences(canShowInCurrentLayout, currentState)) {
return (
<DraggableBox
className={showComments && 'pointer-events-none'}

View file

@ -235,6 +235,7 @@ export const DraggableBox = React.memo(
style={getStyles(isDragging, isSelectedComponent)}
>
<Rnd
maxWidth={canvasWidth}
style={{ ...style }}
resizeGrid={[gridWidth, 10]}
dragGrid={[gridWidth, 10]}

File diff suppressed because it is too large Load diff

View file

@ -1,4 +1,4 @@
import React, { useState, useEffect } from 'react';
import React, { useEffect, useState } from 'react';
import cx from 'classnames';
import { SketchPicker } from 'react-color';
import { Confirm } from '../Viewer/Confirm';
@ -14,18 +14,16 @@ import ExportAppModal from '../../HomePage/ExportAppModal';
import { useAppVersionStore } from '@/_stores/appVersionStore';
import { shallow } from 'zustand/shallow';
import { ButtonSolid } from '@/_ui/AppButton/AppButton';
import { useAppDataActions, useAppInfo } from '@/_stores/appDataStore';
export const GlobalSettings = ({
globalSettings,
globalSettingsChanged,
darkMode,
toggleAppMaintenance,
is_maintenance_on,
app,
isMaintenanceOn,
backgroundFxQuery,
realState,
handleSlugChange,
slug: oldSlug,
}) => {
const { t } = useTranslation();
const { hideHeader, canvasMaxWidth, canvasMaxWidthType, canvasBackgroundColor } = globalSettings;
@ -37,6 +35,7 @@ export const GlobalSettings = ({
const [slug, setSlug] = useState({ value: null, error: '' });
const [slugProgress, setSlugProgress] = useState(false);
const [isSlugUpdated, setSlugUpdatedState] = useState(false);
const { updateState } = useAppDataActions();
const { isVersionReleased } = useAppVersionStore(
(state) => ({
isVersionReleased: state.isVersionReleased,
@ -44,6 +43,8 @@ export const GlobalSettings = ({
shallow
);
const { app, slug: oldSlug } = useAppInfo();
const coverStyles = {
position: 'fixed',
top: '0px',
@ -58,6 +59,7 @@ export const GlobalSettings = ({
special chars or spaces in their app slugs
*/
const existedSlugErrors = validateName(oldSlug, 'App slug', true, false, false, false);
setSlug({ value: oldSlug, error: existedSlugErrors.errorMsg });
}, [oldSlug]);
@ -79,9 +81,11 @@ export const GlobalSettings = ({
error: '',
});
setSlugProgress(false);
handleSlugChange(value);
setSlugUpdatedState(true);
replaceEditorURL(value, realState?.page?.handle);
updateState({
slug: value,
});
})
.catch(({ error }) => {
setSlug({
@ -117,12 +121,13 @@ export const GlobalSettings = ({
outline: showPicker && '1px solid var(--indigo9)',
boxShadow: showPicker && '0px 0px 0px 1px #C6D4F9',
};
return (
<>
<Confirm
show={showConfirmation}
message={
is_maintenance_on
isMaintenanceOn
? 'Users will now be able to launch the released version of this app, do you wish to continue?'
: 'Users will not be able to launch the app until maintenance mode is turned off, do you wish to continue?'
}
@ -219,7 +224,7 @@ export const GlobalSettings = ({
className="form-check-input"
type="checkbox"
checked={hideHeader}
onChange={(e) => globalSettingsChanged('hideHeader', e.target.checked)}
onChange={(e) => globalSettingsChanged({ hideHeader: e.target.checked })}
/>
</div>
</div>
@ -232,7 +237,7 @@ export const GlobalSettings = ({
data-cy={`toggle-maintenance-mode`}
className="form-check-input"
type="checkbox"
checked={is_maintenance_on}
checked={isMaintenanceOn}
onChange={() => setConfirmationShow(true)}
/>
</div>
@ -251,7 +256,7 @@ export const GlobalSettings = ({
placeholder={'0'}
onChange={(e) => {
const width = e.target.value;
if (!Number.isNaN(width) && width >= 0) globalSettingsChanged('canvasMaxWidth', width);
if (!Number.isNaN(width) && width >= 0) globalSettingsChanged({ canvasMaxWidth: width });
}}
value={canvasMaxWidth}
/>
@ -261,12 +266,16 @@ export const GlobalSettings = ({
aria-label="Select canvas width type"
onChange={(event) => {
const newCanvasMaxWidthType = event.currentTarget.value;
globalSettingsChanged('canvasMaxWidthType', newCanvasMaxWidthType);
const options = {
canvasMaxWidthType: newCanvasMaxWidthType,
};
if (newCanvasMaxWidthType === '%') {
globalSettingsChanged('canvasMaxWidth', 100);
options.canvasMaxWidth = 100;
} else if (newCanvasMaxWidthType === 'px') {
globalSettingsChanged('canvasMaxWidth', 1292);
options.canvasMaxWidth = 1292;
}
globalSettingsChanged(options);
}}
>
<option value="%" selected={canvasMaxWidthType === '%'}>
@ -279,26 +288,7 @@ export const GlobalSettings = ({
</div>
</div>
</div>
{/* <div className="d-flex mb-3">
<span className="w-full m-auto" data-cy={`label-max-canvas-height`}>
{t('leftSidebar.Settings.maxHeightOfCanvas', 'Max height of canvas')}
</span>
<div className="global-popover-div-wrap global-popover-div-wrap-width">
<div className="input-with-icon">
<input
data-cy="maximum-canvas-height-input-field"
type="text"
className={`form-control form-control-sm maximum-canvas-height-input-field`}
placeholder={'0'}
onChange={(e) => {
const height = e.target.value;
if (!Number.isNaN(height) && height <= 2400) globalSettingsChanged('canvasMaxHeight', height);
}}
value={canvasMaxHeight}
/>
</div>
</div>
</div> */}
<div className="d-flex justify-content-between mb-3">
<span className="pt-2" data-cy={`label-bg-canvas`}>
{t('leftSidebar.Settings.backgroundColorOfCanvas', 'Canvas bavkground')}
@ -313,8 +303,12 @@ export const GlobalSettings = ({
onFocus={() => setShowPicker(true)}
color={canvasBackgroundColor}
onChangeComplete={(color) => {
globalSettingsChanged('canvasBackgroundColor', [color.hex, color.rgb]);
globalSettingsChanged('backgroundFxQuery', color.hex);
const options = {
canvasBackgroundColor: [color.hex, color.rgb],
backgroundFxQuery: color.hex,
};
globalSettingsChanged(options);
}}
/>
</div>
@ -357,8 +351,11 @@ export const GlobalSettings = ({
className="canvas-hinter-wrap"
lineNumbers={false}
onChange={(color) => {
globalSettingsChanged('canvasBackgroundColor', resolveReferences(color, realState));
globalSettingsChanged('backgroundFxQuery', color);
const options = {
canvasBackgroundColor: resolveReferences(color, realState),
backgroundFxQuery: color,
};
globalSettingsChanged(options);
}}
/>
)}

View file

@ -14,30 +14,29 @@ import { useUpdatePresence } from '@y-presence/react';
import { useAppVersionStore } from '@/_stores/appVersionStore';
import { useCurrentState } from '@/_stores/currentStateStore';
import { shallow } from 'zustand/shallow';
import { useAppInfo, useCurrentUser } from '@/_stores/appDataStore';
import SolidIcon from '@/_ui/Icon/SolidIcons';
import { redirectToDashboard } from '@/_helpers/routes';
export default function EditorHeader({
M,
app,
appVersionPreviewLink,
slug,
appId,
canUndo,
canRedo,
handleUndo,
handleRedo,
isSaving,
saveError,
onNameChanged,
setAppDefinitionFromVersion,
handleSlugChange,
onVersionRelease,
saveEditingVersion,
onVersionDelete,
currentUser,
slug,
darkMode,
}) {
const currentUser = useCurrentUser();
const { isSaving, appId, appName, app, isPublic, appVersionPreviewLink } = useAppInfo();
const { isVersionReleased, editingVersion } = useAppVersionStore(
(state) => ({
isVersionReleased: state.isVersionReleased,
@ -48,6 +47,7 @@ export default function EditorHeader({
const currentState = useCurrentState();
const updatePresence = useUpdatePresence();
useEffect(() => {
const initialPresence = {
firstName: currentUser?.first_name ?? '',
@ -62,7 +62,9 @@ export default function EditorHeader({
updatePresence(initialPresence);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [currentUser]);
const handleLogoClick = () => {
const handleLogoClick = (e) => {
e.preventDefault();
// Force a reload for clearing interval triggers
redirectToDashboard();
};
@ -96,7 +98,7 @@ export default function EditorHeader({
}}
>
<div className="global-settings-app-wrapper p-0 m-0 ">
<EditAppName appId={app.id} appName={app.name} onNameChanged={onNameChanged} />
<EditAppName appId={appId} appName={appName} onNameChanged={onNameChanged} />
</div>
<HeaderActions canUndo={canUndo} canRedo={canRedo} handleUndo={handleUndo} handleRedo={handleRedo} />
<div className="d-flex align-items-center">
@ -133,9 +135,9 @@ export default function EditorHeader({
{editingVersion && (
<AppVersionsManager
appId={appId}
releasedVersionId={app.current_version_id}
setAppDefinitionFromVersion={setAppDefinitionFromVersion}
onVersionDelete={onVersionDelete}
isPublic={isPublic ?? false}
/>
)}
</div>
@ -147,15 +149,16 @@ export default function EditorHeader({
>
<div className="navbar-nav flex-row order-md-last release-buttons ">
<div className="nav-item">
{app.id && (
{appId && (
<ManageAppUsers
app={app}
appId={appId}
slug={slug}
darkMode={darkMode}
handleSlugChange={handleSlugChange}
isVersionReleased={isVersionReleased}
pageHandle={currentState?.page?.handle}
M={M}
isPublic={isPublic ?? false}
/>
)}
</div>
@ -172,14 +175,12 @@ export default function EditorHeader({
</Link>
</div>
<div className="nav-item dropdown">
{app.id && (
<ReleaseVersionButton
appId={app.id}
appName={app.name}
onVersionRelease={onVersionRelease}
saveEditingVersion={saveEditingVersion}
/>
)}
<ReleaseVersionButton
appId={appId}
appName={appName}
onVersionRelease={onVersionRelease}
saveEditingVersion={saveEditingVersion}
/>
</div>
</div>
</div>

View file

@ -69,7 +69,7 @@ export function GotoApp({ getAllApps, event, handlerChanged, eventIndex, darkMod
<div key={index} className="row input-group mt-1">
<div className="col">
<CodeHinter
initialValue={event.queryParams[index][0]}
initialValue={event?.queryParams?.[index]?.[0]}
onChange={(value) => queryParamChangeHandler(index, 0, value)}
mode="javascript"
height={30}
@ -77,7 +77,7 @@ export function GotoApp({ getAllApps, event, handlerChanged, eventIndex, darkMod
</div>
<div className="col">
<CodeHinter
initialValue={event.queryParams[index][1]}
initialValue={event?.queryParams?.[index]?.[1]}
onChange={(value) => queryParamChangeHandler(index, 1, value)}
mode="javascript"
height={30}

View file

@ -69,7 +69,7 @@ export function SwitchPage({ getPages, event, handlerChanged, eventIndex, darkMo
<div key={index} className="row input-group mt-1">
<div className="col">
<CodeHinter
initialValue={event.queryParams[index][0]}
initialValue={event?.queryParams?.[index]?.[0]}
onChange={(value) => queryParamChangeHandler(index, 0, value)}
mode="javascript"
className="form-control codehinter-query-editor-input"
@ -79,7 +79,7 @@ export function SwitchPage({ getPages, event, handlerChanged, eventIndex, darkMo
</div>
<div className="col">
<CodeHinter
initialValue={event.queryParams[index][1]}
initialValue={event?.queryParams?.[index]?.[1]}
onChange={(value) => queryParamChangeHandler(index, 1, value)}
mode="javascript"
className="form-control codehinter-query-editor-input"

View file

@ -56,17 +56,16 @@ class Chart extends React.Component {
}
render() {
const { dataQueries, component, paramUpdated, componentMeta, components, currentState } = this.state;
const data = this.state.component.component.definition.properties.data;
const { dataQueries, component, paramUpdated, componentMeta, components, currentState } = this.props;
const data = this.props.component.component.definition.properties.data; // since component is not unmounting on every render in current scenario
const jsonDescription = this.state.component.component.definition.properties.jsonDescription;
const jsonDescription = this.props.component.component.definition.properties.jsonDescription;
const plotFromJson = resolveReferences(
this.state.component.component.definition.properties.plotFromJson?.value,
this.props.component.component.definition.properties.plotFromJson?.value,
currentState
);
const chartType = this.state.component.component.definition.properties.type.value;
const chartType = this.props.component.component.definition.properties.type.value;
let items = [];

View file

@ -98,8 +98,9 @@ export const baseComponentProperties = (
isOpen: true,
children: (
<EventManager
component={component}
componentMeta={componentMeta}
sourceId={component?.id}
eventSourceType="component"
eventMetaDefinition={componentMeta}
currentState={currentState}
dataQueries={dataQueries}
components={allComponents}

View file

@ -25,7 +25,7 @@ export const Form = ({
const { id } = component;
const newOptions = [{ name: 'None', value: 'none' }];
Object.entries(allComponents).forEach(([componentId, component]) => {
if (component.parent === id && component?.component?.component === 'Button') {
if (component.component.parent === id && component?.component?.component === 'Button') {
newOptions.push({ name: component.component.name, value: componentId });
}
});
@ -94,8 +94,9 @@ export const baseComponentProperties = (
isOpen: true,
children: (
<EventManager
component={component}
componentMeta={componentMeta}
sourceId={component?.id}
eventSourceType="component"
eventMetaDefinition={componentMeta}
currentState={currentState}
dataQueries={dataQueries}
components={allComponents}

View file

@ -139,8 +139,9 @@ export function Icon({ componentMeta, darkMode, ...restProps }) {
isOpen: false,
children: (
<EventManager
component={component}
componentMeta={componentMeta}
sourceId={component?.id}
eventSourceType="component"
eventMetaDefinition={componentMeta}
currentState={currentState}
dataQueries={dataQueries}
components={allComponents}

View file

@ -17,6 +17,7 @@ import List from '@/ToolJetUI/List/List';
import { capitalize, has } from 'lodash';
import NoListItem from './NoListItem';
import { ProgramaticallyHandleProperties } from './ProgramaticallyHandleProperties';
import { useAppDataStore } from '@/_stores/appDataStore';
class TableComponent extends React.Component {
constructor(props) {
super(props);
@ -73,13 +74,13 @@ class TableComponent extends React.Component {
onActionButtonPropertyChanged = (index, property, value) => {
const actions = this.props.component.component.definition.properties.actions;
actions.value[index][property] = value;
this.props.paramUpdated({ name: 'actions' }, 'value', actions.value, 'properties');
this.props.paramUpdated({ name: 'actions' }, 'value', actions.value, 'properties', true);
};
actionButtonEventsChanged = (events, index) => {
let actions = this.props.component.component.definition.properties.actions.value;
actions[index]['events'] = events;
this.props.paramUpdated({ name: 'actions' }, 'value', actions, 'properties');
this.props.paramUpdated({ name: 'actions' }, 'value', actions, 'properties', true);
};
actionButtonEventUpdated = (event, value, extraData) => {
@ -91,7 +92,7 @@ class TableComponent extends React.Component {
actionId: value,
};
this.props.paramUpdated({ name: 'actions' }, 'value', newValues, 'properties');
this.props.paramUpdated({ name: 'actions' }, 'value', newValues, 'properties', true);
};
actionButtonEventOptionUpdated = (event, option, value, extraData) => {
@ -106,7 +107,7 @@ class TableComponent extends React.Component {
[option]: value,
};
this.props.paramUpdated({ name: 'actions' }, 'value', newValues, 'properties');
this.props.paramUpdated({ name: 'actions' }, 'value', newValues, 'properties', true);
};
columnEventChanged = (columnForWhichEventsAreChanged, events) => {
@ -454,22 +455,18 @@ class TableComponent extends React.Component {
/>
</div>
<EventManager
component={{
component: {
definition: {
events: column.events ?? [],
},
},
}}
sourceId={this.props?.component?.id}
eventSourceType="table_column"
hideEmptyEventsAlert={true}
componentMeta={{ events: { onChange: { displayName: 'On change' } } }}
currentState={this.props.currentState}
eventMetaDefinition={{ events: { onChange: { displayName: 'On change' } } }}
currentState={this.state.currentState}
dataQueries={this.props.dataQueries}
components={this.props.components}
eventsChanged={(events) => this.columnEventChanged(column, events)}
apps={this.props.apps}
popOverCallback={(showing) => {
this.setColumnPopoverRootCloseBlocker('event-manager', showing);
this.setState({ actionPopOverRootClose: !showing });
this.setState({ showPopOver: showing });
}}
pages={this.props.pages}
/>
@ -751,6 +748,18 @@ class TableComponent extends React.Component {
);
};
deleteEvents = (ref, eventTarget) => {
const events = useAppDataStore.getState().events.filter((event) => event.target === eventTarget);
const toDelete = events?.filter((e) => e.event?.ref === ref.ref);
return new Promise.all(
toDelete?.forEach((e) => {
return useAppDataStore.getState().actions.deleteAppVersionEventHandler(e.id);
})
);
};
actionPopOver = (action, index) => {
const dummyComponentForActionButton = {
component: {
@ -760,6 +769,8 @@ class TableComponent extends React.Component {
},
};
const actionRef = { ref: `${action?.name}` };
return (
<Popover id="popover-basic" className={`${this.props.darkMode && 'dark-theme'}`}>
<Popover.Body>
@ -827,8 +838,12 @@ class TableComponent extends React.Component {
paramType="properties"
/>
<EventManager
//!have to check
component={dummyComponentForActionButton}
componentMeta={{ events: { onClick: { displayName: 'On click' } } }}
sourceId={this.props?.component?.id}
eventSourceType="table_action"
customEventRefs={actionRef}
eventMetaDefinition={{ events: { onClick: { displayName: 'On click' } } }}
currentState={this.state.currentState}
dataQueries={this.props.dataQueries}
components={this.props.components}
@ -840,7 +855,10 @@ class TableComponent extends React.Component {
}}
pages={this.props.pages}
/>
<button className="btn btn-sm btn-outline-danger mt-2 col" onClick={() => this.removeAction(index)}>
<button
className="btn btn-sm btn-outline-danger mt-2 col"
onClick={() => this.removeAction(index, actionRef)}
>
{this.props.t('widget.Table.remove', 'Remove')}
</button>
</Popover.Body>
@ -891,20 +909,21 @@ class TableComponent extends React.Component {
const columns = this.props.component.component.definition.properties.columns;
const newValue = columns.value;
newValue.push({ name: this.generateNewColumnName(columns.value), id: uuidv4() });
this.props.paramUpdated({ name: 'columns' }, 'value', newValue, 'properties');
this.props.paramUpdated({ name: 'columns' }, 'value', newValue, 'properties', true);
};
addNewAction = () => {
const actions = this.props.component.component.definition.properties.actions;
const newValue = actions ? actions.value : [];
newValue.push({ name: computeActionName(actions), buttonText: 'Button', events: [] });
this.props.paramUpdated({ name: 'actions' }, 'value', newValue, 'properties');
this.props.paramUpdated({ name: 'actions' }, 'value', newValue, 'properties', true);
};
removeAction = (index) => {
removeAction = (index, ref) => {
const newValue = this.props.component.component.definition.properties.actions.value;
newValue.splice(index, 1);
this.props.paramUpdated({ name: 'actions' }, 'value', newValue, 'properties');
this.props.paramUpdated({ name: 'actions' }, 'value', newValue, 'properties', true);
this.deleteEvents(ref, 'table_action');
};
onColumnItemChange = (index, item, value) => {
@ -914,7 +933,8 @@ class TableComponent extends React.Component {
column[item] = value;
const newColumns = columns.value;
newColumns[index] = column;
this.props.paramUpdated({ name: 'columns' }, 'value', newColumns, 'properties');
this.props.paramUpdated({ name: 'columns' }, 'value', newColumns, 'properties', true);
};
getItemStyle = (isDragging, draggableStyle) => ({
@ -922,11 +942,11 @@ class TableComponent extends React.Component {
...draggableStyle,
});
removeColumn = (index) => {
removeColumn = (index, ref) => {
const columns = this.props.component.component.definition.properties.columns;
const newValue = columns.value;
const removedColumns = newValue.splice(index, 1);
this.props.paramUpdated({ name: 'columns' }, 'value', newValue, 'properties');
this.props.paramUpdated({ name: 'columns' }, 'value', newValue, 'properties', true);
const existingcolumnDeletionHistory =
this.props.component.component.definition.properties.columnDeletionHistory?.value ?? [];
@ -934,14 +954,16 @@ class TableComponent extends React.Component {
...existingcolumnDeletionHistory,
...removedColumns.map((column) => column.key || column.name),
];
this.props.paramUpdated({ name: 'columnDeletionHistory' }, 'value', newcolumnDeletionHistory, 'properties');
this.props.paramUpdated({ name: 'columnDeletionHistory' }, 'value', newcolumnDeletionHistory, 'properties', true);
this.deleteEvents(ref, 'table_column');
};
reorderColumns = (startIndex, endIndex) => {
const result = this.props.component.component.definition.properties.columns.value;
const [removed] = result.splice(startIndex, 1);
result.splice(endIndex, 0, removed);
this.props.paramUpdated({ name: 'columns' }, 'value', result, 'properties');
this.props.paramUpdated({ name: 'columns' }, 'value', result, 'properties', true);
};
onDragEnd({ source, destination }) {
@ -959,7 +981,6 @@ class TableComponent extends React.Component {
const columns = component.component.definition.properties.columns;
const actions = component.component.definition.properties.actions || { value: [] };
if (!component.component.definition.properties.displaySearchBox)
paramUpdated({ name: 'displaySearchBox' }, 'value', true, 'properties');
const displaySearchBox = component.component.definition.properties.displaySearchBox.value;
@ -1056,7 +1077,8 @@ class TableComponent extends React.Component {
enableActionsMenu
isEditable={item.isEditable === '{{true}}'}
onMenuOptionClick={(listItem, menuOptionLabel) => {
if (menuOptionLabel === 'Delete') this.removeColumn(index);
if (menuOptionLabel === 'Delete')
this.removeColumn(index, `${item.name}-${index}`);
}}
darkMode={darkMode}
menuActions={[
@ -1142,8 +1164,11 @@ class TableComponent extends React.Component {
isOpen: true,
children: (
<EventManager
//!have to check
component={component}
componentMeta={componentMeta}
sourceId={this.props?.component?.id}
eventSourceType="component"
eventMetaDefinition={componentMeta}
currentState={currentState}
dataQueries={dataQueries}
components={components}

View file

@ -14,25 +14,25 @@ import defaultStyles from '@/_ui/Select/styles';
import { useTranslation } from 'react-i18next';
import { useDataQueriesStore } from '@/_stores/dataQueriesStore';
import RunjsParameters from './ActionConfigurationPanels/RunjsParamters';
import AddNewButton from '@/ToolJetUI/Buttons/AddNewButton/AddNewButton';
import { useAppDataActions, useAppInfo } from '@/_stores/appDataStore';
import { isQueryRunnable } from '@/_helpers/utils';
import { shallow } from 'zustand/shallow';
import ManageEventButton from './ManageEventButton';
import AddNewButton from '@/ToolJetUI/Buttons/AddNewButton/AddNewButton';
import NoListItem from './Components/Table/NoListItem';
import ManageEventButton from './ManageEventButton';
export const EventManager = ({
component,
componentMeta,
currentState = {},
sourceId,
eventSourceType,
eventMetaDefinition,
components,
eventsChanged,
apps,
excludeEvents,
popOverCallback,
popoverPlacement,
pages,
hideEmptyEventsAlert,
callerQueryId,
customEventRefs = undefined,
}) => {
const dataQueries = useDataQueriesStore(({ dataQueries = [] }) => {
if (callerQueryId) {
@ -41,13 +41,35 @@ export const EventManager = ({
}
return dataQueries;
}, shallow);
const [events, setEvents] = useState(() => component.component.definition.events || []);
const { apps, appId, events: allAppEvents } = useAppInfo();
const { updateAppVersionEventHandlers, createAppVersionEventHandlers, deleteAppVersionEventHandler } =
useAppDataActions();
const currentEvents = allAppEvents.filter((event) => {
if (customEventRefs) {
if (event.event.ref !== customEventRefs.ref) {
return false;
}
}
return event.sourceId === sourceId && event.target === eventSourceType;
});
const [events, setEvents] = useState([]);
const [focusedEventIndex, setFocusedEventIndex] = useState(null);
const { t } = useTranslation();
useEffect(() => {
setEvents(component.component.definition.events || []);
}, [component?.component?.definition?.events]);
if (_.isEqual(currentEvents, events)) return;
const sortedEvents = currentEvents.sort((a, b) => {
return a.index - b.index;
});
setEvents(sortedEvents || []);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [JSON.stringify(currentEvents)]);
let actionOptions = ActionTypes.map((action) => {
return { name: action.name, value: action.id };
@ -100,11 +122,11 @@ export const EventManager = ({
excludeEvents = excludeEvents || [];
/* Filter events based on excludesEvents ( a list of event ids to exclude ) */
let possibleEvents = Object.keys(componentMeta.events)
let possibleEvents = Object.keys(eventMetaDefinition.events)
.filter((eventId) => !excludeEvents.includes(eventId))
.map((eventId) => {
return {
name: componentMeta.events[eventId].displayName,
name: eventMetaDefinition?.events[eventId]?.displayName,
value: eventId,
};
});
@ -151,7 +173,7 @@ export const EventManager = ({
const actions = targetComponentMeta.actions;
const options = actions.map((action) => ({
name: action.displayName,
name: action?.displayName,
value: action.handle,
}));
@ -172,7 +194,7 @@ export const EventManager = ({
function getComponentActionDefaultParams(componentId, actionHandle) {
const action = getAction(componentId, actionHandle);
const defaultParams = (action.params ?? []).map((param) => ({
const defaultParams = (action?.params ?? []).map((param) => ({
handle: param.handle,
value: param.defaultValue,
}));
@ -182,7 +204,7 @@ export const EventManager = ({
function getAllApps() {
let appsOptionsList = [];
apps
.filter((item) => item.slug !== undefined)
.filter((item) => item.slug !== undefined && item.id !== appId)
.forEach((item) => {
appsOptionsList.push({
name: item.name,
@ -208,51 +230,105 @@ export const EventManager = ({
}));
}
function handlerChanged(index, param, value) {
let newEvents = [...events];
function handleQueryChange(index, updates) {
let newEvents = _.cloneDeep(events);
let updatedEvent = newEvents[index];
updatedEvent[param] = value;
updatedEvent.event = {
...updatedEvent.event,
...updates,
};
newEvents[index] = updatedEvent;
setEvents(newEvents);
eventsChanged(newEvents);
updateAppVersionEventHandlers(
[
{
event_id: updatedEvent.id,
diff: updatedEvent,
},
],
'update'
);
}
function handlerChanged(index, param, value) {
let newEvents = _.cloneDeep(events);
let updatedEvent = newEvents[index];
updatedEvent.event[param] = value;
if (param === 'componentSpecificActionHandle') {
const getDefault = getComponentActionDefaultParams(updatedEvent.event?.componentId, value);
updatedEvent.event['componentSpecificActionParams'] = getDefault;
}
newEvents[index] = updatedEvent;
updateAppVersionEventHandlers(
[
{
event_id: updatedEvent.id,
diff: updatedEvent,
},
],
'update'
);
}
function removeHandler(index) {
let newEvents = component.component.definition.events;
newEvents.splice(index, 1);
setEvents(newEvents);
eventsChanged(newEvents);
const eventsHandler = _.cloneDeep(events);
const eventId = eventsHandler[index].id;
deleteAppVersionEventHandler(eventId);
}
function addHandler() {
let newEvents = component.component.definition.events;
newEvents.push({
eventId: Object.keys(componentMeta.events)[0],
actionId: 'show-alert',
message: 'Hello world!',
alertType: 'info',
let newEvents = events;
const eventIndex = newEvents.length;
createAppVersionEventHandlers({
event: {
eventId: Object.keys(eventMetaDefinition?.events)[0],
actionId: 'show-alert',
message: 'Hello world!',
alertType: 'info',
...customEventRefs,
},
eventType: eventSourceType,
attachedTo: sourceId,
index: eventIndex,
});
setEvents(newEvents);
eventsChanged(newEvents);
}
//following two are functions responsible for on change and value for the control specific actions
const onChangeHandlerForComponentSpecificActionHandle = (value, index, param, event) => {
const newParam = { ...param, value: value };
const params = event?.componentSpecificActionParams ?? [];
const newParams = params.map((paramOfParamList) =>
paramOfParamList.handle === param.handle ? newParam : paramOfParamList
);
const newParams =
params.length > 0
? params.map((paramOfParamList) => {
return paramOfParamList.handle === param.handle ? newParam : paramOfParamList;
})
: [newParam];
return handlerChanged(index, 'componentSpecificActionParams', newParams);
};
const valueForComponentSpecificActionHandle = (event, param) => {
return (
event?.componentSpecificActionParams?.find((paramItem) => paramItem.handle === param.handle)?.value ??
param.defaultValue
);
const componentSpecificActionParamsExits = Array.isArray(event?.componentSpecificActionParams);
const defaultValue = param.defaultValue ?? '';
if (componentSpecificActionParamsExits) {
const paramValue =
event?.componentSpecificActionParams?.find((paramItem) => paramItem.handle === param.handle)?.value ??
defaultValue;
return paramValue;
}
return defaultValue;
};
function eventPopover(event, index) {
@ -260,10 +336,14 @@ export const EventManager = ({
<Popover
id="popover-basic"
style={{ width: '350px', maxWidth: '350px' }}
className={`${darkMode && ' dark-theme'} shadow event-manager-popover`}
className={`${darkMode && 'dark-theme'} shadow`}
data-cy="popover-card"
>
<Popover.Body>
<Popover.Body
onClick={(e) => {
e.stopPropagation();
}}
>
<div className="row">
<div className="col-3 p-2">
<span data-cy="event-label">{t('editor.inspector.eventManager.event', 'Event')}</span>
@ -443,10 +523,11 @@ export const EventManager = ({
options={dataQueries
.filter((qry) => isQueryRunnable(qry))
.map((qry) => ({ name: qry.name, value: qry.id }))}
value={event.queryId}
value={event?.queryId}
search={true}
onChange={(value) => {
const query = dataQueries.find((dataquery) => dataquery.id === value);
const parameters = (query?.options?.parameters ?? []).reduce(
(paramObj, param) => ({
...paramObj,
@ -454,9 +535,12 @@ export const EventManager = ({
}),
{}
);
handlerChanged(index, 'queryId', query.id);
handlerChanged(index, 'queryName', query.name);
handlerChanged(index, 'parameters', parameters);
handleQueryChange(index, {
queryId: query.id,
queryName: query.name,
parameters: parameters,
});
}}
placeholder={t('globals.select', 'Select') + '...'}
styles={styles}
@ -688,7 +772,6 @@ export const EventManager = ({
value={event?.componentId}
search={true}
onChange={(value) => {
handlerChanged(index, 'componentSpecificActionHandle', '');
handlerChanged(index, 'componentId', value);
}}
placeholder={t('globals.select', 'Select') + '...'}
@ -710,11 +793,6 @@ export const EventManager = ({
search={true}
onChange={(value) => {
handlerChanged(index, 'componentSpecificActionHandle', value);
handlerChanged(
index,
'componentSpecificActionParams',
getComponentActionDefaultParams(event?.componentId, value)
);
}}
placeholder={t('globals.select', 'Select') + '...'}
styles={styles}
@ -725,10 +803,10 @@ export const EventManager = ({
</div>
{event?.componentId &&
event?.componentSpecificActionHandle &&
(getAction(event?.componentId, event?.componentSpecificActionHandle).params ?? []).map((param) => (
(getAction(event?.componentId, event?.componentSpecificActionHandle)?.params ?? []).map((param) => (
<div className="row mt-2" key={param.handle}>
<div className="col-3 p-1" data-cy={`action-options-${param.displayName}-field-label`}>
{param.displayName}
<div className="col-3 p-1" data-cy={`action-options-${param?.displayName}-field-label`}>
{param?.displayName}
</div>
{param.type === 'select' ? (
<div className="col-9" data-cy="action-options-action-selection-field">
@ -763,7 +841,7 @@ export const EventManager = ({
enablePreview={true}
type={param?.type}
fieldMeta={{ options: param?.options }}
cyLabel={param.displayName}
cyLabel={param?.displayName}
/>
</div>
)}
@ -789,11 +867,24 @@ export const EventManager = ({
}
const reorderEvents = (startIndex, endIndex) => {
const result = [...component.component.definition.events];
const result = _.cloneDeep(events);
const [removed] = result.splice(startIndex, 1);
result.splice(endIndex, 0, removed);
setEvents(result);
eventsChanged(result, true);
const reorderedEvents = result.map((event, index) => {
return {
...event,
index: index,
};
});
updateAppVersionEventHandlers(
reorderedEvents.map((event) => ({
event_id: event.id,
diff: event,
})),
'reorder'
);
};
const onDragEnd = ({ source, destination }) => {
@ -817,8 +908,8 @@ export const EventManager = ({
{({ innerRef, droppableProps, placeholder }) => (
<div {...droppableProps} ref={innerRef}>
{events.map((event, index) => {
const actionMeta = ActionTypes.find((action) => action.id === event.actionId);
const rowClassName = `card-body p-0 ${focusedEventIndex === index ? ' bg-azure-lt' : ''}`;
const actionMeta = ActionTypes.find((action) => action.id === event.event.actionId);
// const rowClassName = `card-body p-0 ${focusedEventIndex === index ? ' bg-azure-lt' : ''}`;
return (
<Draggable key={index} draggableId={`${event.eventId}-${index}`} index={index}>
{renderDraggable((provided, snapshot) => {
@ -831,14 +922,13 @@ export const EventManager = ({
trigger="click"
placement={popoverPlacement || 'left'}
rootClose={true}
overlay={eventPopover(event, index)}
overlay={eventPopover(event.event, index)}
onHide={() => setFocusedEventIndex(null)}
onToggle={(showing) => {
if (showing) {
setFocusedEventIndex(index);
} else {
setFocusedEventIndex(null);
eventsChanged(events);
}
if (typeof popOverCallback === 'function') popOverCallback(showing);
}}
@ -850,7 +940,7 @@ export const EventManager = ({
{...provided.dragHandleProps}
>
<ManageEventButton
eventDisplayName={componentMeta.events[event.eventId]['displayName']}
eventDisplayName={eventMetaDefinition?.events[event.event.eventId]?.displayName}
actionName={actionMeta.name}
removeHandler={removeHandler}
index={index}
@ -888,12 +978,33 @@ export const EventManager = ({
);
}
const componentName = eventMetaDefinition?.name ? eventMetaDefinition.name : 'query';
if (events.length === 0) {
return (
<>
{renderAddHandlerBtn()}
{!hideEmptyEventsAlert ? (
<div className="text-left">
<small className="color-disabled" data-cy="no-event-handler-message">
{t(
'editor.inspector.eventManager.emptyMessage',
"This {{componentName}} doesn't have any event handlers",
{
componentName: componentName.toLowerCase(),
}
)}
</small>
</div>
) : null}
</>
);
}
return (
<>
<div className="mb-3">
{renderHandlers(events)}
{renderAddHandlerBtn()}
</div>
{renderHandlers(events)}
{renderAddHandlerBtn()}
</>
);
};

View file

@ -56,7 +56,6 @@ export const Inspector = ({
selectedComponentId,
componentDefinitionChanged,
allComponents,
apps,
darkMode,
switchSidebarTab,
removeComponent,
@ -66,18 +65,17 @@ export const Inspector = ({
const dataQueries = useDataQueries();
const component = {
id: selectedComponentId,
component: allComponents[selectedComponentId].component,
component: JSON.parse(JSON.stringify(allComponents[selectedComponentId].component)),
layouts: allComponents[selectedComponentId].layouts,
parent: allComponents[selectedComponentId].parent,
};
const currentState = useCurrentState();
const [showWidgetDeleteConfirmation, setWidgetDeleteConfirmation] = useState(false);
// eslint-disable-next-line no-unused-vars
const [tabHeight, setTabHeight] = React.useState(0);
const componentNameRef = useRef(null);
const [newComponentName, setNewComponentName] = useState(component.component.name);
const [inputRef, setInputFocus] = useFocus();
const [selectedTab, setSelectedTab] = useState('properties');
// const [selectedTab, setSelectedTab] = useState('properties');
const [showHeaderActionsMenu, setShowHeaderActionsMenu] = useState(false);
const { isVersionReleased } = useAppVersionStore(
(state) => ({
@ -101,13 +99,6 @@ export const Inspector = ({
componentNameRef.current = newComponentName;
}, [newComponentName]);
useEffect(() => {
return () => {
handleComponentNameChange(componentNameRef.current);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const validateComponentName = (name) => {
const isValid = !Object.values(allComponents)
.map((component) => component.component.name)
@ -131,9 +122,9 @@ export const Inspector = ({
return setInputFocus();
}
if (validateQueryName(newName)) {
let newComponent = { ...component };
let newComponent = JSON.parse(JSON.stringify(component));
newComponent.component.name = newName;
componentDefinitionChanged(newComponent);
componentDefinitionChanged(newComponent, { componentNameUpdated: true });
} else {
toast.error(
t(
@ -152,9 +143,9 @@ export const Inspector = ({
return null;
};
function paramUpdated(param, attr, value, paramType) {
console.log({ param, attr, value, paramType });
let newDefinition = _.cloneDeep(component.component.definition);
function paramUpdated(param, attr, value, paramType, isParamFromTableColumn = false) {
let newComponent = JSON.parse(JSON.stringify(component));
let newDefinition = _.cloneDeep(newComponent.component.definition);
let allParams = newDefinition[paramType] || {};
const paramObject = allParams[param.name];
if (!paramObject) {
@ -163,13 +154,19 @@ export const Inspector = ({
if (attr) {
allParams[param.name][attr] = value;
const defaultValue = getDefaultValue(value);
// This is needed to have enable pagination as backward compatible
// This is needed to have enable pagination in Table as backward compatible
// Whenever enable pagination is false, we turn client and server side pagination as false
if (param.name === 'enablePagination' && !resolveReferences(value, currentState)) {
if (
component.component.component === 'Table' &&
param.name === 'enablePagination' &&
!resolveReferences(value, currentState)
) {
if (allParams?.['clientSidePagination']?.[attr]) {
allParams['clientSidePagination'][attr] = value;
}
allParams['serverSidePagination'][attr] = value;
if (allParams['serverSidePagination']?.[attr]) {
allParams['serverSidePagination'][attr] = value;
}
}
// This case is required to handle for older apps when serverSidePagination is connected to Fx
if (param.name === 'serverSidePagination' && !allParams?.['enablePagination']?.[attr]) {
@ -194,12 +191,11 @@ export const Inspector = ({
allParams[param.name] = value;
}
newDefinition[paramType] = allParams;
let newComponent = _.merge(component, {
component: {
definition: newDefinition,
},
newComponent.component.definition = newDefinition;
componentDefinitionChanged(newComponent, {
componentPropertyUpdated: true,
isParamFromTableColumn: isParamFromTableColumn,
});
componentDefinitionChanged(newComponent);
}
function layoutPropertyChanged(param, attr, value, paramType) {
@ -207,9 +203,7 @@ export const Inspector = ({
// User wants to show the widget on mobile devices
if (param.name === 'showOnMobile' && value === true) {
let newComponent = {
...component,
};
let newComponent = JSON.parse(JSON.stringify(component));
const { width, height } = newComponent.layouts['desktop'];
@ -223,7 +217,7 @@ export const Inspector = ({
},
};
componentDefinitionChanged(newComponent);
componentDefinitionChanged(newComponent, { layoutPropertyChanged: true });
// Child components should also have a mobile layout
const childComponents = Object.keys(allComponents).filter((key) => allComponents[key].parent === component.id);
@ -246,54 +240,11 @@ export const Inspector = ({
},
};
componentDefinitionChanged(newChild);
componentDefinitionChanged(newChild, { withChildLayout: true });
});
}
}
function eventUpdated(event, actionId) {
let newDefinition = { ...component.component.definition };
newDefinition.events[event.name] = { actionId };
let newComponent = {
...component,
};
componentDefinitionChanged(newComponent);
}
function eventsChanged(newEvents, isReordered = false) {
let newDefinition;
if (isReordered) {
newDefinition = { ...component.component };
newDefinition.definition.events = newEvents;
} else {
newDefinition = { ...component.component.definition };
newDefinition.events = newEvents;
}
let newComponent = {
...component,
};
componentDefinitionChanged(newComponent);
}
function eventOptionUpdated(event, option, value) {
console.log('eventOptionUpdated', event, option, value);
let newDefinition = { ...component.component.definition };
let eventDefinition = newDefinition.events[event.name] || { options: {} };
newDefinition.events[event.name] = { ...eventDefinition, options: { ...eventDefinition.options, [option]: value } };
let newComponent = {
...component,
};
componentDefinitionChanged(newComponent);
}
const handleInspectorHeaderActions = (value) => {
if (value === 'rename') {
setTimeout(() => setInputFocus(), 0);
@ -339,13 +290,13 @@ export const Inspector = ({
paramUpdated={paramUpdated}
dataQueries={dataQueries}
componentMeta={componentMeta}
eventUpdated={eventUpdated}
eventOptionUpdated={eventOptionUpdated}
// eventUpdated={eventUpdated}
// eventOptionUpdated={eventOptionUpdated}
components={allComponents}
currentState={currentState}
darkMode={darkMode}
eventsChanged={eventsChanged}
apps={apps}
// eventsChanged={eventsChanged}
// apps={apps} !check
pages={pages}
allComponents={allComponents}
/>
@ -382,31 +333,15 @@ export const Inspector = ({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [JSON.stringify({ showHeaderActionsMenu })]);
const handleDeleteConfirm = React.useCallback(() => {
switchSidebarTab(2);
removeComponent(component);
setWidgetDeleteConfirmation(false);
}, [switchSidebarTab, removeComponent, component, setWidgetDeleteConfirmation]);
React.useEffect(() => {
const handleKeyPress = (event) => {
if (showWidgetDeleteConfirmation && event.key === 'Enter') {
handleDeleteConfirm();
}
};
document.addEventListener('keydown', handleKeyPress);
return () => {
document.removeEventListener('keydown', handleKeyPress);
};
}, [showWidgetDeleteConfirmation, handleDeleteConfirm]);
return (
<div className="inspector">
<ConfirmDialog
show={showWidgetDeleteConfirmation}
message={'Widget will be deleted, do you want to continue?'}
onConfirm={handleDeleteConfirm}
onConfirm={() => {
switchSidebarTab(2);
removeComponent(component.id);
}}
onCancel={() => setWidgetDeleteConfirmation(false)}
darkMode={darkMode}
/>

View file

@ -62,6 +62,7 @@ export const LeftSidebarInspector = ({
delete jsontreeData.server;
delete jsontreeData.actions;
delete jsontreeData.succededQuery;
delete jsontreeData.layout;
//*Sorted components and queries alphabetically
const sortedComponents = Object.keys(jsontreeData['components'])
@ -117,7 +118,7 @@ export const LeftSidebarInspector = ({
const iconsList = useMemo(() => [...queryIcons, ...componentIcons], [queryIcons, componentIcons]);
const handleRemoveComponent = (component) => {
removeComponent(component);
removeComponent(component.id);
};
const handleSelectComponentOnEditor = (component) => {

View file

@ -4,7 +4,7 @@ import { useAppVersionStore } from '@/_stores/appVersionStore';
import { shallow } from 'zustand/shallow';
import SolidIcon from '@/_ui/Icon/SolidIcons';
export const GlobalSettings = ({ darkMode, showHideViewerNavigationControls, showPageViwerPageNavitation }) => {
export const GlobalSettings = ({ darkMode, showHideViewerNavigationControls, isViewerNavigationDisabled }) => {
const { isVersionReleased, enableReleasedVersionPopupState } = useAppVersionStore(
(state) => ({
isVersionReleased: state.isVersionReleased,
@ -35,7 +35,7 @@ export const GlobalSettings = ({ darkMode, showHideViewerNavigationControls, sho
</label>
<hr style={{ margin: '0.75rem 0' }} />
<div className="menu-options mb-0">
<Toggle onChange={onChange} value={!showPageViwerPageNavitation} />
<Toggle onChange={onChange} value={isViewerNavigationDisabled} />
</div>
</div>
</Popover.Body>

View file

@ -25,7 +25,7 @@ export const PageHandler = ({
currentPageId,
updateHomePage,
updatePageHandle,
updateOnPageLoadEvents,
apps,
pages,
components,
@ -201,7 +201,6 @@ export const PageHandler = ({
!haveUserPinned && pinPagesPopover(false);
}}
darkMode={darkMode}
updateOnPageLoadEvents={updateOnPageLoadEvents}
apps={apps}
pages={pages}
components={components}

View file

@ -8,7 +8,7 @@ export const SettingsModal = ({
show,
handleClose,
darkMode,
updateOnPageLoadEvents,
apps,
pages,
components,
@ -55,6 +55,7 @@ export const SettingsModal = ({
<Modal.Body onClick={() => pinPagesPopover(true)}>
<b data-cy={'page-events-labe'}>Events</b>
<EventManager
//!page
component={{
component: {
definition: {
@ -62,11 +63,12 @@ export const SettingsModal = ({
},
},
}}
componentMeta={{ events: { onPageLoad: { displayName: 'On page load' } }, name: 'page' }}
sourceId={page?.id}
eventSourceType="page"
eventMetaDefinition={{ events: { onPageLoad: { displayName: 'On page load' } }, name: 'page' }}
components={components}
apps={apps}
pages={allpages}
eventsChanged={(events) => updateOnPageLoadEvents(page.id, events)}
popOverCallback={(showing) => showing}
/>
</Modal.Body>

View file

@ -30,7 +30,7 @@ const LeftSidebarPageSelector = ({
homePageId,
showHideViewerNavigationControls,
updateOnSortingPages,
updateOnPageLoadEvents,
apps,
pinned,
setPinned,
@ -89,7 +89,7 @@ const LeftSidebarPageSelector = ({
<GlobalSettings
darkMode={darkMode}
showHideViewerNavigationControls={showHideViewerNavigationControls}
showPageViwerPageNavitation={appDefinition?.showViewerNavigation || false}
isViewerNavigationDisabled={!appDefinition?.showViewerNavigation}
/>
}
>
@ -166,7 +166,6 @@ const LeftSidebarPageSelector = ({
updatePageHandle={updatePageHandle}
classNames="page-handler"
onSort={updateOnSortingPages}
updateOnPageLoadEvents={updateOnPageLoadEvents}
currentState={currentState}
apps={apps}
allpages={pages}

View file

@ -19,8 +19,8 @@ import { useDataSources } from '@/_stores/dataSourcesStore';
import { shallow } from 'zustand/shallow';
import useDebugger from './SidebarDebugger/useDebugger';
import { GlobalSettings } from '../Header/GlobalSettings';
import { useCurrentState } from '@/_stores/currentStateStore';
import { resolveReferences } from '@/_helpers/utils';
import { useCurrentState } from '@/_stores/currentStateStore';
export const LeftSidebar = forwardRef((props, ref) => {
const router = useRouter();
@ -46,7 +46,6 @@ export const LeftSidebar = forwardRef((props, ref) => {
updatePageHandle,
showHideViewerNavigationControls,
updateOnSortingPages,
updateOnPageLoadEvents,
apps,
clonePage,
setEditorMarginLeft,
@ -54,10 +53,8 @@ export const LeftSidebar = forwardRef((props, ref) => {
toggleAppMaintenance,
app,
disableEnablePage,
slug,
handleSlugChange,
isMaintenanceOn,
} = props;
const { is_maintenance_on } = app;
const dataSources = useDataSources();
const prevSelectedSidebarItem = localStorage.getItem('selectedSidebarItem');
@ -80,8 +77,9 @@ export const LeftSidebar = forwardRef((props, ref) => {
}),
shallow
);
const [pinned, setPinned] = useState(!!localStorage.getItem('selectedSidebarItem'));
const currentState = useCurrentState();
const [pinned, setPinned] = useState(!!localStorage.getItem('selectedSidebarItem'));
const [realState, setRealState] = useState(currentState);
const { errorLogs, clearErrorLogs, unReadErrorCount, allLog } = useDebugger({
@ -142,9 +140,10 @@ export const LeftSidebar = forwardRef((props, ref) => {
sideBarBtnRefs.current[page] = ref;
};
useEffect(() => {
setRealState(currentState);
setRealState(currentState); //!ceck this
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [currentState.components]);
const backgroundFxQuery = appDefinition?.globalSettings?.backgroundFxQuery;
const SELECTED_ITEMS = {
@ -164,11 +163,14 @@ export const LeftSidebar = forwardRef((props, ref) => {
updateHomePage={updateHomePage}
updatePageHandle={updatePageHandle}
clonePage={clonePage}
pages={Object.entries(appDefinition.pages).map(([id, page]) => ({ id, ...page })) || []}
pages={
Object.entries(_.cloneDeep(appDefinition).pages)
.map(([id, page]) => ({ id, ...page }))
.sort((a, b) => a.index - b.index) || []
}
homePageId={appDefinition.homePageId}
showHideViewerNavigationControls={showHideViewerNavigationControls}
updateOnSortingPages={updateOnSortingPages}
updateOnPageLoadEvents={updateOnPageLoadEvents}
apps={apps}
setPinned={handlePin}
pinned={pinned}
@ -221,21 +223,19 @@ export const LeftSidebar = forwardRef((props, ref) => {
globalSettings={appDefinition.globalSettings}
darkMode={darkMode}
toggleAppMaintenance={toggleAppMaintenance}
is_maintenance_on={is_maintenance_on}
isMaintenanceOn={isMaintenanceOn}
app={app}
realState={currentState}
backgroundFxQuery={backgroundFxQuery}
realState={realState}
slug={slug}
handleSlugChange={handleSlugChange}
/>
),
};
useEffect(() => {
backgroundFxQuery &&
globalSettingsChanged('canvasBackgroundColor', resolveReferences(backgroundFxQuery, realState));
globalSettingsChanged({ canvasBackgroundColor: resolveReferences(backgroundFxQuery, currentState) });
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [JSON.stringify(resolveReferences(backgroundFxQuery, realState))]);
}, [JSON.stringify(resolveReferences(backgroundFxQuery, currentState))]);
return (
<div className="left-sidebar" data-cy="left-sidebar-inspector">

View file

@ -13,6 +13,7 @@ import SolidIcon from '@/_ui/Icon/SolidIcons';
import cx from 'classnames';
import { ToolTip } from '@/_components/ToolTip';
import { TOOLTIP_MESSAGES } from '@/_helpers/constants';
import { useAppDataStore } from '@/_stores/appDataStore';
class ManageAppUsersComponent extends React.Component {
constructor(props) {
@ -21,7 +22,7 @@ class ManageAppUsersComponent extends React.Component {
this.state = {
showModal: false,
app: { ...props.app },
appId: null,
isLoading: true,
isSlugVerificationInProgress: false,
addingUser: false,
@ -49,14 +50,14 @@ class ManageAppUsersComponent extends React.Component {
};
componentDidMount() {
const appId = this.props.app.id;
this.fetchAppUsers();
const appId = this.props.appId;
this.fetchAppUsers(appId);
this.setState({ appId });
}
fetchAppUsers = () => {
fetchAppUsers = (appId) => {
appsService
.getAppUsers(this.props.app.id)
.getAppUsers(appId)
.then((data) =>
this.setState({
users: data.users,
@ -65,7 +66,8 @@ class ManageAppUsersComponent extends React.Component {
)
.catch((error) => {
this.setState({ isLoading: false });
toast.error(error);
const errorMessage = error?.message || 'Something went wrong';
toast.error(errorMessage);
});
};
@ -89,11 +91,11 @@ class ManageAppUsersComponent extends React.Component {
const { organizationUserId, role } = this.state.newUser;
appService
.createAppUser(this.state.app.id, organizationUserId, role)
.createAppUser(this.state.appId, organizationUserId, role)
.then(() => {
this.setState({ addingUser: false, newUser: {} });
toast.success('Added user successfully');
this.fetchAppUsers();
this.fetchAppUsers(this.state.appId);
})
.catch(({ error }) => {
this.setState({ addingUser: false });
@ -102,21 +104,19 @@ class ManageAppUsersComponent extends React.Component {
};
toggleAppVisibility = () => {
const newState = !this.state.app.is_public;
const newState = !this.props.isPublic;
this.setState({
ischangingVisibility: true,
});
useAppDataStore.getState().actions.updateState({ isPublic: newState });
// eslint-disable-next-line no-unused-vars
appsService
.setVisibility(this.state.app.id, newState)
.setVisibility(this.state.appId, newState)
.then(() => {
this.setState({
ischangingVisibility: false,
app: {
...this.state.app,
is_public: newState,
},
});
if (newState) {
@ -153,7 +153,7 @@ class ManageAppUsersComponent extends React.Component {
isSlugVerificationInProgress: true,
});
appsService
.setSlug(this.state.app.id, value)
.setSlug(this.state.appId, value)
.then(() => {
this.setState({
newSlug: {
@ -163,8 +163,9 @@ class ManageAppUsersComponent extends React.Component {
isSlugVerificationInProgress: false,
isSlugUpdated: true,
});
this.props.handleSlugChange(value);
replaceEditorURL(value, this.props.pageHandle);
useAppDataStore.getState().actions.updateState({ slug: value });
})
.catch(({ error }) => {
this.setState({
@ -189,8 +190,8 @@ class ManageAppUsersComponent extends React.Component {
};
render() {
const { isLoading, app, isSlugVerificationInProgress, newSlug, isSlugUpdated } = this.state;
const appId = app.id;
const { isLoading, appId, isSlugVerificationInProgress, newSlug, isSlugUpdated } = this.state;
const appLink = `${getHostURL()}/applications/`;
const shareableLink = appLink + (this.props.slug || appId);
const slugButtonClass = !_.isEmpty(newSlug.error) ? 'is-invalid' : 'is-valid';
@ -249,7 +250,7 @@ class ManageAppUsersComponent extends React.Component {
className="form-check-input"
type="checkbox"
onClick={this.toggleAppVisibility}
checked={this.state.app.is_public}
checked={this?.props?.isPublic}
disabled={this.state.ischangingVisibility}
data-cy="make-public-app-toggle"
/>
@ -356,7 +357,7 @@ class ManageAppUsersComponent extends React.Component {
<label className="label label-info">{`URL-friendly 'slug' consists of lowercase letters, numbers, and hyphens`}</label>
)}
</div>
{(this.state.app.is_public || window?.public_config?.ENABLE_PRIVATE_APP_EMBED === 'true') && (
{(this?.props?.isPublic || window?.public_config?.ENABLE_PRIVATE_APP_EMBED === 'true') && (
<div className="tj-app-input">
<label className="field-name">Embedded app link</label>
<span className={`tj-text-input justify-content-between ${this.props.darkMode ? 'dark' : ''}`}>

View file

@ -14,7 +14,7 @@ import { EventManager } from '@/Editor/Inspector/EventManager';
import { staticDataSources, customToggles, mockDataQueryAsComponent } from '../constants';
import { DataSourceTypes } from '../../DataSourceManager/SourceComponents';
import { useDataSources, useGlobalDataSources } from '@/_stores/dataSourcesStore';
import { useDataQueriesActions, useDataQueriesStore } from '@/_stores/dataQueriesStore';
import { useDataQueriesActions } from '@/_stores/dataQueriesStore';
import { useSelectedQuery, useSelectedDataSource } from '@/_stores/queryPanelStore';
import { useAppVersionStore } from '@/_stores/appVersionStore';
import { shallow } from 'zustand/shallow';
@ -97,14 +97,6 @@ export const QueryManagerBody = ({
validateNewOptions(newOptions);
};
const eventsChanged = (events) => {
optionchanged('events', events);
//added this here since the subscriber added in QueryManager component does not detect this change
useDataQueriesStore
.getState()
.actions.saveData({ ...selectedQuery, options: { ...selectedQuery.options, events: events } });
};
const toggleOption = (option) => {
const currentValue = selectedQuery?.options?.[option] ?? false;
optionchanged(option, !currentValue);
@ -185,9 +177,9 @@ export const QueryManagerBody = ({
<div className={`form-label`}>{t('editor.queryManager.eventsHandler', 'Events')}</div>
<div className="query-manager-events pb-4 flex-grow-1">
<EventManager
eventsChanged={eventsChanged}
component={queryComponent.component}
componentMeta={queryComponent.componentMeta}
sourceId={selectedQuery?.id}
eventSourceType="data_query" //check
eventMetaDefinition={queryComponent.componentMeta}
currentState={currentState}
components={allComponents}
callerQueryId={selectedQueryId}

View file

@ -1,35 +1,21 @@
import React, { useEffect, useState, useRef } from 'react';
import React, { useEffect, useState } from 'react';
import cx from 'classnames';
import { QueryManagerHeader } from './Components/QueryManagerHeader';
import { QueryManagerBody } from './Components/QueryManagerBody';
import { runQuery } from '@/_helpers/appUtils';
import { defaultSources } from './constants';
import { useQueryCreationLoading, useQueryUpdationLoading } from '@/_stores/dataQueriesStore';
import { useDataSources, useGlobalDataSources, useLoadingDataSources } from '@/_stores/dataSourcesStore';
import { useQueryToBeRun, useSelectedQuery, useQueryPanelActions } from '@/_stores/queryPanelStore';
const QueryManager = ({ mode, dataQueriesChanged, appId, darkMode, apps, allComponents, appDefinition, editorRef }) => {
const QueryManager = ({ mode, appId, darkMode, apps, allComponents, appDefinition, editorRef }) => {
const loadingDataSources = useLoadingDataSources();
const dataSources = useDataSources();
const globalDataSources = useGlobalDataSources();
const queryToBeRun = useQueryToBeRun();
const isCreationInProcess = useQueryCreationLoading();
const isUpdationInProcess = useQueryUpdationLoading();
const selectedQuery = useSelectedQuery();
const { setSelectedDataSource, setQueryToBeRun } = useQueryPanelActions();
const [options, setOptions] = useState({});
const mounted = useRef(false);
/** TODO: Below effect primarily used only for websocket invocation post update. Can be removed onece websocket logic is revamped */
useEffect(() => {
if (mounted.current && !isCreationInProcess && !isUpdationInProcess) {
return dataQueriesChanged();
}
mounted.current = true;
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isCreationInProcess, isUpdationInProcess, mounted.current]);
useEffect(() => {
setOptions(selectedQuery?.options || {});

View file

@ -15,7 +15,6 @@ const QueryPanel = ({
dataQueriesChanged,
fetchDataQueries,
darkMode,
apps,
allComponents,
appId,
appDefinition,
@ -203,7 +202,6 @@ const QueryPanel = ({
dataQueriesChanged={updateDataQueries}
appId={appId}
darkMode={darkMode}
apps={apps}
allComponents={allComponents}
appDefinition={appDefinition}
editorRef={editorRef}

View file

@ -1,8 +1,9 @@
import React from 'react';
import React, { useEffect } from 'react';
import Popover from '@/_ui/Popover';
import Avatar from '@/_ui/Avatar';
// eslint-disable-next-line import/no-unresolved
import { useOthers, useSelf } from '@y-presence/react';
import { useAppDataActions, useAppInfo } from '@/_stores/appDataStore';
const MAX_DISPLAY_USERS = 2;
const RealtimeAvatars = ({ darkMode }) => {
@ -17,6 +18,17 @@ const RealtimeAvatars = ({ darkMode }) => {
const getAvatarText = (presence) => presence.firstName?.charAt(0) + presence.lastName?.charAt(0);
const getAvatarTitle = (presence) => `${presence.firstName} ${presence.lastName}`;
const { updateState } = useAppDataActions();
const { areOthersOnSameVersionAndPage, currentVersionId } = useAppInfo();
useEffect(() => {
const areActiveUsersOnSameVersionAndPage = othersOnSameVersionAndPage.length > 0;
const shouldUpdateState = areActiveUsersOnSameVersionAndPage !== areOthersOnSameVersionAndPage;
if (shouldUpdateState) updateState({ areOthersOnSameVersionAndPage: areActiveUsersOnSameVersionAndPage });
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [JSON.stringify({ others, self, currentVersionId })]);
const popoverContent = () => {
return othersOnSameVersionAndPage
.slice(MAX_DISPLAY_USERS, othersOnSameVersionAndPage.length)

View file

@ -8,13 +8,7 @@ import { ConfirmDialog } from '@/_components/ConfirmDialog';
import { shallow } from 'zustand/shallow';
import { ButtonSolid } from '@/_ui/AppButton/AppButton';
export const ReleaseVersionButton = function DeployVersionButton({
appId,
appName,
fetchApp,
onVersionRelease,
saveEditingVersion,
}) {
export const ReleaseVersionButton = function DeployVersionButton({ appId, appName, fetchApp, onVersionRelease }) {
const [isReleasing, setIsReleasing] = useState(false);
const { isVersionReleased, editingVersion } = useAppVersionStore(
(state) => ({
@ -29,7 +23,7 @@ export const ReleaseVersionButton = function DeployVersionButton({
const releaseVersion = (editingVersion) => {
setShowPageDeletionConfirmation(false);
setIsReleasing(true);
saveEditingVersion();
appsService
.saveApp(appId, {
name: appName,

View file

@ -15,6 +15,8 @@ import { useCurrentState } from '@/_stores/currentStateStore';
import { useAppVersionStore } from '@/_stores/appVersionStore';
import { shallow } from 'zustand/shallow';
import { useMounted } from '@/_hooks/use-mount';
// eslint-disable-next-line import/no-unresolved
import { diff } from 'deep-object-diff';
const NO_OF_GRIDS = 43;
@ -92,17 +94,19 @@ export const SubContainer = ({
false;
const getChildWidgets = (components) => {
let childWidgets = [];
let childWidgets = {};
Object.keys(components).forEach((key) => {
if (components[key].parent === parent) {
const componentParent = components[key].component.parent;
if (componentParent === parent) {
childWidgets[key] = { ...components[key], component: { ...components[key]['component'], parent } };
}
});
return childWidgets;
};
const [boxes, setBoxes] = useState(allComponents);
const [childWidgets, setChildWidgets] = useState(() => getChildWidgets(allComponents));
const [childWidgets, setChildWidgets] = useState([]);
const [isDragging, setIsDragging] = useState(false);
const [isResizing, setIsResizing] = useState(false);
// const [subContainerHeight, setSubContainerHeight] = useState('100%'); //used to determine the height of the sub container for modal
@ -111,6 +115,7 @@ export const SubContainer = ({
useEffect(() => {
setBoxes(allComponents);
setChildWidgets(() => getChildWidgets(allComponents));
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [allComponents, parent]);
@ -183,8 +188,11 @@ export const SubContainer = ({
);
_.set(childrenBoxes, newComponent.id, {
component: newComponent.component,
parent: parentComponent.component === 'Tabs' ? parentId + '-' + tab : parentId,
component: {
...newComponent.component,
parent: parentComponent.component === 'Tabs' ? parentId + '-' + tab : parentId,
},
layouts: {
[currentLayout]: {
...layout,
@ -233,7 +241,23 @@ export const SubContainer = ({
},
},
};
appDefinitionChanged(newDefinition);
const oldComponents = appDefinition.pages[currentPageId]?.components ?? {};
const newComponents = boxes;
const componendAdded = Object.keys(newComponents).length > Object.keys(oldComponents).length;
const opts = { containerChanges: true };
if (componendAdded) {
opts.componentAdded = true;
}
const shouldUpdate = !_.isEmpty(diff(appDefinition, newDefinition));
if (shouldUpdate) {
appDefinitionChanged(newDefinition, opts);
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [boxes]);
@ -271,6 +295,7 @@ export const SubContainer = ({
}
});
//!Todo: need to check: this never gets called as draggingState is always false
useEffect(() => {
setIsDragging(draggingState);
}, [draggingState]);
@ -306,8 +331,10 @@ export const SubContainer = ({
setBoxes({
...boxes,
[newComponent.id]: {
component: newComponent.component,
parent: parentRef.current.id,
component: {
...newComponent.component,
parent: parentRef.current.id,
},
layouts: {
...newComponent.layout,
},
@ -357,6 +384,7 @@ export const SubContainer = ({
enableReleasedVersionPopupState();
return;
}
const canvasWidth = getContainerCanvasWidth();
const nodeBounds = direction.node.getBoundingClientRect();
@ -430,7 +458,12 @@ export const SubContainer = ({
}
//round the width to nearest multiple of gridwidth before converting to %
const currentWidth = (_containerCanvasWidth * width) / NO_OF_GRIDS;
let currentWidth = (_containerCanvasWidth * width) / NO_OF_GRIDS;
if (currentWidth > _containerCanvasWidth) {
currentWidth = _containerCanvasWidth;
}
let newWidth = currentWidth + deltaWidth;
newWidth = Math.round(newWidth / gridWidth) * gridWidth;
width = (newWidth * NO_OF_GRIDS) / _containerCanvasWidth;
@ -536,9 +569,11 @@ export const SubContainer = ({
Object.keys(childWidgets).map((key) => {
const addDefaultChildren = childWidgets[key]['withDefaultChildren'] || false;
const box = childWidgets[key];
const canShowInCurrentLayout =
box.component.definition.others[currentLayout === 'mobile' ? 'showOnMobile' : 'showOnDesktop'].value;
if (box.parent && resolveReferences(canShowInCurrentLayout, currentState)) {
if (box.component.parent && resolveReferences(canShowInCurrentLayout, currentState)) {
return (
<DraggableBox
onComponentClick={onComponentClick}
@ -577,6 +612,7 @@ export const SubContainer = ({
onComponentHover={onComponentHover}
hoveredComponent={hoveredComponent}
parentId={parentComponent?.name}
parent={parent}
sideBarDebugger={sideBarDebugger}
exposedVariables={exposedVariables ?? {}}
childComponents={childComponents[key]}

View file

@ -1,9 +1,10 @@
import React from 'react';
import {
appsService,
authenticationService,
orgEnvironmentVariableService,
orgEnvironmentConstantService,
dataqueryService,
appService,
} from '@/_services';
import { DndProvider } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend';
@ -18,6 +19,7 @@ import {
onEvent,
runQuery,
computeComponentState,
buildAppDefinition,
} from '@/_helpers/appUtils';
import queryString from 'query-string';
import ViewerLogoIcon from './Icons/viewer-logo.svg';
@ -33,9 +35,8 @@ import { setCookie } from '@/_helpers/cookie';
import { useDataQueriesStore } from '@/_stores/dataQueriesStore';
import { useCurrentStateStore } from '@/_stores/currentStateStore';
import { shallow } from 'zustand/shallow';
import { useAppDataStore } from '@/_stores/appDataStore';
import { getPreviewQueryParams, redirectToDashboard, redirectToErrorPage } from '@/_helpers/routes';
import toast from 'react-hot-toast';
import { useAppDataActions, useAppDataStore } from '@/_stores/appDataStore';
import { getPreviewQueryParams, redirectToErrorPage } from '@/_helpers/routes';
import { ERROR_TYPES } from '@/_helpers/constants';
class ViewerComponent extends React.Component {
@ -54,31 +55,44 @@ class ViewerComponent extends React.Component {
isLoading: true,
users: null,
appDefinition: { pages: {} },
queryConfirmationList: [],
isAppLoaded: false,
pages: {},
homepage: null,
};
}
setStateForApp = (data) => {
const copyDefinition = _.cloneDeep(data.definition);
const pagesObj = copyDefinition.pages || {};
const newDefinition = {
...copyDefinition,
pages: pagesObj,
getViewerRef() {
return {
appDefinition: this.state.appDefinition,
queryConfirmationList: this.props.queryConfirmationList,
updateQueryConfirmationList: this.updateQueryConfirmationList,
navigate: this.props.navigate,
switchPage: this.switchPage,
currentPageId: this.state.currentPageId,
};
}
setStateForApp = (data, byAppSlug = false) => {
const appDefData = buildAppDefinition(data);
if (byAppSlug) {
appDefData.globalSettings = data.globalSettings;
appDefData.homePageId = data.homePageId;
appDefData.showViewerNavigation = data.showViewerNavigation;
}
this.setState({
app: data,
isLoading: false,
isAppLoaded: true,
appDefinition: newDefinition || { components: {} },
appDefinition: { ...appDefData },
pages: appDefData.pages,
});
};
setStateForContainer = async (data) => {
setStateForContainer = async (data, appVersionId) => {
const appDefData = buildAppDefinition(data);
const currentUser = this.state.currentUser;
let userVars = {};
@ -94,38 +108,57 @@ class ViewerComponent extends React.Component {
let mobileLayoutHasWidgets = false;
if (this.props.currentLayout === 'mobile') {
const currentComponents = data.definition.pages[data.definition.homePageId].components;
const currentComponents = appDefData.pages[appDefData.homePageId].components;
mobileLayoutHasWidgets =
Object.keys(currentComponents).filter((componentId) => currentComponents[componentId]['layouts']['mobile'])
.length > 0;
}
let queryState = {};
data.data_queries.forEach((query) => {
if (query.pluginId || query?.plugin?.id) {
queryState[query.name] = {
...query.plugin.manifestFile.data.source.exposedVariables,
...this.props.currentState.queries[query.name],
};
} else {
const dataSourceTypeDetail = DataSourceTypes.find((source) => source.kind === query.kind);
queryState[query.name] = {
...dataSourceTypeDetail.exposedVariables,
...this.props.currentState.queries[query.name],
};
}
});
let dataQueries = [];
if (appVersionId) {
const { data_queries } = await dataqueryService.getAll(appVersionId);
dataQueries = data_queries;
} else {
dataQueries = data.data_queries;
}
const queryConfirmationList = [];
if (dataQueries.length > 0) {
dataQueries.forEach((query) => {
if (query?.options && query?.options?.requestConfirmation && query?.options?.runOnPageLoad) {
queryConfirmationList.push({ queryId: query.id, queryName: query.name });
}
if (query.pluginId || query?.plugin?.id) {
queryState[query.name] = {
...query.plugin.manifestFile.data.source.exposedVariables,
...this.props.currentState.queries[query.name],
};
} else {
const dataSourceTypeDetail = DataSourceTypes.find((source) => source.kind === query.kind);
queryState[query.name] = {
...dataSourceTypeDetail.exposedVariables,
...this.props.currentState.queries[query.name],
};
}
});
}
if (queryConfirmationList.length !== 0) {
this.updateQueryConfirmationList(queryConfirmationList);
}
const variables = await this.fetchOrgEnvironmentVariables(data.slug, data.is_public);
const constants = await this.fetchOrgEnvironmentConstants(data.slug, data.is_public);
const pages = Object.entries(data.definition.pages).map(([pageId, page]) => ({ id: pageId, ...page }));
const homePageId = data.definition.homePageId;
const pages = data.pages;
const homePageId = appVersionId ? data.editing_version.homePageId : data?.homePageId;
const startingPageHandle = this.props?.params?.pageHandle;
const currentPageId = pages.filter((page) => page.handle === startingPageHandle)[0]?.id ?? homePageId;
const currentPage = pages.find((page) => page.id === currentPageId);
useDataQueriesStore.getState().actions.setDataQueries(data.data_queries);
useDataQueriesStore.getState().actions.setDataQueries(dataQueries);
this.props.setCurrentState({
queries: queryState,
components: {},
@ -148,6 +181,7 @@ class ViewerComponent extends React.Component {
...constants,
});
useEditorStore.getState().actions.toggleCurrentLayout(mobileLayoutHasWidgets ? 'mobile' : 'desktop');
this.props.updateState({ events: data.events ?? [] });
this.setState(
{
currentUser,
@ -159,21 +193,24 @@ class ViewerComponent extends React.Component {
? `${this.state.deviceWindowWidth}px`
: '1292px',
selectedComponent: null,
dataQueries: data.data_queries,
dataQueries: dataQueries,
currentPageId: currentPage.id,
pages: {},
homepage: this.state.appDefinition?.pages?.[this.state.appDefinition?.homePageId]?.handle,
homepage: appDefData?.pages?.[this.state.appDefinition?.homePageId]?.handle,
events: data.events ?? [],
},
() => {
computeComponentState(this, data?.definition?.pages[currentPage.id]?.components).then(async () => {
this.setState({ initialComputationOfStateDone: true });
const components = appDefData?.pages[currentPageId]?.components || {};
computeComponentState(components).then(async () => {
this.setState({ initialComputationOfStateDone: true, defaultComponentStateComputed: true });
console.log('Default component state computed and set');
this.runQueries(data.data_queries);
// eslint-disable-next-line no-unsafe-optional-chaining
const { events } = this.state.appDefinition?.pages[this.state.currentPageId];
for (const event of events ?? []) {
await this.handleEvent(event.eventId, event);
}
this.runQueries(dataQueries);
const currentPageEvents = this.state.events.filter(
(event) => event.target === 'page' && event.sourceId === this.state.currentPageId
);
await this.handleEvent('onPageLoad', currentPageEvents);
});
}
);
@ -182,7 +219,7 @@ class ViewerComponent extends React.Component {
runQueries = (data_queries) => {
data_queries.forEach((query) => {
if (query.options.runOnPageLoad && isQueryRunnable(query)) {
runQuery(this, query.id, query.name, undefined, 'view');
runQuery(this.getViewerRef(), query.id, query.name, undefined, 'view');
}
});
};
@ -239,13 +276,14 @@ class ViewerComponent extends React.Component {
};
loadApplicationBySlug = (slug, authentication_failed = false) => {
appsService
.getAppBySlug(slug)
appService
.fetchAppBySlug(slug)
.then((data) => {
if (authentication_failed && !data.current_version_id) {
redirectToErrorPage(ERROR_TYPES.URL_UNAVAILABLE, {});
const isAppPublic = data?.is_public;
if (authentication_failed && !isAppPublic) {
return redirectToErrorPage(ERROR_TYPES.URL_UNAVAILABLE, {});
}
this.setStateForApp(data);
this.setStateForApp(data, true);
this.setStateForContainer(data);
this.setWindowTitle(data.name);
})
@ -265,11 +303,11 @@ class ViewerComponent extends React.Component {
};
loadApplicationByVersion = (appId, versionId) => {
appsService
.getAppByVersion(appId, versionId)
appService
.fetchAppByVersion(appId, versionId)
.then((data) => {
this.setStateForApp(data);
this.setStateForContainer(data);
this.setStateForContainer(data, versionId);
})
.catch(() => {
this.setState({
@ -278,6 +316,9 @@ class ViewerComponent extends React.Component {
});
};
updateQueryConfirmationList = (queryConfirmationList) =>
useEditorStore.getState().actions.updateQueryConfirmationList(queryConfirmationList);
setupViewer() {
this.subscription = authenticationService.currentSession.subscribe((currentSession) => {
const slug = this.props.params.slug;
@ -306,6 +347,7 @@ class ViewerComponent extends React.Component {
userVars,
versionId,
});
versionId ? this.loadApplicationByVersion(appId, versionId) : this.loadApplicationBySlug(slug);
} else if (currentSession?.authentication_failed) {
this.loadApplicationBySlug(slug, true);
@ -352,9 +394,13 @@ class ViewerComponent extends React.Component {
handlePageSwitchingBasedOnURLparam() {
const handleOnURL = this.props.params.pageHandle;
const pageIdCorrespondingToHandleOnURL = handleOnURL
? this.findPageIdFromHandle(handleOnURL)
: this.state.appDefinition.homePageId;
const shouldShowPage = handleOnURL ? this.validatePageHandle(handleOnURL) : true;
if (!shouldShowPage) return this.switchPage(this.state.appDefinition.homePageId);
const pageIdCorrespondingToHandleOnURL =
handleOnURL && shouldShowPage ? this.findPageIdFromHandle(handleOnURL) : this.state.appDefinition.homePageId;
const currentPageId = this.state.currentPageId;
if (pageIdCorrespondingToHandleOnURL != this.state.currentPageId) {
@ -388,20 +434,23 @@ class ViewerComponent extends React.Component {
name: targetPage.name,
},
async () => {
computeComponentState(this, this.state.appDefinition?.pages[this.state.currentPageId].components).then(
async () => {
// eslint-disable-next-line no-unsafe-optional-chaining
const { events } = this.state.appDefinition?.pages[this.state.currentPageId];
for (const event of events ?? []) {
await this.handleEvent(event.eventId, event);
}
}
);
computeComponentState(this.state.appDefinition?.pages[this.state.currentPageId].components).then(async () => {
const currentPageEvents = this.state.events.filter(
(event) => event.target === 'page' && event.sourceId === this.state.currentPageId
);
await this.handleEvent('onPageLoad', currentPageEvents);
});
}
);
}
}
validatePageHandle(handle) {
const allPages = this.state.appDefinition.pages;
return Object.values(allPages).some((page) => page.handle === handle && !page.disabled);
}
findPageIdFromHandle(handle) {
return (
Object.entries(this.state.appDefinition.pages).filter(([_id, page]) => page.handle === handle)?.[0]?.[0] ??
@ -466,7 +515,9 @@ class ViewerComponent extends React.Component {
);
};
handleEvent = (eventName, options) => onEvent(this, eventName, options, 'view');
handleEvent = (eventName, events, options) => {
return onEvent(this.getViewerRef(), eventName, events, options, 'view');
};
computeCanvasMaxWidth = () => {
const { appDefinition } = this.state;
@ -493,11 +544,11 @@ class ViewerComponent extends React.Component {
deviceWindowWidth,
defaultComponentStateComputed,
dataQueries,
queryConfirmationList,
canvasWidth,
} = this.state;
const currentCanvasWidth = canvasWidth;
const queryConfirmationList = this.props?.queryConfirmationList ?? [];
const canvasMaxWidth = this.computeCanvasMaxWidth();
@ -531,8 +582,10 @@ class ViewerComponent extends React.Component {
<Confirm
show={queryConfirmationList.length > 0}
message={'Do you want to run this query?'}
onConfirm={(queryConfirmationData) => onQueryConfirmOrCancel(this, queryConfirmationData, true, 'view')}
onCancel={() => onQueryConfirmOrCancel(this, queryConfirmationList[0], false, 'view')}
onConfirm={(queryConfirmationData) =>
onQueryConfirmOrCancel(this.getViewerRef(), queryConfirmationData, true, 'view')
}
onCancel={() => onQueryConfirmOrCancel(this.getViewerRef(), queryConfirmationList[0], false, 'view')}
queryConfirmationData={queryConfirmationList[0]}
key={queryConfirmationList[0]?.queryName}
/>
@ -591,7 +644,7 @@ class ViewerComponent extends React.Component {
snapToGrid={true}
appLoading={isLoading}
darkMode={this.props.darkMode}
onEvent={(eventName, options) => onEvent(this, eventName, options, 'view')}
onEvent={this.handleEvent}
mode="view"
deviceWindowWidth={deviceWindowWidth}
selectedComponent={this.state.selectedComponent}
@ -602,11 +655,9 @@ class ViewerComponent extends React.Component {
onComponentClick(this, id, component, 'view');
}}
onComponentOptionChanged={(component, optionName, value) => {
return onComponentOptionChanged(this, component, optionName, value);
return onComponentOptionChanged(component, optionName, value);
}}
onComponentOptionsChanged={(component, options) =>
onComponentOptionsChanged(this, component, options)
}
onComponentOptionsChanged={onComponentOptionsChanged}
canvasWidth={this.getCanvasWidth()}
dataQueries={dataQueries}
currentPageId={this.state.currentPageId}
@ -629,19 +680,23 @@ class ViewerComponent extends React.Component {
}
const withStore = (Component) => (props) => {
const currentState = useCurrentStateStore();
const { currentLayout } = useEditorStore(
const { currentLayout, queryConfirmationList } = useEditorStore(
(state) => ({
currentLayout: state?.currentLayout,
queryConfirmationList: state?.queryConfirmationList,
}),
shallow
);
const { updateState } = useAppDataActions();
return (
<Component
{...props}
currentState={currentState}
setCurrentState={currentState?.actions?.setCurrentState}
currentLayout={currentLayout}
updateState={updateState}
queryConfirmationList={queryConfirmationList}
/>
);
};

View file

@ -54,7 +54,7 @@ export default function ExportAppModal({ title, show, closeModal, customClassNam
const requestBody = {
...appOpts,
...(exportTjDb && { tooljet_database: tables }),
organization_id: app.organization_id,
organization_id: app.organization_id ?? app.organizationId,
};
appsService

View file

@ -32,6 +32,8 @@ import { useDataQueriesStore } from '@/_stores/dataQueriesStore';
import { useQueryPanelStore } from '@/_stores/queryPanelStore';
import { useCurrentStateStore, getCurrentState } from '@/_stores/currentStateStore';
import { useAppVersionStore } from '@/_stores/appVersionStore';
import { camelizeKeys } from 'humps';
import { useAppDataStore } from '@/_stores/appDataStore';
import { useEditorStore } from '@/_stores/editorStore';
const ERROR_TYPES = Object.freeze({
@ -60,7 +62,7 @@ export function setCurrentStateAsync(_ref, changes) {
});
}
export function onComponentOptionsChanged(_ref, component, options) {
export function onComponentOptionsChanged(component, options) {
const componentName = component.name;
const components = getCurrentState().components;
let componentData = components[componentName];
@ -76,7 +78,7 @@ export function onComponentOptionsChanged(_ref, component, options) {
return Promise.resolve();
}
export function onComponentOptionChanged(_ref, component, option_name, value) {
export function onComponentOptionChanged(component, option_name, value) {
const componentName = component.name;
const components = getCurrentState().components;
let componentData = components[componentName];
@ -322,12 +324,11 @@ export async function runTransformation(
}
}
export async function executeActionsForEventId(_ref, eventId, component, mode, customVariables) {
const events = component?.definition?.events || [];
const filteredEvents = events.filter((event) => event.eventId === eventId);
export async function executeActionsForEventId(_ref, eventId, events = [], mode, customVariables) {
const filteredEvents = events.filter((event) => event?.event.eventId === eventId);
for (const event of filteredEvents) {
await executeAction(_ref, event, mode, customVariables); // skipcq: JS-0032
await executeAction(_ref, event.event, mode, customVariables); // skipcq: JS-0032
}
}
@ -336,13 +337,11 @@ export function onComponentClick(_ref, id, component, mode = 'edit') {
}
export function onQueryConfirmOrCancel(_ref, queryConfirmationData, isConfirm = false, mode = 'edit') {
const filtertedQueryConfirmation = _ref.state?.queryConfirmationList.filter(
const filtertedQueryConfirmation = _ref?.queryConfirmationList.filter(
(query) => query.queryId !== queryConfirmationData.queryId
);
_ref.setState({
queryConfirmationList: filtertedQueryConfirmation,
});
_ref.updateQueryConfirmationList(filtertedQueryConfirmation, 'check');
isConfirm && runQuery(_ref, queryConfirmationData.queryId, queryConfirmationData.queryName, true, mode);
}
@ -362,7 +361,7 @@ function showModal(_ref, modal, show) {
return Promise.resolve();
}
const modalMeta = _ref.state.appDefinition.pages[_ref.state.currentPageId].components[modalId];
const modalMeta = _ref.appDefinition.pages[_ref.currentPageId].components[modalId]; //! NeedToFix
const _components = {
...getCurrentState().components,
@ -377,7 +376,7 @@ function showModal(_ref, modal, show) {
return Promise.resolve();
}
function logoutAction(_ref) {
function logoutAction() {
localStorage.clear();
authenticationService.logout(true);
@ -442,7 +441,7 @@ function executeActionWithDebounce(_ref, event, mode, customVariables) {
return runQuery(_ref, queryId, name, undefined, mode, resolvedParams);
}
case 'logout': {
return logoutAction(_ref);
return logoutAction();
}
case 'open-webpage': {
@ -477,7 +476,7 @@ function executeActionWithDebounce(_ref, event, mode, customVariables) {
}
if (mode === 'view') {
_ref.props.navigate(url);
_ref.navigate(url);
} else {
if (confirm('The app will be opened in a new tab as the action is triggered from the editor.')) {
window.open(urlJoin(window.public_config?.TOOLJET_HOST, url));
@ -522,7 +521,7 @@ function executeActionWithDebounce(_ref, event, mode, customVariables) {
}
case 'set-table-page': {
setTablePageIndex(_ref, event.table, event.pageIndex);
setTablePageIndex(event.table, event.pageIndex);
break;
}
@ -585,12 +584,13 @@ function executeActionWithDebounce(_ref, event, mode, customVariables) {
}
case 'switch-page': {
const { name, disabled } = _ref.state.appDefinition.pages[event.pageId];
const { name, disabled } = _ref.appDefinition.pages[event.pageId];
// Don't allow switching to disabled page in editor as well as viewer
if (!disabled) {
_ref.switchPage(event.pageId, resolveReferences(event.queryParams, getCurrentState(), [], customVariables));
}
if (_ref.state.appDefinition.pages[event.pageId]) {
if (_ref.appDefinition.pages[event.pageId]) {
if (disabled) {
const generalProps = {
navToDisablePage: {
@ -612,12 +612,15 @@ function executeActionWithDebounce(_ref, event, mode, customVariables) {
}
}
export async function onEvent(_ref, eventName, options, mode = 'edit') {
export async function onEvent(_ref, eventName, events, options = {}, mode = 'edit') {
let _self = _ref;
const { customVariables } = options;
if (eventName === 'onPageLoad') {
await executeActionsForEventId(_ref, 'onPageLoad', { definition: { events: [options] } }, mode, customVariables);
//hack to make sure that the page is loaded before executing the actions
setTimeout(async () => {
return await executeActionsForEventId(_ref, 'onPageLoad', events, mode, customVariables);
}, 0);
}
if (eventName === 'onTrigger') {
@ -635,6 +638,7 @@ export async function onEvent(_ref, eventName, options, mode = 'edit') {
if (eventName === 'onCalendarEventSelect') {
const { component, calendarEvent } = options;
useCurrentStateStore.getState().actions.setCurrentState({
components: {
...getCurrentState().components,
@ -644,7 +648,8 @@ export async function onEvent(_ref, eventName, options, mode = 'edit') {
},
},
});
executeActionsForEventId(_ref, 'onCalendarEventSelect', component, mode, customVariables);
executeActionsForEventId(_ref, 'onCalendarEventSelect', events, mode, customVariables);
}
if (eventName === 'onCalendarSlotSelect') {
@ -658,26 +663,18 @@ export async function onEvent(_ref, eventName, options, mode = 'edit') {
},
},
});
executeActionsForEventId(_ref, 'onCalendarSlotSelect', component, mode, customVariables);
executeActionsForEventId(_ref, 'onCalendarSlotSelect', events, mode, customVariables);
}
if (eventName === 'onTableActionButtonClicked') {
const { component, data, action, rowId } = options;
useCurrentStateStore.getState().actions.setCurrentState({
components: {
...getCurrentState().components,
[component.name]: {
...getCurrentState().components[component.name],
selectedRow: data,
selectedRowId: rowId,
},
},
});
if (action && action.events) {
for (const event of action.events) {
if (event.actionId) {
// the event param uses a hacky workaround for using same format used by event manager ( multiple handlers )
await executeAction(_self, { ...event, ...event.options }, mode, customVariables);
const { action, tableActionEvents } = options;
const executeableActions = tableActionEvents.filter((event) => event?.event?.ref === action?.name);
if (action && executeableActions) {
for (const event of executeableActions) {
if (event?.event?.actionId) {
await executeAction(_self, event.event, mode, customVariables);
}
}
} else {
@ -686,23 +683,12 @@ export async function onEvent(_ref, eventName, options, mode = 'edit') {
}
if (eventName === 'OnTableToggleCellChanged') {
const { component, column, rowId, row } = options;
useCurrentStateStore.getState().actions.setCurrentState({
components: {
...getCurrentState().components,
[component.name]: {
...getCurrentState().components[component.name],
selectedRow: row,
selectedRowId: rowId,
},
},
});
const { column, tableColumnEvents } = options;
if (column && column.events) {
for (const event of column.events) {
if (event.actionId) {
// the event param uses a hacky workaround for using same format used by event manager ( multiple handlers )
await executeAction(_self, { ...event, ...event.options }, mode, customVariables);
if (column && tableColumnEvents) {
for (const event of tableColumnEvents) {
if (event?.event?.actionId) {
await executeAction(_self, event.event, mode, customVariables);
}
}
} else {
@ -763,18 +749,15 @@ export async function onEvent(_ref, eventName, options, mode = 'edit') {
'onNewRowsAdded',
].includes(eventName)
) {
const { component } = options;
executeActionsForEventId(_ref, eventName, component, mode, customVariables);
executeActionsForEventId(_ref, eventName, events, mode, customVariables);
}
if (eventName === 'onBulkUpdate') {
onComponentOptionChanged(_self, options.component, 'isSavingChanges', true);
await executeActionsForEventId(_self, eventName, options.component, mode, customVariables);
onComponentOptionChanged(_self, options.component, 'isSavingChanges', false);
await executeActionsForEventId(_self, eventName, events, mode, customVariables);
}
if (['onDataQuerySuccess', 'onDataQueryFailure'].includes(eventName)) {
await executeActionsForEventId(_self, eventName, options, mode, customVariables);
await executeActionsForEventId(_self, eventName, events, mode, customVariables);
}
}
@ -940,6 +923,10 @@ export function previewQuery(_ref, query, calledFromQuery = false, parameters =
export function runQuery(_ref, queryId, queryName, confirmed = undefined, mode = 'edit', parameters = {}) {
const query = useDataQueriesStore.getState().dataQueries.find((query) => query.id === queryId);
const queryEvents = useAppDataStore
.getState()
.events.filter((event) => event.target === 'data_query' && event.sourceId === queryId);
let dataQuery = {};
if (query) {
@ -951,9 +938,11 @@ export function runQuery(_ref, queryId, queryName, confirmed = undefined, mode =
const options = getQueryVariables(dataQuery.options, getCurrentState());
if (dataQuery.options.requestConfirmation) {
// eslint-disable-next-line no-unsafe-optional-chaining
const queryConfirmationList = _ref.state?.queryConfirmationList ? [..._ref.state?.queryConfirmationList] : [];
if (dataQuery.options?.requestConfirmation) {
const queryConfirmationList = useEditorStore.getState().queryConfirmationList
? [...useEditorStore.getState().queryConfirmationList]
: [];
const queryConfirmation = {
queryId,
queryName,
@ -963,9 +952,8 @@ export function runQuery(_ref, queryId, queryName, confirmed = undefined, mode =
}
if (confirmed === undefined) {
_ref.setState({
queryConfirmationList,
});
//!check
_ref.updateQueryConfirmationList(queryConfirmationList);
return;
}
}
@ -1071,9 +1059,7 @@ export function runQuery(_ref, queryId, queryName, confirmed = undefined, mode =
},
});
resolve(data);
onEvent(_self, 'onDataQueryFailure', {
definition: { events: dataQuery.options.events },
});
onEvent(_self, 'onDataQueryFailure', queryEvents);
if (mode !== 'view') {
const err = query.kind == 'tooljetdb' ? data?.error || data : _.isEmpty(data.data) ? data : data.data;
toast.error(err?.message);
@ -1111,9 +1097,7 @@ export function runQuery(_ref, queryId, queryName, confirmed = undefined, mode =
},
});
resolve(finalData);
onEvent(_self, 'onDataQueryFailure', {
definition: { events: dataQuery.options.events },
});
onEvent(_self, 'onDataQueryFailure', queryEvents);
return;
}
}
@ -1152,7 +1136,7 @@ export function runQuery(_ref, queryId, queryName, confirmed = undefined, mode =
},
});
resolve({ status: 'ok', data: finalData });
onEvent(_self, 'onDataQuerySuccess', { definition: { events: dataQuery.options.events } }, mode);
onEvent(_self, 'onDataQuerySuccess', queryEvents, mode);
}
})
.catch(({ error }) => {
@ -1171,7 +1155,7 @@ export function runQuery(_ref, queryId, queryName, confirmed = undefined, mode =
});
}
export function setTablePageIndex(_ref, tableId, index) {
export function setTablePageIndex(tableId, index) {
if (_.isEmpty(tableId)) {
console.log('No table is associated with this event.');
return Promise.resolve();
@ -1192,52 +1176,69 @@ export function renderTooltip({ props, text }) {
);
}
export function computeComponentState(_ref, components = {}) {
let componentState = {};
const currentComponents = getCurrentState().components;
Object.keys(components).forEach((key) => {
const component = components[key];
const componentMeta = componentTypes.find((comp) => component.component.component === comp.component);
/*
@computeComponentState: (components = {}) => Promise<void>
This change is made to enhance the code readability by optimizing the logic
for computing component state. It replaces the previous try-catch block with
a more efficient approach, precomputing the parent component types and using
conditional checks for better performance and error handling.*/
const existingComponentName = Object.keys(currentComponents).find((comp) => currentComponents[comp].id === key);
const existingValues = currentComponents[existingComponentName];
export function computeComponentState(components = {}) {
try {
let componentState = {};
const currentComponents = getCurrentState().components;
if (component.parent) {
const parentComponent = components[component.parent];
let isListView = false,
isForm = false;
try {
isListView = parentComponent.component.component === 'Listview';
isForm = parentComponent.component.component === 'Form';
} catch {
console.log('error');
}
// Precompute parent component types
const parentComponentTypes = {};
Object.keys(components).forEach((key) => {
const { component } = components[key];
parentComponentTypes[key] = component.component;
});
if (!isListView && !isForm) {
componentState[component.component.name] = {
Object.keys(components).forEach((key) => {
if (!components[key]) return;
const { component } = components[key];
const componentMeta = componentTypes.find((comp) => component.component === comp.component);
const existingComponentName = Object.keys(currentComponents).find((comp) => currentComponents[comp].id === key);
const existingValues = currentComponents[existingComponentName];
if (component.parent) {
const parentComponentType = parentComponentTypes[component.parent];
if (parentComponentType !== 'Listview' && parentComponentType !== 'Form') {
componentState[component.name] = {
...componentMeta.exposedVariables,
id: key,
...existingValues,
};
}
} else {
componentState[component.name] = {
...componentMeta.exposedVariables,
id: key,
...existingValues,
};
}
} else {
componentState[component.component.name] = {
...componentMeta.exposedVariables,
id: key,
...existingValues,
};
}
});
});
useCurrentStateStore.getState().actions.setCurrentState({
components: {
...componentState,
},
});
useCurrentStateStore.getState().actions.setCurrentState({
components: {
...componentState,
},
});
return setStateAsync(_ref, {
defaultComponentStateComputed: true,
});
return new Promise((resolve) => {
useEditorStore.getState().actions.updateEditorState({
defaultComponentStateComputed: true,
});
resolve();
});
} catch (error) {
console.log(error);
return Promise.reject(error);
}
}
export const getSvgIcon = (key, height = 50, width = 50, iconFile = undefined, styles = {}) => {
@ -1253,13 +1254,13 @@ export const getSvgIcon = (key, height = 50, width = 50, iconFile = undefined, s
};
export const debuggerActions = {
error: (_self, errors) => {
error: (errors) => {
useCurrentStateStore.getState().actions.setErrors({
...errors,
});
},
flush: (_self) => {
flush: () => {
useCurrentStateStore.getState().actions.setCurrentState({
errors: {},
});
@ -1370,105 +1371,148 @@ export const getComponentName = (currentState, id) => {
}
};
const updateNewComponents = (pageId, appDefinition, newComponents, updateAppDefinition) => {
const updateNewComponents = (pageId, appDefinition, newComponents, updateAppDefinition, componentMap, isCut) => {
const newAppDefinition = JSON.parse(JSON.stringify(appDefinition));
newComponents.forEach((newComponent) => {
newComponent.component.name = computeComponentName(
newComponent.component.component,
newAppDefinition.pages[pageId].components
);
newAppDefinition.pages[pageId].components[newComponent.id] = newComponent;
});
updateAppDefinition(newAppDefinition);
newAppDefinition.pages[pageId].components = {
...newAppDefinition.pages[pageId].components,
...newComponents,
};
const opts = {
componentAdded: true,
containerChanges: true,
};
if (!isCut) {
opts.cloningComponent = componentMap;
}
updateAppDefinition(newAppDefinition, opts);
};
export const cloneComponents = (_ref, updateAppDefinition, isCloning = true, isCut = false) => {
const { appDefinition, currentPageId } = _ref.state;
const selectedComponents = useEditorStore.getState().selectedComponents;
export const cloneComponents = (
selectedComponents,
appDefinition,
currentPageId,
updateAppDefinition,
isCloning = true,
isCut = false
) => {
if (selectedComponents.length < 1) return getSelectedText();
const { components: allComponents } = appDefinition.pages[currentPageId];
// if parent is selected, then remove the parent from the selected components
const filteredSelectedComponents = selectedComponents.filter((component) => {
const parentComponentId = component.component?.parent;
if (parentComponentId) {
// Check if the parent component is also selected
const isParentSelected = selectedComponents.some((comp) => comp.id === parentComponentId);
// If the parent is selected, filter out the child component
if (isParentSelected) {
return false;
}
}
return true;
});
let newDefinition = _.cloneDeep(appDefinition);
let newComponents = [],
newComponentObj = {},
addedComponentId = new Set();
for (let selectedComponent of selectedComponents) {
for (let selectedComponent of filteredSelectedComponents) {
if (addedComponentId.has(selectedComponent.id)) continue;
const component = {
id: selectedComponent.id,
component: allComponents[selectedComponent.id]?.component,
layouts: allComponents[selectedComponent.id]?.layouts,
parent: allComponents[selectedComponent.id]?.parent,
componentId: selectedComponent.id,
};
addedComponentId.add(selectedComponent.id);
let clonedComponent = JSON.parse(JSON.stringify(component));
clonedComponent.parent = undefined;
clonedComponent.children = [];
clonedComponent.children = [...getChildComponents(allComponents, component, clonedComponent, addedComponentId)];
newComponents = [...newComponents, clonedComponent];
newComponents.push(clonedComponent);
const children = getAllChildComponents(allComponents, selectedComponent.id);
if (children.length > 0) {
newComponents.push(...children);
}
newComponentObj = {
newComponents,
isCloning,
isCut,
currentPageId,
};
}
if (isCloning) {
addComponents(currentPageId, appDefinition, updateAppDefinition, undefined, newComponentObj);
const parentId = selectedComponents[0]['component']?.parent ?? undefined;
addComponents(currentPageId, appDefinition, updateAppDefinition, parentId, newComponentObj, true);
toast.success('Component cloned succesfully');
} else if (isCut) {
navigator.clipboard.writeText(JSON.stringify(newComponentObj));
removeSelectedComponent(currentPageId, newDefinition, selectedComponents);
updateAppDefinition(newDefinition);
removeSelectedComponent(currentPageId, newDefinition, selectedComponents, updateAppDefinition);
} else {
navigator.clipboard.writeText(JSON.stringify(newComponentObj));
const successMessage =
newComponentObj.newComponents.length > 1 ? 'Components copied successfully' : 'Component copied successfully';
toast.success(successMessage);
}
_ref.setState({ currentSidebarTab: 2 });
return new Promise((resolve) => {
useEditorStore.getState().actions.updateEditorState({
currentSidebarTab: 2,
});
resolve();
});
};
const getChildComponents = (allComponents, component, parentComponent, addedComponentId) => {
let childComponents = [],
selectedChildComponents = [];
const getAllChildComponents = (allComponents, parentId) => {
const childComponents = [];
if (component.component.component === 'Tabs' || component.component.component === 'Calendar') {
childComponents = Object.keys(allComponents).filter((key) => allComponents[key].parent?.startsWith(component.id));
} else {
childComponents = Object.keys(allComponents).filter((key) => allComponents[key].parent === component.id);
}
Object.keys(allComponents).forEach((componentId) => {
const componentParentId = allComponents[componentId].component?.parent;
childComponents.forEach((componentId) => {
let childComponent = JSON.parse(JSON.stringify(allComponents[componentId]));
childComponent.id = componentId;
const newComponent = JSON.parse(
JSON.stringify({
id: componentId,
component: allComponents[componentId]?.component,
layouts: allComponents[componentId]?.layouts,
parent: allComponents[componentId]?.parent,
})
);
addedComponentId.add(componentId);
const isParentTabORCalendar =
allComponents[parentId]?.component?.component === 'Tabs' ||
allComponents[parentId]?.component?.component === 'Calendar';
if ((component.component.component === 'Tabs') | (component.component.component === 'Calendar')) {
const childTabId = childComponent.parent.split('-').at(-1);
childComponent.parent = `${parentComponent.id}-${childTabId}`;
} else {
childComponent.parent = parentComponent.id;
if (componentParentId && isParentTabORCalendar) {
const childComponent = allComponents[componentId];
const childTabId = componentParentId.split('-').at(-1);
if (componentParentId === `${parentId}-${childTabId}`) {
childComponent.componentId = componentId;
childComponents.push(childComponent);
// Recursively find children of the current child component
const childrenOfChild = getAllChildComponents(allComponents, componentId);
childComponents.push(...childrenOfChild);
}
}
if (componentParentId === parentId) {
const childComponent = allComponents[componentId];
childComponent.componentId = componentId;
childComponents.push(childComponent);
// Recursively find children of the current child component
const childrenOfChild = getAllChildComponents(allComponents, componentId);
childComponents.push(...childrenOfChild);
}
parentComponent.children = [...(parentComponent.children || []), childComponent];
childComponent.children = [...getChildComponents(allComponents, newComponent, childComponent, addedComponentId)];
selectedChildComponents.push(childComponent);
});
return selectedChildComponents;
return childComponents;
};
const updateComponentLayout = (components, parentId, isCut = false) => {
let prevComponent;
components.forEach((component, index) => {
Object.keys(component.layouts).map((layout) => {
if (parentId !== undefined) {
if (parentId !== undefined && !component?.component?.parent) {
if (index > 0) {
component.layouts[layout].top = prevComponent.layouts[layout].top + prevComponent.layouts[layout].height;
component.layouts[layout].left = 0;
@ -1477,56 +1521,100 @@ const updateComponentLayout = (components, parentId, isCut = false) => {
component.layouts[layout].left = 0;
}
prevComponent = component;
} else if (!isCut) {
} else if (!isCut && !component.component.parent) {
component.layouts[layout].top = component.layouts[layout].top + component.layouts[layout].height;
}
});
});
};
//
const isChildOfTabsOrCalendar = (component, allComponents = [], componentParentId = undefined) => {
const parentId = componentParentId ?? component.component?.parent?.split('-').slice(0, -1).join('-');
export const addComponents = (pageId, appDefinition, appDefinitionChanged, parentId = undefined, newComponentObj) => {
const finalComponents = [];
const parentComponent = allComponents.find((comp) => comp.componentId === parentId);
if (parentComponent) {
return parentComponent.component.component === 'Tabs' || parentComponent.component.component === 'Calendar';
}
return false;
};
export const addComponents = (
pageId,
appDefinition,
appDefinitionChanged,
parentId = undefined,
newComponentObj,
fromClipboard = false
) => {
const finalComponents = {};
const componentMap = {};
let parentComponent = undefined;
const { isCloning, isCut, newComponents: pastedComponent = [] } = newComponentObj;
const { isCloning, isCut, newComponents: pastedComponents = [], currentPageId } = newComponentObj;
if (parentId) {
const id = Object.keys(appDefinition.pages[pageId].components).filter((key) => parentId.startsWith(key));
parentComponent = JSON.parse(JSON.stringify(appDefinition.pages[pageId].components[id[0]]));
parentComponent.id = parentId;
}
!isCloning && updateComponentLayout(pastedComponent, parentId, isCut);
pastedComponents.forEach((component) => {
const newComponentId = isCut ? component.componentId : uuidv4();
const componentName = computeComponentName(component.component.component, {
...appDefinition.pages[pageId].components,
...finalComponents,
});
const buildComponents = (components, parentComponent = undefined, skipTabCalendarCheck = false) => {
if (Array.isArray(components) && components.length > 0) {
components.forEach((component) => {
const newComponent = {
id: uuidv4(),
component: component?.component,
layouts: component?.layouts,
};
if (parentComponent) {
if (
!skipTabCalendarCheck &&
(parentComponent.component.component === 'Tabs' || parentComponent.component.component === 'Calendar')
) {
const childTabId = component.parent.split('-').at(-1);
newComponent.parent = `${parentComponent.id}-${childTabId}`;
} else {
newComponent.parent = parentComponent.id;
}
}
finalComponents.push(newComponent);
if (component.children.length > 0) {
buildComponents(component.children, newComponent);
}
});
const isParentTabOrCalendar = isChildOfTabsOrCalendar(component, pastedComponents, parentId);
const parentRef = isParentTabOrCalendar
? component.component.parent.split('-').slice(0, -1).join('-')
: component.component.parent;
const isParentAlsoCopied = parentRef && componentMap[parentRef];
componentMap[component.componentId] = newComponentId;
let isChild = isParentAlsoCopied ? component.component.parent : parentId;
const componentData = JSON.parse(JSON.stringify(component.component));
if (isCloning && parentId && !componentData.parent) {
isChild = component.component.parent;
}
};
buildComponents(pastedComponent, parentComponent, true);
if (!parentComponent && !isParentAlsoCopied && fromClipboard) {
isChild = undefined;
componentData.parent = undefined;
}
updateNewComponents(pageId, appDefinition, finalComponents, appDefinitionChanged);
if (!isCloning && parentComponent && fromClipboard) {
componentData.parent = isParentAlsoCopied ?? parentId;
} else if (isChild && isChildOfTabsOrCalendar(component, pastedComponents, parentId)) {
const parentId = component.component.parent.split('-').slice(0, -1).join('-');
const childTabId = component.component.parent.split('-').at(-1);
componentData.parent = `${componentMap[parentId]}-${childTabId}`;
} else if (isChild) {
const isParentInMap = componentMap[isChild] !== undefined;
componentData.parent = isParentInMap ? componentMap[isChild] : isChild;
}
const newComponent = {
component: {
...componentData,
name: componentName,
},
layouts: component.layouts,
};
finalComponents[newComponentId] = newComponent;
// const doesComponentHaveChildren = getAllChildComponents
});
if (currentPageId === pageId) {
updateComponentLayout(pastedComponents, parentId, isCut);
}
updateNewComponents(pageId, appDefinition, finalComponents, appDefinitionChanged, componentMap, isCut);
!isCloning && toast.success('Component pasted succesfully');
};
@ -1594,6 +1682,7 @@ export const addNewWidgetToTheEditor = (
const widgetsWithDefaultComponents = ['Listview', 'Tabs', 'Form', 'Kanban'];
const nonActiveLayout = currentLayout === 'desktop' ? 'mobile' : 'desktop';
const newComponent = {
id: uuidv4(),
component: componentData,
@ -1604,6 +1693,12 @@ export const addNewWidgetToTheEditor = (
width: defaultWidth,
height: defaultHeight,
},
[nonActiveLayout]: {
top: top,
left: left,
width: defaultWidth,
height: defaultHeight,
},
},
withDefaultChildren: widgetsWithDefaultComponents.includes(componentData.component),
@ -1619,26 +1714,38 @@ export function snapToGrid(canvasWidth, x, y) {
const snappedY = Math.round(y / 10) * 10;
return [snappedX, snappedY];
}
export const removeSelectedComponent = (pageId, newDefinition, selectedComponents) => {
selectedComponents.forEach((component) => {
let childComponents = [];
export const removeSelectedComponent = (pageId, newDefinition, selectedComponents, updateAppDefinition) => {
const toDeleteComponents = [];
if (newDefinition.pages[pageId].components[component.id]?.component?.component === 'Tabs') {
childComponents = Object.keys(newDefinition.pages[pageId].components).filter((key) =>
newDefinition.pages[pageId].components[key].parent?.startsWith(component.id)
);
} else {
childComponents = Object.keys(newDefinition.pages[pageId].components).filter(
(key) => newDefinition.pages[pageId].components[key].parent === component.id
);
if (selectedComponents.length < 1) return getSelectedText();
const { components: allComponents } = newDefinition.pages[pageId];
const findAllChildComponents = (componentId) => {
if (!toDeleteComponents.includes(componentId)) {
toDeleteComponents.push(componentId);
// Find the children of this component
const children = getAllChildComponents(allComponents, componentId).map((child) => child.componentId);
if (children.length > 0) {
// Recursively find children of children
children.forEach((child) => {
findAllChildComponents(child);
});
}
}
};
childComponents.forEach((componentId) => {
delete newDefinition.pages[pageId].components[componentId];
});
delete newDefinition.pages[pageId].components[component.id];
selectedComponents.forEach((component) => {
findAllChildComponents(component.id);
});
toDeleteComponents.forEach((componentId) => {
delete newDefinition.pages[pageId].components[componentId];
});
updateAppDefinition(newDefinition, { componentDefinitionChanged: true, componentDeleted: true, componentCut: true });
};
const getSelectedText = () => {
@ -1678,7 +1785,7 @@ export const runQueries = (queries, _ref) => {
});
};
export const computeQueryState = (queries, _ref) => {
export const computeQueryState = (queries) => {
let queryState = {};
queries.forEach((query) => {
if (query.plugin?.plugin_id) {
@ -1705,6 +1812,87 @@ export const computeQueryState = (queries, _ref) => {
}
};
export const buildComponentMetaDefinition = (components = {}) => {
for (const componentId in components) {
const currentComponentData = components[componentId];
const componentMeta = componentTypes.find((comp) => currentComponentData.component.component === comp.component);
const mergedDefinition = {
...componentMeta.definition,
properties: {
...componentMeta.definition.properties,
...currentComponentData?.component.definition.properties,
},
styles: {
...componentMeta.definition.styles,
...currentComponentData?.component.definition.styles,
},
generalStyles: {
...componentMeta.definition.generalStyles,
...currentComponentData?.component.definition.generalStyles,
},
validation: {
...componentMeta.definition.validation,
...currentComponentData?.component.definition.validation,
},
others: {
...componentMeta.definition.others,
...currentComponentData?.component.definition.others,
},
general: {
...componentMeta.definition.general,
...currentComponentData?.component.definition.general,
},
};
const mergedComponent = {
component: {
...componentMeta,
...currentComponentData.component,
},
layouts: {
...currentComponentData.layouts,
},
withDefaultChildren: componentMeta.withDefaultChildren ?? false,
};
mergedComponent.component.definition = mergedDefinition;
components[componentId] = mergedComponent;
}
return components;
};
export const buildAppDefinition = (data) => {
const editingVersion = _.omit(camelizeKeys(data.editing_version), ['definition', 'updatedAt', 'createdAt', 'name']);
editingVersion['currentVersionId'] = editingVersion.id;
_.unset(editingVersion, 'id');
const pages = data.pages.reduce((acc, page) => {
const currentComponents = buildComponentMetaDefinition(_.cloneDeep(page?.components));
page.components = currentComponents;
acc[page.id] = page;
return acc;
}, {});
const appJSON = {
globalSettings: editingVersion.globalSettings,
homePageId: editingVersion.homePageId,
showViewerNavigation: editingVersion.showViewerNavigation ?? true,
pages: pages,
};
return appJSON;
};
export const removeFunctionObjects = (obj) => {
for (const key in obj) {
if (typeof obj[key] === 'function') {

View file

@ -570,10 +570,11 @@ export const hightlightMentionedUserInComment = (comment) => {
};
export const generateAppActions = (_ref, queryId, mode, isPreview = false) => {
const currentPageId = _ref.state.currentPageId;
const currentComponents = _ref.state?.appDefinition?.pages[currentPageId]?.components
? Object.entries(_ref.state.appDefinition.pages[currentPageId]?.components)
const currentPageId = _ref.currentPageId;
const currentComponents = _ref.appDefinition?.pages[currentPageId]?.components
? Object.entries(_ref.appDefinition.pages[currentPageId]?.components)
: {};
const runQuery = (queryName = '', parameters) => {
const query = useDataQueriesStore.getState().dataQueries.find((query) => {
const isFound = query.name === queryName;
@ -743,7 +744,7 @@ export const generateAppActions = (_ref, queryId, mode, isPreview = false) => {
});
return Promise.resolve();
}
const pages = _ref.state.appDefinition.pages;
const pages = _ref.appDefinition.pages;
const pageId = Object.keys(pages).find((key) => pages[key].handle === pageHandle);
if (!pageId) {

View file

@ -0,0 +1,37 @@
// useDebouncedArrowKeyPress.js
import { useEffect, useState } from 'react';
function useDebouncedArrowKeyPress(delay) {
const [lastKeyPressTimestamp, setLastKeyPressTimestamp] = useState(0);
useEffect(() => {
let timer;
function handleKeyPress(event) {
if (
event.key === 'ArrowUp' ||
event.key === 'ArrowDown' ||
event.key === 'ArrowLeft' ||
event.key === 'ArrowRight'
) {
// Arrow key was pressed; debounce the update
clearTimeout(timer);
timer = setTimeout(() => {
// Trigger the update only after the specified delay
setLastKeyPressTimestamp(Date.now());
}, delay);
}
}
document.addEventListener('keydown', handleKeyPress);
return () => {
document.removeEventListener('keydown', handleKeyPress);
};
}, [delay]);
return lastKeyPressTimestamp;
}
export default useDebouncedArrowKeyPress;

View file

@ -3,6 +3,24 @@ import { authHeader, handleResponse } from '@/_helpers';
export const appService = {
getConfig,
getAll,
createApp,
cloneApp,
exportApp,
importApp,
exportResource,
importResource,
cloneResource,
changeIcon,
deleteApp,
getApp,
fetchApp,
getAppBySlug,
fetchAppBySlug,
getAppByVersion,
fetchAppByVersion,
saveApp,
getAppUsers,
createAppUser,
setPasswordFromToken,
acceptInvite,
@ -13,6 +31,142 @@ function getConfig() {
return fetch(`${config.apiUrl}/config`, requestOptions).then(handleResponse);
}
function getAll(page, folder, searchKey) {
const requestOptions = { method: 'GET', headers: authHeader(), credentials: 'include' };
if (page === 0) return fetch(`${config.apiUrl}/apps`, requestOptions).then(handleResponse);
else
return fetch(
`${config.apiUrl}/apps?page=${page}&folder=${folder || ''}&searchKey=${searchKey}`,
requestOptions
).then(handleResponse);
}
function createApp(body = {}) {
const requestOptions = { method: 'POST', headers: authHeader(), credentials: 'include', body: JSON.stringify(body) };
return fetch(`${config.apiUrl}/apps`, requestOptions).then(handleResponse);
}
function cloneApp(id) {
const requestOptions = { method: 'POST', headers: authHeader(), credentials: 'include' };
return fetch(`${config.apiUrl}/apps/${id}/clone`, requestOptions).then(handleResponse);
}
function exportApp(id, versionId) {
const requestOptions = { method: 'GET', headers: authHeader(), credentials: 'include' };
return fetch(`${config.apiUrl}/apps/${id}/export${versionId ? `?versionId=${versionId}` : ''}`, requestOptions).then(
handleResponse
);
}
function exportResource(body) {
const requestOptions = {
method: 'POST',
headers: authHeader(),
body: JSON.stringify(body),
credentials: 'include',
};
return fetch(`${config.apiUrl}/v2/resources/export`, requestOptions).then(handleResponse);
}
function importResource(body) {
const requestOptions = {
method: 'POST',
headers: authHeader(),
credentials: 'include',
body: JSON.stringify(body),
};
return fetch(`${config.apiUrl}/v2/resources/import`, requestOptions).then(handleResponse);
}
function cloneResource(body) {
const requestOptions = {
method: 'POST',
headers: authHeader(),
body: JSON.stringify(body),
credentials: 'include',
};
return fetch(`${config.apiUrl}/v2/resources/clone`, requestOptions).then(handleResponse);
}
function getVersions(id) {
const requestOptions = { method: 'GET', headers: authHeader(), credentials: 'include' };
return fetch(`${config.apiUrl}/apps/${id}/versions`, requestOptions).then(handleResponse);
}
function importApp(body) {
const requestOptions = { method: 'POST', headers: authHeader(), credentials: 'include', body: JSON.stringify(body) };
return fetch(`${config.apiUrl}/apps/import`, requestOptions).then(handleResponse);
}
function getTables(id) {
const requestOptions = { method: 'GET', headers: authHeader(), credentials: 'include' };
return fetch(`${config.apiUrl}/apps/${id}/tables`, requestOptions).then(handleResponse);
}
function changeIcon(icon, appId) {
const requestOptions = {
method: 'PUT',
headers: authHeader(),
credentials: 'include',
body: JSON.stringify({ icon }),
};
return fetch(`${config.apiUrl}/apps/${appId}/icons`, requestOptions).then(handleResponse);
}
function getApp(id, accessType) {
const requestOptions = { method: 'GET', headers: authHeader(), credentials: 'include' };
return fetch(`${config.apiUrl}/apps/${id}${accessType ? `?access_type=${accessType}` : ''}`, requestOptions).then(
handleResponse
);
}
// v2 api for fetching app
function fetchApp(id) {
const requestOptions = { method: 'GET', headers: authHeader(), credentials: 'include' };
return fetch(`${config.apiUrl}/v2/apps/${id}`, requestOptions).then(handleResponse);
}
function deleteApp(id) {
const requestOptions = { method: 'DELETE', headers: authHeader(), credentials: 'include' };
return fetch(`${config.apiUrl}/apps/${id}`, requestOptions).then(handleResponse);
}
function getAppBySlug(slug) {
const requestOptions = { method: 'GET', headers: authHeader(), credentials: 'include' };
return fetch(`${config.apiUrl}/apps/slugs/${slug}`, requestOptions).then(handleResponse);
}
function fetchAppBySlug(slug) {
const requestOptions = { method: 'GET', headers: authHeader(), credentials: 'include' };
return fetch(`${config.apiUrl}/v2/apps/slugs/${slug}`, requestOptions).then((resp) => handleResponse(resp, true));
}
function getAppByVersion(appId, versionId) {
const requestOptions = { method: 'GET', headers: authHeader(), credentials: 'include' };
return fetch(`${config.apiUrl}/apps/${appId}/versions/${versionId}`, requestOptions).then(handleResponse);
}
function fetchAppByVersion(appId, versionId) {
const requestOptions = { method: 'GET', headers: authHeader(), credentials: 'include' };
return fetch(`${config.apiUrl}/v2/apps/${appId}/versions/${versionId}`, requestOptions).then(handleResponse);
}
function saveApp(id, attributes) {
const requestOptions = {
method: 'PUT',
headers: authHeader(),
credentials: 'include',
body: JSON.stringify({ app: attributes }),
};
return fetch(`${config.apiUrl}/apps/${id}`, requestOptions).then(handleResponse);
}
function getAppUsers(id) {
const requestOptions = { method: 'GET', headers: authHeader(), credentials: 'include' };
return fetch(`${config.apiUrl}/apps/${id}/users`, requestOptions).then(handleResponse);
}
function createAppUser(app_id, org_user_id, role) {
const body = {
app_id,

View file

@ -4,9 +4,16 @@ import { authHeader, handleResponse } from '@/_helpers';
export const appVersionService = {
getAll,
getOne,
getAppVersionData,
create,
del,
save,
autoSaveApp,
saveAppVersionEventHandlers,
createAppVersionEventHandler,
deleteAppVersionEventHandler,
clonePage,
findAllEventsWithSourceId,
};
function getAll(appId) {
@ -18,6 +25,10 @@ function getOne(appId, versionId) {
const requestOptions = { method: 'GET', headers: authHeader(), credentials: 'include' };
return fetch(`${config.apiUrl}/apps/${appId}/versions/${versionId}`, requestOptions).then(handleResponse);
}
function getAppVersionData(appId, versionId) {
const requestOptions = { method: 'GET', headers: authHeader(), credentials: 'include' };
return fetch(`${config.apiUrl}/v2/apps/${appId}/versions/${versionId}`, requestOptions).then(handleResponse);
}
function create(appId, versionName, versionFromId) {
const body = {
@ -47,6 +58,7 @@ function save(appId, versionId, values, isUserSwitchedVersion = false) {
const body = { is_user_switched_version: isUserSwitchedVersion };
if (values.definition) body['definition'] = values.definition;
if (values.name) body['name'] = values.name;
if (values.diff) body['app_diff'] = values.diff;
const requestOptions = {
method: 'PUT',
@ -56,3 +68,118 @@ function save(appId, versionId, values, isUserSwitchedVersion = false) {
};
return fetch(`${config.apiUrl}/apps/${appId}/versions/${versionId}`, requestOptions).then(handleResponse);
}
function autoSaveApp(
appId,
versionId,
diff,
type,
pageId,
operation,
isUserSwitchedVersion = false,
isComponentCutProcess = false
) {
const OPERATION = {
create: 'POST',
update: 'PUT',
delete: 'DELETE',
};
const bodyMappings = {
pages: {
create: { ...diff },
delete: { ...diff },
},
global_settings: {
update: { ...diff },
},
};
const body = !type
? { ...diff }
: bodyMappings[type]?.[operation] || {
is_user_switched_version: isUserSwitchedVersion,
pageId,
diff,
};
if (type === 'components' && operation === 'delete' && isComponentCutProcess) {
body['is_component_cut'] = true;
}
const requestOptions = {
method: OPERATION[operation],
headers: authHeader(),
credentials: 'include',
body: JSON.stringify(body),
};
const url = `${config.apiUrl}/v2/apps/${appId}/versions/${versionId}/${type ?? ''}`;
return fetch(url, requestOptions).then(handleResponse);
}
function saveAppVersionEventHandlers(appId, versionId, events, updateType = 'update') {
const body = {
events,
updateType,
};
const requestOptions = {
method: 'PUT',
headers: authHeader(),
credentials: 'include',
body: JSON.stringify(body),
};
return fetch(`${config.apiUrl}/v2/apps/${appId}/versions/${versionId}/events`, requestOptions).then(handleResponse);
}
function createAppVersionEventHandler(appId, versionId, event) {
const body = {
...event,
};
const requestOptions = {
method: 'POST',
headers: authHeader(),
credentials: 'include',
body: JSON.stringify(body),
};
return fetch(`${config.apiUrl}/v2/apps/${appId}/versions/${versionId}/events`, requestOptions).then(handleResponse);
}
function deleteAppVersionEventHandler(appId, versionId, eventId) {
const requestOptions = {
method: 'DELETE',
headers: authHeader(),
credentials: 'include',
};
return fetch(`${config.apiUrl}/v2/apps/${appId}/versions/${versionId}/events/${eventId}`, requestOptions).then(
handleResponse
);
}
function clonePage(appId, versionId, pageId) {
const requestOptions = {
method: 'POST',
headers: authHeader(),
credentials: 'include',
};
return fetch(`${config.apiUrl}/v2/apps/${appId}/versions/${versionId}/pages/${pageId}/clone`, requestOptions).then(
handleResponse
);
}
function findAllEventsWithSourceId(appId, versionId, sourceId = undefined) {
const requestOptions = {
method: 'GET',
headers: authHeader(),
credentials: 'include',
};
return fetch(
`${config.apiUrl}/v2/apps/${appId}/versions/${versionId}/events${sourceId ? `?sourceId=${sourceId}` : ''}
`,
requestOptions
).then(handleResponse);
}

View file

@ -1,19 +1,126 @@
import { appVersionService } from '@/_services';
import { create, zustandDevTools } from './utils';
const initialState = {
editingVersion: null,
currentUser: null,
apps: [],
appName: null,
slug: null,
isPublic: null,
isMaintenanceOn: null,
organizationId: null,
currentVersionId: null,
userId: null,
app: {},
components: [],
pages: [],
layouts: [],
events: [],
eventHandlers: [],
appDefinitionDiff: null,
appDiffOptions: {},
isSaving: false,
appId: null,
areOthersOnSameVersionAndPage: false,
appVersionPreviewLink: null,
};
export const useAppDataStore = create(
zustandDevTools(
(set) => ({
(set, get) => ({
...initialState,
actions: {
updateEditingVersion: (version) => set(() => ({ editingVersion: version })),
updateApps: (apps) => set(() => ({ apps: apps })),
updateState: (state) => set((prev) => ({ ...prev, ...state })),
updateAppDefinitionDiff: (appDefinitionDiff) => set(() => ({ appDefinitionDiff: appDefinitionDiff })),
updateAppVersion: (appId, versionId, pageId, appDefinitionDiff, isUserSwitchedVersion = false) => {
return new Promise((resolve, reject) => {
useAppDataStore.getState().actions.setIsSaving(true);
const isComponentCutProcess = get().appDiffOptions?.componentCut === true;
appVersionService
.autoSaveApp(
appId,
versionId,
appDefinitionDiff.updateDiff,
appDefinitionDiff.type,
pageId,
appDefinitionDiff.operation,
isUserSwitchedVersion,
isComponentCutProcess
)
.then(() => {
useAppDataStore.getState().actions.setIsSaving(false);
})
.catch((error) => {
useAppDataStore.getState().actions.setIsSaving(false);
reject(error);
})
.finally(() => resolve());
});
},
updateAppVersionEventHandlers: async (events, updateType = 'update') => {
useAppDataStore.getState().actions.setIsSaving(true);
const appId = get().appId;
const versionId = get().currentVersionId;
const response = await appVersionService.saveAppVersionEventHandlers(appId, versionId, events, updateType);
useAppDataStore.getState().actions.setIsSaving(false);
const updatedEvents = get().events;
updatedEvents.forEach((e, index) => {
const toUpdate = response.find((r) => r.id === e.id);
if (toUpdate) {
updatedEvents[index] = toUpdate;
}
});
set(() => ({ events: updatedEvents }));
},
createAppVersionEventHandlers: async (event) => {
useAppDataStore.getState().actions.setIsSaving(true);
const appId = get().appId;
const versionId = get().currentVersionId;
const updatedEvents = get().events;
const response = await appVersionService.createAppVersionEventHandler(appId, versionId, event);
useAppDataStore.getState().actions.setIsSaving(false);
updatedEvents.push(response);
set(() => ({ events: updatedEvents }));
},
deleteAppVersionEventHandler: async (eventId) => {
useAppDataStore.getState().actions.setIsSaving(true);
const appId = get().appId;
const versionId = get().currentVersionId;
const updatedEvents = get().events;
const response = await appVersionService.deleteAppVersionEventHandler(appId, versionId, eventId);
useAppDataStore.getState().actions.setIsSaving(false);
if (response?.affected === 1) {
updatedEvents.splice(
updatedEvents.findIndex((e) => e.id === eventId),
1
);
set(() => ({ events: updatedEvents }));
}
},
autoUpdateEventStore: async (versionId) => {
const appId = get().appId;
const response = await appVersionService.findAllEventsWithSourceId(appId, versionId);
set(() => ({ events: response }));
},
setIsSaving: (isSaving) => set(() => ({ isSaving })),
setAppId: (appId) => set(() => ({ appId })),
setAppPreviewLink: (appVersionPreviewLink) => set(() => ({ appVersionPreviewLink })),
},
}),
{ name: 'App Data Store' }
@ -23,3 +130,6 @@ export const useAppDataStore = create(
export const useEditingVersion = () => useAppDataStore((state) => state.editingVersion);
export const useIsSaving = () => useAppDataStore((state) => state.isSaving);
export const useUpdateEditingVersion = () => useAppDataStore((state) => state.actions);
export const useCurrentUser = () => useAppDataStore((state) => state.currentUser);
export const useAppInfo = () => useAppDataStore((state) => state);
export const useAppDataActions = () => useAppDataStore((state) => state.actions);

View file

@ -5,6 +5,7 @@ const initialState = {
isUserEditingTheVersion: false,
releasedVersionId: null,
isVersionReleased: false,
appVersions: [],
};
export const useAppVersionStore = create(
@ -21,8 +22,12 @@ export const useAppVersionStore = create(
releasedVersionId: versionId,
isVersionReleased: get().editingVersion?.id ? get().editingVersion?.id === versionId : false,
}),
setAppVersions: (versions) => set({ appVersions: versions }),
},
}),
{ name: 'App Version Manager Store' }
)
);
export const useAppVersionActions = () => useAppVersionStore((state) => state.actions);
export const useAppVersionState = () => useAppVersionStore((state) => state);

View file

@ -51,6 +51,7 @@ export const useCurrentState = () =>
page: state.page,
succededQuery: state.succededQuery,
constants: state.constants,
layout: state.layout,
};
}, shallow);

View file

@ -1,7 +1,7 @@
import { create, zustandDevTools } from './utils';
import { getDefaultOptions } from './storeHelper';
import { dataqueryService } from '@/_services';
import debounce from 'lodash/debounce';
// import debounce from 'lodash/debounce';
import { useAppDataStore } from '@/_stores/appDataStore';
import { useQueryPanelStore } from '@/_stores/queryPanelStore';
import { useAppVersionStore } from '@/_stores/appVersionStore';
@ -9,6 +9,7 @@ import { runQueries } from '@/_helpers/appUtils';
import { v4 as uuidv4 } from 'uuid';
import { toast } from 'react-hot-toast';
import { isEmpty, throttle } from 'lodash';
import { useEditorStore } from './editorStore';
const initialState = {
dataQueries: [],
@ -30,15 +31,27 @@ export const useDataQueriesStore = create(
...initialState,
actions: {
// TODO: Remove editor state while changing currentState
fetchDataQueries: async (appId, selectFirstQuery = false, runQueriesOnAppLoad = false, editorRef) => {
fetchDataQueries: async (appVersionId, selectFirstQuery = false, runQueriesOnAppLoad = false, ref) => {
set({ loadingDataQueries: true });
const data = await dataqueryService.getAll(appId);
const data = await dataqueryService.getAll(appVersionId);
set((state) => ({
dataQueries: sortByAttribute(data.data_queries, state.sortBy, state.sortOrder),
loadingDataQueries: false,
}));
// Runs query on loading application
if (runQueriesOnAppLoad) runQueries(data.data_queries, editorRef);
if (data.data_queries.length !== 0) {
const queryConfirmationList = [];
data.data_queries.forEach(({ id, name, options }) => {
if (options && options?.requestConfirmation && options?.runOnPageLoad) {
queryConfirmationList.push({ queryId: id, queryName: name });
}
});
if (queryConfirmationList.length !== 0) {
useEditorStore.getState().actions.updateQueryConfirmationList(queryConfirmationList);
}
}
// Compute query state to be added in the current state
const { actions, selectedQuery } = useQueryPanelStore.getState();
if (selectFirstQuery) {
@ -47,6 +60,9 @@ export const useDataQueriesStore = create(
const query = data.data_queries.find((query) => query.id === selectedQuery?.id);
actions.setSelectedQuery(query?.id);
}
// Runs query on loading application
if (runQueriesOnAppLoad) runQueries(data.data_queries, ref);
},
setDataQueries: (dataQueries) => set({ dataQueries }),
deleteDataQueries: (queryId) => {
@ -232,7 +248,7 @@ export const useDataQueriesStore = create(
newName = queryToClone.name + '_copy' + count.toString();
}
queryToClone.name = newName;
delete queryToClone.id;
useAppDataStore.getState().actions.setIsSaving(true);
dataqueryService
.create(
@ -250,6 +266,26 @@ export const useDataQueriesStore = create(
dataQueries: [{ ...data, data_source_id: queryToClone.data_source_id }, ...state.dataQueries],
}));
actions.setSelectedQuery(data.id, { ...data, data_source_id: queryToClone.data_source_id });
const dataQueryEvents = useAppDataStore
.getState()
.events?.filter((event) => event.target === 'data_query' && event.sourceId === queryToClone.id);
if (dataQueryEvents?.length === 0) return;
return Promise.all(
dataQueryEvents.map((event) => {
const newEvent = {
event: {
...event?.event,
},
eventType: event?.target,
attachedTo: data.id,
index: event?.index,
};
useAppDataStore.getState().actions?.createAppVersionEventHandlers(newEvent);
})
);
})
.catch((error) => {
console.error('error', error);

View file

@ -1,5 +1,5 @@
import { create, zustandDevTools } from './utils';
import { v4 as uuid } from 'uuid';
const STORE_NAME = 'Editor';
const ACTIONS = {
@ -19,53 +19,79 @@ const initialState = {
selectionInProgress: false,
selectedComponents: [],
isEditorActive: false,
currentSidebarTab: 2,
selectedComponent: null,
scrollOptions: {
container: null,
throttleTime: 0,
threshold: 0,
},
canUndo: false,
canRedo: false,
currentVersion: {},
noOfVersionsSupported: 100,
appDefinition: {},
// isSaving: false,
isUpdatingEditorStateInProcess: false,
saveError: false,
isLoading: true,
defaultComponentStateComputed: false,
showLeftSidebar: true,
queryConfirmationList: [],
currentPageId: null,
currentSessionId: uuid(),
};
export const useEditorStore = create(
zustandDevTools(
(set, get) => ({
...initialState,
actions: {
setShowComments: (showComments) =>
set({ showComments }, false, {
type: ACTIONS.SET_HOVERED_COMPONENT,
showComments,
}),
toggleComments: () =>
set({ showComments: !get().showComments }, false, {
type: ACTIONS.TOGGLE_COMMENTS,
}),
toggleCurrentLayout: (currentLayout) =>
set({ currentLayout }, false, {
type: ACTIONS.TOGGLE_CURRENT_LAYOUT,
currentLayout,
}),
setIsEditorActive: (isEditorActive) => set(() => ({ isEditorActive })),
setHoveredComponent: (hoveredComponent) =>
set({ hoveredComponent }, false, {
type: ACTIONS.SET_HOVERED_COMPONENT,
hoveredComponent,
}),
setSelectionInProgress: (isSelectionInProgress) => {
set(
{
isSelectionInProgress,
},
false,
{ type: ACTIONS.SET_SELECTION_IN_PROGRESS }
);
},
setSelectedComponents: (selectedComponents) => {
set(
{
selectedComponents,
},
false,
{ type: ACTIONS.SET_SELECTED_COMPONENTS }
);
},
// Dev tools for this store are disabled comments since its freezing chrome tab
(set, get) => ({
...initialState,
actions: {
setShowComments: (showComments) =>
set({ showComments }, false, {
type: ACTIONS.SET_HOVERED_COMPONENT,
showComments,
}),
toggleComments: () =>
set({ showComments: !get().showComments }, false, {
type: ACTIONS.TOGGLE_COMMENTS,
}),
toggleCurrentLayout: (currentLayout) =>
set({ currentLayout }, false, {
type: ACTIONS.TOGGLE_CURRENT_LAYOUT,
currentLayout,
}),
setIsEditorActive: (isEditorActive) => set(() => ({ isEditorActive })),
updateEditorState: (state) => set((prev) => ({ ...prev, ...state })),
updateQueryConfirmationList: (queryConfirmationList) => set({ queryConfirmationList }),
setHoveredComponent: (hoveredComponent) =>
set({ hoveredComponent }, false, {
type: ACTIONS.SET_HOVERED_COMPONENT,
hoveredComponent,
}),
setSelectionInProgress: (isSelectionInProgress) => {
set(
{
isSelectionInProgress,
},
false,
{ type: ACTIONS.SET_SELECTION_IN_PROGRESS }
);
},
}),
{ name: STORE_NAME }
)
setSelectedComponents: (selectedComponents, isMulti = false) => {
const newSelectedComponents = isMulti
? [...get().selectedComponents, ...selectedComponents]
: selectedComponents;
set({
selectedComponents: newSelectedComponents,
});
},
setCurrentPageId: (currentPageId) => set({ currentPageId }),
},
}),
{ name: STORE_NAME }
);
export const useEditorActions = () => useEditorStore((state) => state.actions);
export const useEditorState = () => useEditorStore((state) => state);

View file

@ -1,5 +1,9 @@
import { create as _create } from 'zustand';
import { devtools } from 'zustand/middleware';
// eslint-disable-next-line import/no-unresolved
import { diff } from 'deep-object-diff';
import { componentTypes } from '@/Editor/WidgetManager/components';
import _ from 'lodash';
export const zustandDevTools = (fn, options = {}) =>
devtools(fn, { ...options, enabled: process.env.NODE_ENV === 'production' ? false : true });
@ -21,3 +25,304 @@ export const resetAllStores = () => {
resetter();
}
};
const defaultComponent = {
name: '',
properties: {},
styles: {},
validation: {},
type: '',
others: {
showOnDesktop: { value: '{{true}}' },
showOnMobile: { value: '{{false}}' },
},
};
const updateType = Object.freeze({
pageDefinitionChanged: 'pages',
containerChanges: 'components/layout',
componentAdded: 'components',
componentDefinitionChanged: 'components',
componentDeleted: 'components',
});
export const computeAppDiff = (appDiff, currentPageId, opts, currentLayout) => {
const { updateDiff, type, operation, error } = updateFor(appDiff, currentPageId, opts, currentLayout);
return { updateDiff, type, operation, error };
};
// for table column diffs, we need to compute the diff for each column separately and send the the entire column data
function generatePath(obj, targetKey, currentPath = '') {
for (const key in obj) {
const newPath = currentPath ? currentPath + '.' + key : key;
if (key === targetKey) {
return newPath;
}
if (typeof obj[key] === 'object' && obj[key] !== null) {
const result = generatePath(obj[key], targetKey, newPath);
if (result) {
return result;
}
}
}
return null;
}
function getValueFromJson(json, path) {
if (!path || typeof path !== 'string') return null;
let value = json;
path.split('.').forEach((key) => {
value = value[key];
});
return value;
}
function updateValueInJson(json, path, value) {
let obj = json;
const keys = path?.split('.');
if (!keys) {
return null;
}
const lastKey = keys.pop();
keys.forEach((key) => {
obj = obj[key];
});
obj[lastKey] = value;
return json;
}
export function isParamFromTableColumn(appDiff, definition) {
const path = generatePath(appDiff, 'columns') || generatePath(appDiff, 'actions');
if (!path) {
return false;
}
const value2 = getValueFromJson(definition, path);
return value2 !== undefined;
}
export const computeComponentPropertyDiff = (appDiff, definition, opts) => {
if (!opts?.isParamFromTableColumn) {
return appDiff;
}
const columnsPath = generatePath(appDiff, 'columns');
const actionsPath = generatePath(appDiff, 'actions');
const deletionHistoryPath = generatePath(appDiff, 'columnDeletionHistory');
let _diff = _.cloneDeep(appDiff);
if (columnsPath) {
const columnsValue = getValueFromJson(definition, columnsPath);
_diff = updateValueInJson(_diff, columnsPath, columnsValue);
}
if (actionsPath) {
const actionsValue = getValueFromJson(definition, actionsPath);
_diff = updateValueInJson(_diff, actionsPath, actionsValue);
}
if (deletionHistoryPath) {
const deletionHistoryValue = getValueFromJson(definition, deletionHistoryPath);
_diff = updateValueInJson(_diff, deletionHistoryPath, deletionHistoryValue);
}
return _diff;
};
const updateFor = (appDiff, currentPageId, opts, currentLayout) => {
const updateTypeMappings = [
{
updateTypes: ['componentAdded', 'componentDefinitionChanged', 'componentDeleted', 'containerChanges'],
processingFunction: computeComponentDiff,
},
{
updateTypes: ['pageDefinitionChanged', 'pageSortingChanged', 'deletePageRequest', 'addNewPage'],
processingFunction: computePageUpdate,
},
{
updateTypes: ['homePageChanged'],
processingFunction: () => ({
updateDiff: appDiff,
type: null,
operation: 'update',
}),
},
{
updateTypes: ['globalSettings', 'generalAppDefinitionChanged'],
processingFunction: () => ({
updateDiff: appDiff,
type: 'global_settings',
operation: 'update',
}),
},
];
const options = _.keys(opts);
for (const { updateTypes, processingFunction } of updateTypeMappings) {
const optionsTypes = _.intersection(options, updateTypes);
if (optionsTypes.length > 0) {
try {
return processingFunction(appDiff, currentPageId, optionsTypes, currentLayout);
} catch (error) {
return { error, updateDiff: {}, type: null, operation: null };
}
}
}
return null;
};
const computePageUpdate = (appDiff, currentPageId, opts) => {
let type;
let updateDiff;
let operation = 'update';
if (opts.includes('deletePageRequest')) {
const deletePageId = _.keys(appDiff?.pages).map((pageId) => {
if (appDiff?.pages[pageId]?.pageId === undefined) {
return pageId;
}
})[0];
updateDiff = {
pageId: deletePageId,
};
type = updateType.pageDefinitionChanged;
operation = 'delete';
} else if (opts.includes('pageSortingChanged')) {
updateDiff = appDiff?.pages;
type = updateType.pageDefinitionChanged;
} else if (opts.includes('pageDefinitionChanged')) {
updateDiff = appDiff?.pages[currentPageId];
type = updateType.pageDefinitionChanged;
if (opts.includes('addNewPage')) {
operation = 'create';
}
}
return { updateDiff, type, operation };
};
const computeComponentDiff = (appDiff, currentPageId, opts, currentLayout) => {
let type;
let updateDiff;
let operation = 'update';
if (opts.includes('componentDeleted')) {
const currentPageComponents = appDiff?.pages[currentPageId]?.components;
updateDiff = _.keys(currentPageComponents);
type = updateType.componentDeleted;
operation = 'delete';
} else if (opts.includes('componentAdded')) {
const currentPageComponents = appDiff?.pages[currentPageId]?.components;
updateDiff = _.toPairs(currentPageComponents ?? []).reduce((result, [id, component]) => {
if (_.keys(component).length === 1 && component.withDefaultChildren !== undefined) {
return result;
}
const componentMeta = componentTypes.find((comp) => comp.component === component.component.component);
if (!componentMeta) {
return result;
}
const metaDiff = diff(componentMeta, component.component);
result[id] = _.defaultsDeep(metaDiff, defaultComponent);
if (metaDiff.definition && !_.isEmpty(metaDiff.definition)) {
const metaAttributes = _.keys(metaDiff.definition);
metaAttributes.forEach((attribute) => {
const doesActionsExist =
metaDiff.definition[attribute]?.actions && !_.isEmpty(metaDiff.definition[attribute]?.actions?.value);
const doesColumnsExist =
metaDiff.definition[attribute]?.columns && !_.isEmpty(metaDiff.definition[attribute]?.columns?.value);
if (doesActionsExist || doesColumnsExist) {
const actions = _.toArray(metaDiff.definition[attribute]?.actions?.value) || [];
const columns = _.toArray(metaDiff.definition[attribute]?.columns?.value) || [];
metaDiff.definition = {
...metaDiff.definition,
[attribute]: {
...metaDiff.definition[attribute],
actions: {
value: actions,
},
columns: {
value: columns,
},
},
};
}
result[id][attribute] = metaDiff.definition[attribute];
});
}
const currentDisplayPreference = currentLayout;
if (currentDisplayPreference === 'mobile') {
result[id].others.showOnMobile = { value: '{{true}}' };
result[id].others.showOnDesktop = { value: '{{false}}' };
}
if (result[id]?.definition) {
delete result[id].definition;
}
result[id].type = componentMeta.component;
result[id].parent = component.component.parent ?? null;
result[id].layouts = appDiff.pages[currentPageId].components[id].layouts;
operation = 'create';
return result;
}, {});
type = updateType.componentDefinitionChanged;
} else if (
(opts.includes('containerChanges') || opts.includes('componentDefinitionChanged')) &&
!opts.includes('componentAdded')
) {
const currentPageComponents = appDiff?.pages[currentPageId]?.components;
updateDiff = toRemoveExposedvariablesFromComponentDiff(currentPageComponents);
type = opts.includes('containerChanges') ? updateType.containerChanges : updateType.componentDefinitionChanged;
}
return { updateDiff, type, operation };
};
function toRemoveExposedvariablesFromComponentDiff(object) {
const copy = _.cloneDeep(object);
const componentIds = _.keys(copy);
componentIds.forEach((componentId) => {
const { component } = copy[componentId];
if (component?.exposedVariables) {
delete component.exposedVariables;
}
});
return copy;
}

View file

@ -1,4 +1,4 @@
import React, { useCallback, useState } from 'react';
import React from 'react';
import { Link } from 'react-router-dom';
import useRouter from '@/_hooks/use-router';
import { ToolTip } from '@/_components/ToolTip';

View file

@ -1 +1 @@
2.23.0
2.24.0

View file

@ -6,39 +6,28 @@ export class ListviewDefaultMode1688977149516 implements MigrationInterface {
const entityManager = queryRunner.manager;
const appVersions = await entityManager.find(AppVersion);
for (const version of appVersions) {
const definition = version['definition'];
const definition = JSON.parse(JSON.stringify(version?.definition));
if (definition) {
const pages = definition['pages'];
if (pages) {
if (Object.keys(pages).length > 0) {
for (const pageId of Object.keys(pages)) {
const components = definition['pages'][pageId]['components'];
if (components) {
if (Object.keys(components).length > 0) {
for (const componentId of Object.keys(components)) {
const component = components[componentId];
if (component?.component?.component === 'Listview') {
component['component']['definition']['properties']['mode'] = {
value: 'list',
};
components[componentId] = {
...component,
component: {
...component.component,
definition: {
...component.component.definition,
},
},
};
if (
component?.component?.component === 'Listview' &&
component.component?.definition?.properties?.mode
) {
component.component.definition.properties.mode['value'] = 'list';
}
}
}
definition['components'] = components;
version.definition = definition;
}
}
version.definition = definition;
await entityManager.update(AppVersion, { id: version.id }, { definition });
}
}

View file

@ -4,25 +4,23 @@ import { AppVersion } from '../src/entities/app_version.entity';
export class CellSizeRegularCondensed1692973078520 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
const entityManager = queryRunner.manager;
const queryBuilder = queryRunner.connection.createQueryBuilder();
const appVersionRepository = entityManager.getRepository(AppVersion);
const appVersions = await appVersionRepository.find();
for (const version of appVersions) {
const definition = version?.['definition'];
const definition = JSON.parse(JSON.stringify(version?.definition));
if (definition) {
const pages = definition?.['pages'];
if (pages) {
if (Object.keys(pages).length > 0) {
for (const pageId of Object.keys(pages)) {
const components = pages?.[pageId]?.['components'];
if (components) {
const components = pages[pageId]?.['components'];
if (Object.keys(components).length > 0) {
for (const componentId of Object.keys(components)) {
const component = components[componentId];
if (component?.component?.component === 'Table') {
component.component.definition.styles.cellSize = {
value: 'regular',
};
if (component?.component?.component === 'Table' && component.component?.definition?.styles?.cellSize) {
component.component.styles.cellSize = {
...component.component.styles.cellSize,
options: [
@ -30,27 +28,19 @@ export class CellSizeRegularCondensed1692973078520 implements MigrationInterface
{ name: 'Regular', value: 'regular' },
],
};
components[componentId] = {
...component,
component: {
...component.component,
definition: {
...component.component.definition,
},
},
component.component.definition.styles.cellSize = {
value: 'regular',
};
}
}
pages[pageId]['components'] = components;
}
}
}
definition['pages'] = pages;
version.definition = definition;
await queryBuilder.update(AppVersion).set({ definition }).where('id = :id', { id: version.id }).execute();
await entityManager.update(AppVersion, { id: version.id }, { definition });
}
}
}

View file

@ -4,20 +4,20 @@ import { AppVersion } from '../src/entities/app_version.entity';
export class TableRowCellStyle1692974311591 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
const entityManager = queryRunner.manager;
const queryBuilder = queryRunner.connection.createQueryBuilder();
const appVersionRepository = entityManager.getRepository(AppVersion);
const appVersions = await appVersionRepository.find();
for (const version of appVersions) {
const definition = version['definition'];
const definition = JSON.parse(JSON.stringify(version?.definition));
if (definition) {
const pages = definition['pages'];
if (pages) {
if (Object.keys(pages).length > 0) {
for (const pageId of Object.keys(pages)) {
const components = pages[pageId]['components'];
if (components) {
if (Object.keys(components).length > 0) {
for (const componentId of Object.keys(components)) {
const component = components[componentId];
if (component?.component?.component === 'Table') {
@ -32,27 +32,15 @@ export class TableRowCellStyle1692974311591 implements MigrationInterface {
{ name: 'Striped', value: 'table-striped' },
],
};
components[componentId] = {
...component,
component: {
...component.component,
definition: {
...component.component.definition,
},
},
};
}
}
}
pages[pageId]['components'] = components;
}
}
definition['pages'] = pages;
version.definition = definition;
await queryBuilder.update(AppVersion).set({ definition }).where('id = :id', { id: version.id }).execute();
await entityManager.update(AppVersion, { id: version.id }, { definition });
}
}
}

View file

@ -0,0 +1,361 @@
import { In, MigrationInterface, QueryRunner, EntityManager } from 'typeorm';
import { AppVersion } from '../src/entities/app_version.entity';
import { Component } from 'src/entities/component.entity';
import { Page } from 'src/entities/page.entity';
import { Layout } from 'src/entities/layout.entity';
import { EventHandler, Target } from 'src/entities/event_handler.entity';
import { DataQuery } from 'src/entities/data_query.entity';
import { MigrationProgress, processDataInBatches } from 'src/helpers/utils.helper';
import { v4 as uuid } from 'uuid';
interface AppResourceMappings {
pagesMapping: Record<string, string>;
componentsMapping: Record<string, string>;
}
export class MigrateAppsDefinitionSchemaTransition1697473340856 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
const entityManager = queryRunner.manager;
const appVersionRepository = entityManager.getRepository(AppVersion);
const appVersions = await appVersionRepository.find();
const totalVersions = appVersions.length;
const migrationProgress = new MigrationProgress(
'MigrateAppsDefinitionSchemaTransition1697473340856',
totalVersions
);
const batchSize = 100; // Number of apps to migrate at a time
await processDataInBatches(
entityManager,
async (entityManager: EntityManager, skip: number, take: number) => {
return entityManager.find(AppVersion, {
where: { id: In(appVersions.map((appVersion) => appVersion.id)) },
take,
skip,
});
},
async (entityManager: EntityManager, versions: AppVersion[]) => {
await this.processVersions(entityManager, versions, migrationProgress);
},
batchSize
);
}
private async processVersions(
entityManager: EntityManager,
versions: AppVersion[],
migrationProgress: MigrationProgress
) {
for (const version of versions) {
const definition = version['definition'];
if (!definition) return;
const dataQueriesRepository = entityManager.getRepository(DataQuery);
const dataQueries = await dataQueriesRepository.find({
where: { appVersionId: version.id },
});
let updateHomepageId = null;
const appResourceMappings: AppResourceMappings = {
pagesMapping: {},
componentsMapping: {},
};
if (definition?.pages) {
for (const pageId of Object.keys(definition?.pages)) {
const page = definition.pages[pageId];
const pagePositionInTheList = Object.keys(definition?.pages).indexOf(pageId);
const pageEvents = page.events || [];
const pageComponents = page.components;
const isHomepage = (definition['homePageId'] as any) === pageId;
const componentEvents = [];
const componentLayouts = [];
const transformedComponents = this.transformComponentData(
pageComponents,
componentEvents,
appResourceMappings.componentsMapping
);
const newPage = entityManager.create(Page, {
name: page.name,
handle: page.handle,
appVersionId: version.id,
disabled: page.disabled || false,
hidden: page.hidden || false,
index: pagePositionInTheList,
});
const pageCreated = await entityManager.save(newPage);
appResourceMappings.pagesMapping[pageId] = pageCreated.id;
transformedComponents.forEach((component) => {
component.page = pageCreated;
});
const savedComponents = await entityManager.save(Component, transformedComponents);
for (const componentId in pageComponents) {
const componentLayout = pageComponents[componentId]['layouts'];
if (componentLayout && appResourceMappings.componentsMapping[componentId]) {
for (const type in componentLayout) {
const layout = componentLayout[type];
const newLayout = new Layout();
newLayout.type = type;
newLayout.top = layout.top;
newLayout.left = layout.left;
newLayout.width = layout.width;
newLayout.height = layout.height;
newLayout.componentId = appResourceMappings.componentsMapping[componentId];
componentLayouts.push(newLayout);
}
}
}
await entityManager.save(Layout, componentLayouts);
if (pageEvents.length > 0) {
pageEvents.forEach(async (event, index) => {
const newEvent = {
name: event.eventId,
sourceId: pageCreated.id,
target: Target.page,
event: event,
index: pageEvents.index || index,
appVersionId: version.id,
};
await entityManager.save(EventHandler, newEvent);
});
}
componentEvents.forEach((eventObj) => {
if (eventObj.event?.length === 0) return;
eventObj.event.forEach(async (event, index) => {
const newEvent = {
name: event.eventId,
sourceId: appResourceMappings.componentsMapping[eventObj.componentId],
target: Target.component,
event: event,
index: eventObj.index || index,
appVersionId: version.id,
};
await entityManager.save(EventHandler, newEvent);
});
});
savedComponents.forEach(async (component) => {
if (component.type === 'Table') {
const tableActions = component.properties?.actions?.value || [];
const tableColumns = component.properties?.columns?.value || [];
const tableActionAndColumnEvents = [];
tableActions.forEach((action) => {
const actionEvents = action.events || [];
actionEvents.forEach((event, index) => {
tableActionAndColumnEvents.push({
name: event.eventId,
sourceId: component.id,
target: Target.tableAction,
event: { ...event, ref: action.name },
index: event.index ?? index,
appVersionId: version.id,
});
});
});
tableColumns.forEach((column) => {
if (column?.columnType !== 'toggle') return;
const columnEvents = column.events || [];
columnEvents.forEach((event, index) => {
tableActionAndColumnEvents.push({
name: event.eventId,
sourceId: component.id,
target: Target.tableColumn,
event: { ...event, ref: column.name },
index: event.index ?? index,
appVersionId: version.id,
});
});
});
await entityManager.save(EventHandler, tableActionAndColumnEvents);
}
});
if (isHomepage) {
updateHomepageId = pageCreated.id;
}
}
}
for (const dataQuery of dataQueries) {
const queryEvents = dataQuery?.options?.events || [];
if (queryEvents.length > 0) {
queryEvents.forEach(async (event, index) => {
const newEvent = {
name: event.eventId,
sourceId: dataQuery.id,
target: Target.dataQuery,
event: event,
index: queryEvents.index || index,
appVersionId: version.id,
};
await entityManager.save(EventHandler, newEvent);
});
}
}
await entityManager.update(
AppVersion,
{ id: version.id },
{
homePageId: updateHomepageId,
showViewerNavigation: definition?.showViewerNavigation || true,
globalSettings: definition.globalSettings,
}
);
await this.updateEventActionsForNewVersionWithNewMappingIds(
entityManager,
version.id,
appResourceMappings.componentsMapping,
appResourceMappings.pagesMapping
);
migrationProgress.show();
}
}
async updateEventActionsForNewVersionWithNewMappingIds(
manager: EntityManager,
versionId: string,
oldComponentToNewComponentMapping: Record<string, unknown>,
oldPageToNewPageMapping: Record<string, unknown>
) {
const allEvents = await manager.find(EventHandler, {
where: { appVersionId: versionId },
});
for (const event of allEvents) {
const eventDefinition = event.event;
if (eventDefinition?.actionId === 'switch-page') {
eventDefinition.pageId = oldPageToNewPageMapping[eventDefinition.pageId];
}
if (eventDefinition?.actionId === 'control-component') {
eventDefinition.componentId = oldComponentToNewComponentMapping[eventDefinition.componentId];
}
if (eventDefinition?.actionId == 'show-modal' || eventDefinition?.actionId === 'close-modal') {
eventDefinition.modal = oldComponentToNewComponentMapping[eventDefinition.modal];
}
event.event = eventDefinition;
await manager.save(event);
}
}
private transformComponentData(
data: object,
componentEvents: any[],
componentsMapping: Record<string, string>
): Component[] {
const transformedComponents: Component[] = [];
const allComponents = Object.keys(data).map((key) => {
return {
id: key,
...data[key],
};
});
for (const componentId in data) {
const component = data[componentId];
const componentData = component['component'];
let skipComponent = false;
const transformedComponent: Component = new Component();
let parentId = component.parent ? component.parent : null;
const isParentTabOrCalendar = this.isChildOfTabsOrCalendar(component, allComponents, parentId);
if (isParentTabOrCalendar) {
const childTabId = component.parent.split('-')[component.parent.split('-').length - 1];
const _parentId = component?.parent?.split('-').slice(0, -1).join('-');
const mappedParentId = componentsMapping[_parentId];
parentId = `${mappedParentId}-${childTabId}`;
} else {
if (component.parent && !componentsMapping[parentId]) {
skipComponent = true;
}
parentId = componentsMapping[parentId];
}
if (!skipComponent) {
transformedComponent.id = uuid();
transformedComponent.name = componentData.name;
transformedComponent.type = componentData.component;
transformedComponent.properties = componentData.definition.properties || {};
transformedComponent.styles = componentData.definition.styles || {};
transformedComponent.validation = componentData.definition.validation || {};
transformedComponent.general = componentData.definition.general || {};
transformedComponent.generalStyles = componentData.definition.generalStyles || {};
transformedComponent.displayPreferences = componentData.definition.others || {};
transformedComponent.parent = component.parent ? parentId : null;
transformedComponents.push(transformedComponent);
componentEvents.push({
componentId: componentId,
event: componentData.definition.events,
});
componentsMapping[componentId] = transformedComponent.id;
}
}
return transformedComponents;
}
isChildOfTabsOrCalendar = (component, allComponents = [], componentParentId = undefined) => {
if (componentParentId) {
const parentId = component?.parent?.split('-').slice(0, -1).join('-');
const parentComponent = allComponents.find((comp) => comp.id === parentId);
if (parentComponent) {
return parentComponent.component.component === 'Tabs' || parentComponent.component.component === 'Calendar';
}
}
return false;
};
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query('DELETE FROM page');
await queryRunner.query('DELETE FROM component');
await queryRunner.query('DELETE FROM layout');
await queryRunner.query('DELETE FROM event_handler');
await queryRunner.query('ALTER TABLE app_version DROP COLUMN IF EXISTS homePageId');
await queryRunner.query('ALTER TABLE app_version DROP COLUMN IF EXISTS globalSettings');
await queryRunner.query('ALTER TABLE app_version DROP COLUMN IF EXISTS showViewerNavigation');
}
}

View file

@ -0,0 +1,41 @@
import { MigrationInterface, QueryRunner, TableColumn } from 'typeorm';
export class UpdateAppVersionEntity1691006886222 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
// Add the new columns to the app_versions table
await queryRunner.addColumn(
'app_versions',
new TableColumn({
name: 'global_settings',
type: 'json',
isNullable: true,
})
);
await queryRunner.addColumn(
'app_versions',
new TableColumn({
name: 'show_viewer_navigation',
type: 'boolean',
default: true,
isNullable: false,
})
);
await queryRunner.addColumn(
'app_versions',
new TableColumn({
name: 'home_page_id',
type: 'uuid',
isNullable: true,
})
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
// Remove the new columns from the app_versions table (if necessary)
await queryRunner.dropColumn('app_versions', 'global_settings');
await queryRunner.dropColumn('app_versions', 'show_viewer_navigation');
await queryRunner.dropColumn('app_versions', 'home_page_id');
}
}

View file

@ -0,0 +1,76 @@
import { MigrationInterface, QueryRunner, Table, TableForeignKey } from 'typeorm';
export class CreatePageTable1691004576333 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.createTable(
new Table({
name: 'pages',
columns: [
{
name: 'id',
type: 'uuid',
isPrimary: true,
default: 'gen_random_uuid()',
},
{
name: 'name',
type: 'varchar',
isNullable: false,
},
{
name: 'index',
type: 'int',
isNullable: false,
},
{
name: 'page_handle',
type: 'varchar',
isNullable: false,
},
{
name: 'disabled',
type: 'boolean',
isNullable: true,
},
{
name: 'hidden',
type: 'boolean',
isNullable: true,
},
{
name: 'app_version_id',
type: 'uuid',
isNullable: false,
},
{
name: 'created_at',
type: 'timestamp',
isNullable: true,
default: 'now()',
},
{
name: 'updated_at',
type: 'timestamp',
isNullable: true,
default: 'now()',
},
],
})
);
// Add foreign key to relate Page with AppVersion
await queryRunner.createForeignKey(
'pages',
new TableForeignKey({
columnNames: ['app_version_id'],
referencedColumnNames: ['id'],
referencedTableName: 'app_versions',
onDelete: 'CASCADE',
})
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.dropTable('pages');
}
}

View file

@ -0,0 +1,78 @@
import { MigrationInterface, QueryRunner, Table, TableForeignKey } from 'typeorm';
export class CreateEventHandlerTable1691004706564 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.createTable(
new Table({
name: 'event_handlers',
columns: [
{
name: 'id',
type: 'uuid',
isPrimary: true,
default: 'gen_random_uuid()',
},
{
name: 'name',
type: 'varchar',
isNullable: false,
},
{
name: 'index',
type: 'int',
isNullable: false,
},
{
name: 'event',
type: 'jsonb',
isNullable: false,
},
{
name: 'app_version_id',
type: 'uuid',
isNullable: false,
},
{
name: 'source_id',
type: 'varchar',
isNullable: false,
},
{
name: 'target',
type: 'enum',
enum: ['page', 'component', 'data_query', 'table_column', 'table_action'],
default: "'page'",
isNullable: false,
},
{
name: 'created_at',
type: 'timestamp',
isNullable: true,
default: 'now()',
},
{
name: 'updated_at',
type: 'timestamp',
isNullable: true,
default: 'now()',
},
],
})
);
// Add foreign key to relate EventHandler with AppVersion
await queryRunner.createForeignKey(
'event_handlers',
new TableForeignKey({
columnNames: ['app_version_id'],
referencedColumnNames: ['id'],
referencedTableName: 'app_versions',
onDelete: 'CASCADE',
})
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.dropTable('event_handlers');
}
}

View file

@ -0,0 +1,108 @@
import { MigrationInterface, QueryRunner, Table, TableForeignKey, TableIndex } from 'typeorm';
export class CreateComponentTable1691006952074 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.createTable(
new Table({
name: 'components',
columns: [
{
name: 'id',
type: 'uuid',
isPrimary: true,
default: 'gen_random_uuid()',
},
{
name: 'name',
type: 'varchar',
isNullable: false,
},
{
name: 'type',
type: 'varchar',
isNullable: false,
},
{
name: 'page_id',
type: 'uuid',
isNullable: false,
},
{
name: 'parent',
type: 'varchar',
isNullable: true,
},
{
name: 'properties',
type: 'json',
isNullable: true,
},
{
name: 'general_properties',
type: 'json',
isNullable: true,
},
{
name: 'styles',
type: 'json',
isNullable: true,
},
{
name: 'general_styles',
type: 'json',
isNullable: true,
},
{
name: 'display_preferences',
type: 'json',
isNullable: true,
},
{
name: 'validation',
type: 'json',
isNullable: true,
},
{
name: 'created_at',
type: 'timestamp',
isNullable: true,
default: 'now()',
},
{
name: 'updated_at',
type: 'timestamp',
isNullable: true,
default: 'now()',
},
],
})
);
await queryRunner.createForeignKey(
'components',
new TableForeignKey({
columnNames: ['page_id'],
referencedColumnNames: ['id'],
referencedTableName: 'pages',
onDelete: 'CASCADE',
})
);
await queryRunner.createIndex('components', new TableIndex({ columnNames: ['name'] }));
await queryRunner.createIndex('components', new TableIndex({ columnNames: ['type'] }));
await queryRunner.createIndex('components', new TableIndex({ columnNames: ['page_id'] }));
}
public async down(queryRunner: QueryRunner): Promise<void> {
// Drop indexes
await queryRunner.dropIndex('components', 'IDX_COMPONENT_NAME');
await queryRunner.dropIndex('components', 'IDX_COMPONENT_TYPE');
await queryRunner.dropIndex('components', 'IDX_COMPONENT_PAGE');
// Drop foreign key
await queryRunner.dropForeignKey('components', 'FK_COMPONENT_PAGE');
// Drop table
await queryRunner.dropTable('components');
}
}

View file

@ -0,0 +1,66 @@
import { MigrationInterface, QueryRunner, Table, TableForeignKey } from 'typeorm';
export class CreateLayoutTable1691007037021 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.createTable(
new Table({
name: 'layouts',
columns: [
{
name: 'id',
type: 'uuid',
isPrimary: true,
default: 'gen_random_uuid()',
},
{
name: 'type',
type: 'enum',
enumName: 'layput_type',
enum: ['desktop', 'mobile'],
isNullable: false,
},
{
name: 'top',
type: 'double precision',
isNullable: false,
},
{
name: 'left',
type: 'double precision',
isNullable: false,
},
{
name: 'width',
type: 'double precision',
isNullable: false,
},
{
name: 'height',
type: 'double precision',
isNullable: false,
},
{
name: 'component_id',
type: 'uuid',
isNullable: false,
},
],
})
);
// Add foreign key to relate Layout with Component
await queryRunner.createForeignKey(
'layouts',
new TableForeignKey({
columnNames: ['component_id'],
referencedColumnNames: ['id'],
referencedTableName: 'components',
onDelete: 'CASCADE',
})
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.dropTable('layouts');
}
}

View file

@ -55,7 +55,7 @@
"request-ip": "^3.3.0",
"rxjs": "^7.2.0",
"sanitize-html": "^2.7.0",
"semver": "^7.3.5",
"semver": "^7.5.4",
"ts-node": "^10.0.0",
"tsconfig-paths": "^3.10.1",
"typeorm": "^0.2.38",
@ -11858,8 +11858,9 @@
}
},
"node_modules/semver": {
"version": "7.3.5",
"integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==",
"version": "7.5.4",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
"integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
"dependencies": {
"lru-cache": "^6.0.0"
},
@ -22603,8 +22604,9 @@
}
},
"semver": {
"version": "7.3.5",
"integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==",
"version": "7.5.4",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
"integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
"requires": {
"lru-cache": "^6.0.0"
}

View file

@ -80,7 +80,7 @@
"request-ip": "^3.3.0",
"rxjs": "^7.2.0",
"sanitize-html": "^2.7.0",
"semver": "^7.3.5",
"semver": "^7.5.4",
"ts-node": "^10.0.0",
"tsconfig-paths": "^3.10.1",
"typeorm": "^0.2.38",

View file

@ -0,0 +1,502 @@
import {
Controller,
ForbiddenException,
Get,
Param,
Post,
Put,
Delete,
Query,
UseGuards,
Body,
BadRequestException,
UseInterceptors,
} from '@nestjs/common';
import { JwtAuthGuard } from '../../src/modules/auth/jwt-auth.guard';
import { AppAuthGuard } from 'src/modules/auth/app-auth.guard';
import { AppsService } from '../services/apps.service';
import { camelizeKeys, decamelizeKeys } from 'humps';
import { AppsAbilityFactory } from 'src/modules/casl/abilities/apps-ability.factory';
import { App } from 'src/entities/app.entity';
import { User } from 'src/decorators/user.decorator';
import { CreatePageDto, DeletePageDto } from '@dto/pages.dto';
import { CreateComponentDto, DeleteComponentDto, UpdateComponentDto, LayoutUpdateDto } from '@dto/component.dto';
import { ValidAppInterceptor } from 'src/interceptors/valid.app.interceptor';
import { AppDecorator } from 'src/decorators/app.decorator';
import { ComponentsService } from '@services/components.service';
import { PageService } from '@services/page.service';
import { EventsService } from '@services/events_handler.service';
import { AppVersionUpdateDto } from '@dto/app-version-update.dto';
import { CreateEventHandlerDto, UpdateEventHandlerDto } from '@dto/event-handler.dto';
@Controller({
path: 'apps',
version: '2',
})
export class AppsControllerV2 {
constructor(
private appsService: AppsService,
private componentsService: ComponentsService,
private pageService: PageService,
private eventsService: EventsService,
private eventService: EventsService,
private appsAbilityFactory: AppsAbilityFactory
) {}
@UseGuards(JwtAuthGuard)
@UseInterceptors(ValidAppInterceptor)
@Get(':id')
async show(@User() user, @AppDecorator() app: App, @Query('access_type') accessType: string) {
const ability = await this.appsAbilityFactory.appsActions(user, app.id);
if (!ability.can('viewApp', app)) {
throw new ForbiddenException(
JSON.stringify({
organizationId: app.organizationId,
})
);
}
if (accessType === 'edit' && !ability.can('editApp', app)) {
throw new ForbiddenException(
JSON.stringify({
organizationId: app.organizationId,
})
);
}
const response = decamelizeKeys(app);
const seralizedQueries = [];
const dataQueriesForVersion = app.editingVersion
? await this.appsService.findDataQueriesForVersion(app.editingVersion.id)
: [];
const pagesForVersion = app.editingVersion ? await this.pageService.findPagesForVersion(app.editingVersion.id) : [];
const eventsForVersion = app.editingVersion
? await this.eventsService.findEventsForVersion(app.editingVersion.id)
: [];
// serialize queries
for (const query of dataQueriesForVersion) {
const decamelizedQuery = decamelizeKeys(query);
decamelizedQuery['options'] = query.options;
seralizedQueries.push(decamelizedQuery);
}
response['data_queries'] = seralizedQueries;
response['definition'] = app.editingVersion?.definition;
response['pages'] = pagesForVersion;
response['events'] = eventsForVersion;
//! if editing version exists, camelize the definition
if (app.editingVersion && app.editingVersion.definition) {
response['editing_version'] = {
...response['editing_version'],
definition: camelizeKeys(app.editingVersion.definition),
};
}
return response;
}
@UseGuards(AppAuthGuard) // This guard will allow access for unauthenticated user if the app is public
@Get('slugs/:slug')
async appFromSlug(@User() user, @AppDecorator() app: App) {
if (user) {
const ability = await this.appsAbilityFactory.appsActions(user, app.id);
if (!ability.can('viewApp', app)) {
throw new ForbiddenException(
JSON.stringify({
organizationId: app.organizationId,
})
);
}
}
const versionToLoad = app.currentVersionId
? await this.appsService.findVersion(app.currentVersionId)
: await this.appsService.findVersion(app.editingVersion?.id);
const pagesForVersion = app.editingVersion ? await this.pageService.findPagesForVersion(versionToLoad.id) : [];
const eventsForVersion = app.editingVersion ? await this.eventsService.findEventsForVersion(versionToLoad.id) : [];
// serialize
return {
current_version_id: app['currentVersionId'],
data_queries: versionToLoad?.dataQueries,
definition: versionToLoad?.definition,
is_public: app.isPublic,
is_maintenance_on: app.isMaintenanceOn,
name: app.name,
slug: app.slug,
events: eventsForVersion,
pages: pagesForVersion,
homePageId: versionToLoad.homePageId,
globalSettings: versionToLoad.globalSettings,
showViewerNavigation: versionToLoad.showViewerNavigation,
};
}
@UseGuards(JwtAuthGuard)
@UseInterceptors(ValidAppInterceptor)
@Get(':id/versions/:versionId')
async version(@User() user, @Param('id') id, @Param('versionId') versionId) {
const appVersion = await this.appsService.findVersion(versionId);
const app = appVersion.app;
if (app.id !== id) {
throw new BadRequestException();
}
const ability = await this.appsAbilityFactory.appsActions(user, app.id);
if (!ability.can('fetchVersions', app)) {
throw new ForbiddenException(
JSON.stringify({
organizationId: app.organizationId,
})
);
}
const pagesForVersion = await this.pageService.findPagesForVersion(versionId);
const eventsForVersion = await this.eventsService.findEventsForVersion(versionId);
const appCurrentEditingVersion = JSON.parse(JSON.stringify(appVersion));
delete appCurrentEditingVersion['app'];
const appData = {
...app,
};
delete appData['editingVersion'];
return {
...appData,
editing_version: camelizeKeys(appCurrentEditingVersion),
pages: pagesForVersion,
events: eventsForVersion,
};
}
@UseGuards(JwtAuthGuard)
@UseInterceptors(ValidAppInterceptor)
@Put(':id/versions/:versionId')
async updateVersion(
@User() user,
@Param('id') id,
@Param('versionId') versionId,
@Body() appVersionUpdateDto: AppVersionUpdateDto
) {
const version = await this.appsService.findVersion(versionId);
const app = version.app;
if (app.id !== id) {
throw new BadRequestException();
}
const ability = await this.appsAbilityFactory.appsActions(user, id);
if (!ability.can('updateVersions', app)) {
throw new ForbiddenException('You do not have permissions to perform this action');
}
return await this.appsService.updateAppVersion(version, appVersionUpdateDto);
}
@UseGuards(JwtAuthGuard)
@UseInterceptors(ValidAppInterceptor)
@Put(':id/versions/:versionId/global_settings')
async updateGlobalSettings(
@User() user,
@Param('id') id,
@Param('versionId') versionId,
@Body() appVersionUpdateDto: AppVersionUpdateDto
) {
const version = await this.appsService.findVersion(versionId);
const app = version.app;
if (app.id !== id) {
throw new BadRequestException();
}
const ability = await this.appsAbilityFactory.appsActions(user, id);
if (!ability.can('updateVersions', app)) {
throw new ForbiddenException('You do not have permissions to perform this action');
}
return await this.appsService.updateAppVersion(version, appVersionUpdateDto);
}
//components api
@UseGuards(JwtAuthGuard)
@UseInterceptors(ValidAppInterceptor)
@Post(':id/versions/:versionId/components')
async createComponent(
@User() user,
@Param('id') id,
@Param('versionId') versionId,
@Body() createComponentDto: CreateComponentDto
) {
const version = await this.appsService.findVersion(versionId);
const app = version.app;
if (app.id !== id) {
throw new BadRequestException();
}
const ability = await this.appsAbilityFactory.appsActions(user, id);
if (!ability.can('updateVersions', app)) {
throw new ForbiddenException('You do not have permissions to perform this action');
}
await this.componentsService.create(createComponentDto.diff, createComponentDto.pageId, versionId);
}
@UseGuards(JwtAuthGuard)
@UseInterceptors(ValidAppInterceptor)
@Put(':id/versions/:versionId/components')
async updateComponent(
@User() user,
@Param('id') id,
@Param('versionId') versionId,
@Body() updateComponentDto: UpdateComponentDto
) {
const version = await this.appsService.findVersion(versionId);
const app = version.app;
if (app.id !== id) {
throw new BadRequestException();
}
const ability = await this.appsAbilityFactory.appsActions(user, id);
if (!ability.can('updateVersions', app)) {
throw new ForbiddenException('You do not have permissions to perform this action');
}
await this.componentsService.update(updateComponentDto.diff, versionId);
}
@UseGuards(JwtAuthGuard)
@UseInterceptors(ValidAppInterceptor)
@Delete(':id/versions/:versionId/components')
async deleteComponents(
@User() user,
@Param('id') id,
@Param('versionId') versionId,
@Body() deleteComponentDto: DeleteComponentDto
) {
const version = await this.appsService.findVersion(versionId);
const app = version.app;
if (app.id !== id) {
throw new BadRequestException();
}
const ability = await this.appsAbilityFactory.appsActions(user, id);
if (!ability.can('updateVersions', app)) {
throw new ForbiddenException('You do not have permissions to perform this action');
}
await this.componentsService.delete(deleteComponentDto.diff, versionId, deleteComponentDto.is_component_cut);
}
@UseGuards(JwtAuthGuard)
@UseInterceptors(ValidAppInterceptor)
@Put(':id/versions/:versionId/components/layout')
async updateComponentLayout(
@User() user,
@Param('id') id,
@Param('versionId') versionId,
@Body() updateComponentLayout: LayoutUpdateDto
) {
const version = await this.appsService.findVersion(versionId);
const app = version.app;
if (app.id !== id) {
throw new BadRequestException();
}
const ability = await this.appsAbilityFactory.appsActions(user, id);
if (!ability.can('updateVersions', app)) {
throw new ForbiddenException('You do not have permissions to perform this action');
}
await this.componentsService.componentLayoutChange(updateComponentLayout.diff, versionId);
}
// pages api
@UseGuards(JwtAuthGuard)
@UseInterceptors(ValidAppInterceptor)
@Post(':id/versions/:versionId/pages')
async createPages(
@User() user,
@Param('id') id,
@Param('versionId') versionId,
@Body() createPageDto: CreatePageDto
) {
const version = await this.appsService.findVersion(versionId);
const app = version.app;
if (app.id !== id) {
throw new BadRequestException();
}
const ability = await this.appsAbilityFactory.appsActions(user, id);
if (!ability.can('updateVersions', app)) {
throw new ForbiddenException('You do not have permissions to perform this action');
}
await this.pageService.createPage(createPageDto, versionId);
}
@UseGuards(JwtAuthGuard)
@UseInterceptors(ValidAppInterceptor)
@Post(':id/versions/:versionId/pages/:pageId/clone')
async clonePage(@User() user, @Param('id') id, @Param('versionId') versionId, @Param('pageId') pageId) {
const version = await this.appsService.findVersion(versionId);
const app = version.app;
const ability = await this.appsAbilityFactory.appsActions(user, id);
if (!ability.can('updateVersions', app)) {
throw new ForbiddenException('You do not have permissions to perform this action');
}
return await this.pageService.clonePage(pageId, versionId);
}
@UseGuards(JwtAuthGuard)
@UseInterceptors(ValidAppInterceptor)
@Put(':id/versions/:versionId/pages')
async updatePages(@User() user, @Param('id') id, @Param('versionId') versionId, @Body() updatePageDto) {
const version = await this.appsService.findVersion(versionId);
const app = version.app;
if (app.id !== id) {
throw new BadRequestException();
}
const ability = await this.appsAbilityFactory.appsActions(user, id);
if (!ability.can('updateVersions', app)) {
throw new ForbiddenException('You do not have permissions to perform this action');
}
await this.pageService.updatePage(updatePageDto, versionId);
}
@UseGuards(JwtAuthGuard)
@UseInterceptors(ValidAppInterceptor)
@Delete(':id/versions/:versionId/pages')
async deletePage(@User() user, @Param('id') id, @Param('versionId') versionId, @Body() deletePageDto: DeletePageDto) {
const version = await this.appsService.findVersion(versionId);
const app = version.app;
if (app.id !== id) {
throw new BadRequestException();
}
const ability = await this.appsAbilityFactory.appsActions(user, id);
if (!ability.can('updateVersions', app)) {
throw new ForbiddenException('You do not have permissions to perform this action');
}
await this.pageService.deletePage(deletePageDto.pageId, versionId);
}
// events api
@UseGuards(JwtAuthGuard)
@UseInterceptors(ValidAppInterceptor)
@Get(':id/versions/:versionId/events')
async getEvents(@User() user, @Param('id') id, @Param('versionId') versionId, @Query('sourceId') sourceId) {
const version = await this.appsService.findVersion(versionId);
const app = version.app;
if (app.id !== id) {
throw new BadRequestException();
}
const ability = await this.appsAbilityFactory.appsActions(user, id);
if (!ability.can('viewApp', app)) {
throw new ForbiddenException('You do not have permissions to perform this action');
}
if (!sourceId) {
return this.eventService.findEventsForVersion(versionId);
}
return this.eventService.findAllEventsWithSourceId(sourceId);
}
@UseGuards(JwtAuthGuard)
@UseInterceptors(ValidAppInterceptor)
@Post(':id/versions/:versionId/events')
async createEvent(
@User() user,
@Param('id') id,
@Param('versionId') versionId,
@Body() createEventHandlerDto: CreateEventHandlerDto
) {
const version = await this.appsService.findVersion(versionId);
const app = version.app;
if (app.id !== id) {
throw new BadRequestException();
}
const ability = await this.appsAbilityFactory.appsActions(user, id);
if (!ability.can('updateVersions', app)) {
throw new ForbiddenException('You do not have permissions to perform this action');
}
return this.eventService.createEvent(createEventHandlerDto, versionId);
}
@UseGuards(JwtAuthGuard)
@UseInterceptors(ValidAppInterceptor)
@Put(':id/versions/:versionId/events')
async updateEvents(
@User() user,
@Param('id') id,
@Param('versionId') versionId,
@Body() updateEventHandlerDto: UpdateEventHandlerDto
) {
const version = await this.appsService.findVersion(versionId);
const app = version.app;
if (app.id !== id) {
throw new BadRequestException();
}
const ability = await this.appsAbilityFactory.appsActions(user, id);
if (!ability.can('updateVersions', app)) {
throw new ForbiddenException('You do not have permissions to perform this action');
}
const { events, updateType } = updateEventHandlerDto;
return await this.eventService.updateEvent(events, updateType, versionId);
}
@UseGuards(JwtAuthGuard)
@UseInterceptors(ValidAppInterceptor)
@Delete(':id/versions/:versionId/events/:eventId')
async deleteEvents(@User() user, @Param('id') id, @Param('versionId') versionId, @Param('eventId') eventId) {
const version = await this.appsService.findVersion(versionId);
const app = version.app;
if (app.id !== id) {
throw new BadRequestException();
}
const ability = await this.appsAbilityFactory.appsActions(user, id);
if (!ability.can('updateVersions', app)) {
throw new ForbiddenException('You do not have permissions to perform this action');
}
return await this.eventService.deleteEvent(eventId, versionId);
}
}

View file

@ -0,0 +1,26 @@
import { IsBoolean, IsNotEmpty, IsOptional, IsString, IsUUID, MaxLength } from 'class-validator';
import { Transform } from 'class-transformer';
import { sanitizeInput } from '../helpers/utils.helper';
export class AppVersionUpdateDto {
@IsString()
@IsOptional()
@Transform(({ value }) => {
const newValue = sanitizeInput(value);
return newValue.trim();
})
@IsNotEmpty()
@MaxLength(50, { message: 'Maximum length has been reached.' })
name: string;
@IsBoolean()
@IsOptional()
showViewerNavigation: boolean;
@IsUUID()
@IsOptional()
homePageId: string;
@IsOptional()
globalSettings: any;
}

View file

@ -0,0 +1,159 @@
import { Type } from 'class-transformer';
import {
IsArray,
IsBoolean,
IsNotEmpty,
IsNumber,
IsObject,
IsOptional,
IsString,
IsUUID,
ValidationArguments,
ValidatorConstraint,
ValidatorConstraintInterface,
Validate,
} from 'class-validator';
export class ComponentLayoutDto {
@IsNumber()
@IsOptional()
top?: number;
@IsNumber()
@IsOptional()
left?: number;
@IsNumber()
@IsOptional()
width?: number;
@IsNumber()
@IsOptional()
height?: number;
}
export class LayoutData {
@IsObject()
@IsOptional()
desktop?: ComponentLayoutDto;
@IsObject()
@IsOptional()
mobile?: ComponentLayoutDto;
}
@ValidatorConstraint({ name: 'LayoutDataValidator', async: false })
class LayoutDataValidator implements ValidatorConstraintInterface {
validate(value: any) {
if (value) {
for (const key in value) {
if (!value[key] || typeof value[key] !== 'object' || !value[key].layouts) {
return false;
}
}
}
return true;
}
defaultMessage(args: ValidationArguments) {
return `Each key in "diff" must have the structure { layouts: LayoutData }`;
}
}
export class LayoutUpdateDto {
@IsBoolean()
is_user_switched_version: boolean;
@IsUUID()
pageId: string;
@IsObject()
@IsNotEmpty()
@Validate(LayoutDataValidator, { each: true })
diff: Record<string, { layouts: LayoutData }>;
}
class ComponentDto {
@IsString()
name: string;
@IsObject()
properties: Record<string, any>;
@IsObject()
styles: Record<string, any>;
@IsObject()
validation: Record<string, any>;
@IsString()
type: string;
@IsObject()
others: Record<string, any>;
@IsOptional()
@Type(() => ComponentLayoutDto)
layouts: ComponentLayoutDto;
@IsOptional()
parent: string;
}
@ValidatorConstraint({ name: 'CreateComponentDtoValidator', async: false })
class CreateComponentDtoValidator implements ValidatorConstraintInterface {
validate(value: any, args: ValidationArguments) {
// Check if the diff structure is valid
for (const key in value.diff) {
if (!value.diff[key] || typeof value.diff[key] !== 'object') {
return false;
}
// You can add additional checks for the component structure here
}
return true;
}
defaultMessage(args: ValidationArguments) {
return `Invalid structure in diff for CreateComponentDto`;
}
}
export class CreateComponentDto {
@IsBoolean()
is_user_switched_version: boolean;
@IsUUID()
pageId: string;
@IsObject()
@Validate(CreateComponentDtoValidator)
diff: Record<string, ComponentDto>;
}
export class UpdateComponentDto {
@IsBoolean()
is_user_switched_version: boolean;
@IsUUID()
pageId: string;
@IsObject()
@Validate(CreateComponentDtoValidator)
diff: Record<string, ComponentDto>;
}
export class DeleteComponentDto {
@IsBoolean()
is_user_switched_version: boolean;
@IsUUID()
pageId: string;
@IsArray()
diff: string[];
@IsBoolean()
@IsOptional()
is_component_cut: boolean;
}

View file

@ -0,0 +1,44 @@
import { IsArray, IsIn, IsNumber, IsObject, IsString, IsUUID, ValidateNested } from 'class-validator';
import { Target } from 'src/entities/event_handler.entity';
export class CreateEventHandlerDto {
@IsObject()
event: any;
@IsString()
eventType: Target;
@IsString()
attachedTo: string;
@IsNumber()
index: number;
}
class UpdateEventDiff {
@IsString()
name: string;
@IsNumber()
index: number;
@IsObject()
@ValidateNested()
event: any;
}
export class UpdateEvent {
@IsUUID()
event_id: string;
@IsObject()
diff: UpdateEventDiff;
}
export class UpdateEventHandlerDto {
@IsArray()
events: UpdateEvent[];
@IsIn(['update', 'reorder'])
updateType: 'update' | 'reorder';
}

View file

@ -0,0 +1,38 @@
import { IsNotEmpty, IsNumber, IsOptional, IsString, IsUUID, MaxLength } from 'class-validator';
export class CreatePageDto {
@IsUUID()
@IsNotEmpty()
id: string;
@IsString()
@IsNotEmpty()
@MaxLength(32)
name: string;
@IsString()
@IsNotEmpty()
@MaxLength(50)
handle: string;
@IsNumber()
@IsNotEmpty()
index: number;
@IsOptional()
disabled: boolean;
@IsOptional()
hidden: boolean;
}
export class DeletePageDto {
@IsUUID()
@IsNotEmpty()
pageId: string;
}
export class UpdatePageDto {
pageId: string;
diff: Partial<CreatePageDto>;
}

View file

@ -20,4 +20,11 @@ export class VersionEditDto {
@IsOptional()
@IsBoolean()
is_user_switched_version: boolean;
@IsOptional()
diff: any;
@IsOptional()
@IsString()
pageId: string;
}

Some files were not shown because too many files have changed in this diff Show more