Merge pull request #12495 from ToolJet/release/platform-post-mod-fixes
Release platform post modularisation fixes
4
.github/workflows/cypress-platform.yml
vendored
|
|
@ -102,6 +102,10 @@ jobs:
|
|||
echo "ENABLE_MARKETPLACE_FEATURE=true" >> .env
|
||||
echo "ENABLE_MARKETPLACE_DEV_MODE=true" >> .env
|
||||
echo "ENABLE_PRIVATE_APP_EMBED=true" >> .env
|
||||
echo "SSO_GOOGLE_OAUTH2_CLIENT_ID=123456789.apps.googleusercontent.com" >> .env
|
||||
echo "SSO_GOOGLE_OAUTH2_CLIENT_SECRET=ABCGFDNF-FHSDVFY-bskfh6234" >> .env
|
||||
echo "SSO_GIT_OAUTH2_CLIENT_ID=1234567890" >> .env
|
||||
echo "SSO_GIT_OAUTH2_CLIENT_SECRET=3346shfvkdjjsfkvxce32854e026a4531ed" >> .env
|
||||
|
||||
- name: Set up database
|
||||
run: |
|
||||
|
|
|
|||
2
.version
|
|
@ -1 +1 @@
|
|||
3.9.0
|
||||
3.10.0
|
||||
|
|
|
|||
|
|
@ -98,6 +98,7 @@ module.exports = defineConfig({
|
|||
configFile: environment.configFile,
|
||||
specPattern: [
|
||||
"cypress/e2e/happyPath/platform/ceTestcases/userFlow/firstUserOnboarding.cy.js",
|
||||
"cypress/e2e/happyPath/platform/commonTestcases/workspace/dashboard.cy.js"
|
||||
"cypress/e2e/happyPath/platform/ceTestcases/!(userFlow)/**/*.cy.js",
|
||||
"cypress/e2e/happyPath/platform/commonTestcases/**/*.cy.js",
|
||||
],
|
||||
|
|
|
|||
|
|
@ -92,11 +92,7 @@ module.exports = defineConfig({
|
|||
experimentalModfyObstructiveThirdPartyCode: true,
|
||||
experimentalRunAllSpecs: true,
|
||||
baseUrl: "http://localhost:8082",
|
||||
specPattern: [
|
||||
"cypress/e2e/happyPath/platform/ceTestcases/userFlow/firstUserOnboarding.cy.js",
|
||||
"cypress/e2e/happyPath/platform/ceTestcases/!(userFlow)/**/*.cy.js",
|
||||
"cypress/e2e/happyPath/platform/commonTestcases/**/*.cy.js",
|
||||
],
|
||||
specPattern: "cypress/e2e/happyPath/**/*.cy.js",
|
||||
downloadsFolder: "cypress/downloads",
|
||||
numTestsKeptInMemory: 0,
|
||||
redirectionLimit: 10,
|
||||
|
|
|
|||
|
|
@ -627,10 +627,11 @@ Cypress.Commands.add("apiAddDataToTable", (tableName, data) => {
|
|||
});
|
||||
|
||||
Cypress.Commands.add("apiGetDataSourceIdByName", (dataSourceName) => {
|
||||
const workspaceId = Cypress.env("workspaceId");
|
||||
cy.getAuthHeaders().then((headers) => {
|
||||
cy.request({
|
||||
method: "GET",
|
||||
url: `${Cypress.env("server_host")}/api/data-sources`,
|
||||
url: `${Cypress.env("server_host")}/api/data-sources/${workspaceId}`,
|
||||
headers: headers,
|
||||
}).then((response) => {
|
||||
expect(response.status).to.equal(200);
|
||||
|
|
@ -665,7 +666,7 @@ Cypress.Commands.add(
|
|||
name: dataSourceName,
|
||||
options: [
|
||||
{ key: "connection_type", value: "manual", encrypted: false },
|
||||
{ key: "host", value: "35.202.183.199" },
|
||||
{ key: "host", value: "35.238.9.114" },
|
||||
{ key: "port", value: 5432 },
|
||||
{ key: "database", value: "student" },
|
||||
{ key: "username", value: "postgres" },
|
||||
|
|
|
|||
|
|
@ -15,15 +15,11 @@ const API_ENDPOINT =
|
|||
Cypress.Commands.add(
|
||||
"appUILogin",
|
||||
(email = "dev@tooljet.io", password = "password") => {
|
||||
cy.visit("/");
|
||||
cy.wait(1000);
|
||||
cy.clearAndType(onboardingSelectors.loginEmailInput, email);
|
||||
cy.clearAndType(onboardingSelectors.loginPasswordInput, password);
|
||||
cy.get(onboardingSelectors.signInButton).click();
|
||||
|
||||
cy.intercept("GET", API_ENDPOINT).as("library_apps");
|
||||
cy.get(commonSelectors.homePageLogo, { timeout: 10000 });
|
||||
cy.wait("@library_apps");
|
||||
cy.wait(2000);
|
||||
cy.get('[data-cy="main-wrapper"]', { timeout: 10000 }).should("be.visible");
|
||||
}
|
||||
);
|
||||
|
||||
|
|
@ -400,36 +396,39 @@ Cypress.Commands.add("getPosition", (componentName) => {
|
|||
Cypress.Commands.add("defaultWorkspaceLogin", () => {
|
||||
cy.apiLogin();
|
||||
|
||||
// cy.intercept("GET", API_ENDPOINT).as("library_apps");
|
||||
cy.visit("/my-workspace");
|
||||
cy.intercept("GET", API_ENDPOINT).as("library_apps");
|
||||
cy.wait(2000)
|
||||
cy.get(commonSelectors.homePageLogo, { timeout: 10000 });
|
||||
cy.wait("@library_apps");
|
||||
// });
|
||||
// cy.wait("@library_apps");
|
||||
});
|
||||
|
||||
Cypress.Commands.add(
|
||||
"visitSlug",
|
||||
({
|
||||
actualUrl,
|
||||
currentUrl = `${Cypress.config("baseUrl")}/error/unknown`,
|
||||
errorUrls = [
|
||||
`${Cypress.config("baseUrl")}/error/unknown`,
|
||||
`${Cypress.config("baseUrl")}/error/restricted`,
|
||||
],
|
||||
}) => {
|
||||
// Ensure actualUrl is provided
|
||||
if (!actualUrl) {
|
||||
throw new Error("actualUrl is required for visitSlug command.");
|
||||
}
|
||||
|
||||
cy.visit(actualUrl);
|
||||
|
||||
// Dynamically wait for the correct URL or handle navigation errors
|
||||
cy.url().then((url) => {
|
||||
if (url === currentUrl) {
|
||||
cy.log(`Navigation resulted in unexpected URL: ${url}. Retrying...`);
|
||||
if (errorUrls.includes(url)) {
|
||||
cy.log(`Navigation resulted in error URL: ${url}. Retrying...`);
|
||||
cy.visit(actualUrl);
|
||||
cy.wait(1000);
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
Cypress.Commands.add("releaseApp", () => {
|
||||
if (Cypress.env("environment") !== "Community") {
|
||||
cy.get(commonEeSelectors.promoteButton).click();
|
||||
|
|
@ -520,16 +519,6 @@ Cypress.Commands.add("verifyElement", (selector, text, eqValue) => {
|
|||
element.should("be.visible").and("have.text", text);
|
||||
});
|
||||
|
||||
Cypress.Commands.add("loginWithCredentials", (email, password) => {
|
||||
cy.get(onboardingSelectors.loginEmailInput, { timeout: 20000 }).should(
|
||||
"be.visible"
|
||||
);
|
||||
cy.clearAndType(onboardingSelectors.loginEmailInput, email);
|
||||
cy.clearAndType(onboardingSelectors.loginPasswordInput, password);
|
||||
cy.get(onboardingSelectors.signInButton).click();
|
||||
cy.wait(3000);
|
||||
cy.get(commonSelectors.pageLogo).should("be.visible");
|
||||
});
|
||||
|
||||
Cypress.Commands.add("getAppId", (appName) => {
|
||||
cy.task("dbConnection", {
|
||||
|
|
|
|||
|
|
@ -259,7 +259,7 @@ export const commonSelectors = {
|
|||
cloneAppTitle: '[data-cy="clone-app-title"]',
|
||||
cloneAppButton: '[data-cy="clone-app"]',
|
||||
appNameErrorLabel: '[data-cy="app-name-error-label"]',
|
||||
importAppTitle: '[data-cy="import-app-title"]',
|
||||
importAppTitle: '[data-cy="import-an-app"]',
|
||||
importAppButton: '[data-cy="import-app"]',
|
||||
chooseFromTemplateButton: '[data-cy="choose-from-template-button"]',
|
||||
CreateAppFromTemplateButton: '[data-cy="create-new-app-from-template-title"]',
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ export const dataSourceText = {
|
|||
? "Databases (20)"
|
||||
: "Databases (18)";
|
||||
},
|
||||
allApis: "APIs (20)",
|
||||
allApis: "APIs (21)",
|
||||
allCloudStorage: "Cloud Storages (4)",
|
||||
pluginsLabelAndCount: "Plugins (0)",
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ export const workspaceConstantsText = {
|
|||
secretsConstantInfo: "To resolve a secret workspace constant use {{secrets.access_token}}Read documentation",
|
||||
emptyStateHeader: "No Workspace constants yet",
|
||||
emptyStateText:
|
||||
"Use workspace constants seamlessly in both the app builder and data source connections across ToolJet.",
|
||||
"Use workspace constants seamlessly within both the app builder and data source connections across the platform.",
|
||||
addNewConstantButton: "+ Create new constant",
|
||||
addConstatntText: "Add new constant in production ",
|
||||
constantCreatedToast: (type) => { return `${type} constant created successfully!` },
|
||||
|
|
|
|||
|
|
@ -0,0 +1,219 @@
|
|||
import { fake } from "Fixtures/fake";
|
||||
import { commonSelectors } from "Selectors/common";
|
||||
import { importSelectors } from "Selectors/exportImport";
|
||||
import { commonText } from "Texts/common";
|
||||
|
||||
import { exportAppModalText } from "Texts/exportImport";
|
||||
import {
|
||||
clickOnExportButtonAndVerify,
|
||||
exportAllVersionsAndVerify,
|
||||
verifyElementsOfExportModal,
|
||||
} from "Support/utils/exportImport";
|
||||
import { selectAppCardOption, closeModal } from "Support/utils/common";
|
||||
|
||||
describe("App Export", () => {
|
||||
const TEST_DATA = {
|
||||
appFiles: {
|
||||
multiVersion: "cypress/fixtures/templates/three-versions.json",
|
||||
singleVersion: "cypress/fixtures/templates/one_version.json",
|
||||
},
|
||||
};
|
||||
|
||||
let data;
|
||||
|
||||
data = {
|
||||
workspaceName: fake.firstName,
|
||||
workspaceSlug: fake.firstName.toLowerCase().replace(/\s+/g, "-"),
|
||||
appName: `${fake.companyName}-IE-App`,
|
||||
appReName: `${fake.companyName}-${fake.companyName}-IE-App`,
|
||||
dsName: fake.lastName.toLowerCase().replaceAll("[^A-Za-z]", ""),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
data = {
|
||||
workspaceName: fake.firstName,
|
||||
workspaceSlug: fake.firstName.toLowerCase().replace(/\s+/g, "-"),
|
||||
appName: `${fake.companyName}-IE-App`,
|
||||
appReName: `${fake.companyName}-${fake.companyName}-IE-App`,
|
||||
dsName: fake.lastName.toLowerCase().replaceAll("[^A-Za-z]", ""),
|
||||
};
|
||||
cy.exec("mkdir -p ./cypress/downloads/");
|
||||
cy.wait(3000);
|
||||
|
||||
cy.apiLogin();
|
||||
cy.apiCreateWorkspace(data.workspaceName, data.workspaceSlug);
|
||||
cy.apiLogout();
|
||||
});
|
||||
|
||||
it("Verify the elements of export dialog box", () => {
|
||||
cy.window({ log: false }).then((win) => {
|
||||
win.localStorage.setItem("walkthroughCompleted", "true");
|
||||
});
|
||||
|
||||
cy.apiLogin();
|
||||
cy.visit(`${data.workspaceSlug}`);
|
||||
cy.get(importSelectors.importOptionInput)
|
||||
.eq(0)
|
||||
.selectFile(TEST_DATA.appFiles.multiVersion, {
|
||||
force: true,
|
||||
});
|
||||
cy.wait(1500);
|
||||
cy.clearAndType(commonSelectors.appNameInput, data.appName);
|
||||
cy.get(importSelectors.importAppButton).click();
|
||||
cy.wait(3000);
|
||||
cy.backToApps();
|
||||
|
||||
// Select the app card option to export the app
|
||||
selectAppCardOption(
|
||||
data.appName,
|
||||
commonSelectors.appCardOptions(commonText.exportAppOption)
|
||||
);
|
||||
|
||||
// Verify the elements of the export modal
|
||||
verifyElementsOfExportModal("v3", ["v2", "v1"], [true, false, false]);
|
||||
|
||||
// Close the modal
|
||||
closeModal(exportAppModalText.modalCloseButton);
|
||||
|
||||
// Ensure the modal title is no longer visible
|
||||
cy.get(
|
||||
commonSelectors.modalTitle(exportAppModalText.selectVersionTitle)
|
||||
).should("not.exist");
|
||||
|
||||
// Re-open the export modal and click the export button
|
||||
selectAppCardOption(
|
||||
data.appName,
|
||||
commonSelectors.appCardOptions(commonText.exportAppOption)
|
||||
);
|
||||
clickOnExportButtonAndVerify(exportAppModalText.exportAll, data.appName);
|
||||
|
||||
cy.exec("ls ./cypress/downloads/").then((result) => {
|
||||
const downloadedAppExportFileName = result.stdout.split("\n")[0];
|
||||
const filePath = `./cypress/downloads/${downloadedAppExportFileName}`;
|
||||
|
||||
// Ensure the file name contains the expected app export name
|
||||
expect(downloadedAppExportFileName).to.contain(
|
||||
data.appName.toLowerCase()
|
||||
);
|
||||
|
||||
// Read and validate the exported JSON file
|
||||
cy.readFile(filePath).then((appData) => {
|
||||
// Validate the app name
|
||||
const appNameFromFile = appData.app[0].definition.appV2.name;
|
||||
expect(appNameFromFile).to.equal(data.appName);
|
||||
|
||||
// Validate the schema for the student table in tooljetdb
|
||||
const tooljetDatabase = appData.tooljet_database.find(
|
||||
(db) => db.table_name === "student"
|
||||
);
|
||||
expect(tooljetDatabase).to.exist;
|
||||
expect(tooljetDatabase.schema).to.exist;
|
||||
|
||||
// Validate components and queries
|
||||
const components = appData.app[0].definition.appV2.components;
|
||||
|
||||
const text2Component = components.find(
|
||||
(component) => component.name === "text2"
|
||||
);
|
||||
expect(text2Component).to.exist;
|
||||
expect(text2Component.properties.text.value).to.equal(
|
||||
"{{constants.pageHeader}}"
|
||||
);
|
||||
|
||||
const textinput1 = components.find(
|
||||
(component) => component.name === "textinput1"
|
||||
);
|
||||
expect(textinput1).to.exist;
|
||||
expect(textinput1.properties.value.value).to.include("queries");
|
||||
|
||||
const textinput2 = components.find(
|
||||
(component) => component.name === "textinput2"
|
||||
);
|
||||
expect(textinput2).to.exist;
|
||||
expect(textinput2.properties.value.value).to.include("queries");
|
||||
|
||||
const textinput3 = components.find(
|
||||
(component) => component.name === "textinput3"
|
||||
);
|
||||
expect(textinput3).to.exist;
|
||||
expect(textinput3.properties.value.value).to.include("queries");
|
||||
|
||||
// Validate the data queries
|
||||
const dataQueries = appData.app[0].definition.appV2.dataQueries;
|
||||
|
||||
const postgresqlQuery = dataQueries.find(
|
||||
(query) => query.name === "postgresql1"
|
||||
);
|
||||
expect(postgresqlQuery).to.exist;
|
||||
expect(postgresqlQuery.options.query).to.include(
|
||||
"Select * from {{secrets.db_name}}"
|
||||
);
|
||||
|
||||
const restapiQuery = dataQueries.find(
|
||||
(query) => query.name === "restapi1"
|
||||
);
|
||||
expect(restapiQuery).to.exist;
|
||||
expect(restapiQuery.options.url).to.equal(
|
||||
"https://jsonplaceholder.typicode.com/users/1"
|
||||
);
|
||||
|
||||
const tooljetdbQuery = dataQueries.find(
|
||||
(query) => query.name === "tooljetdb1"
|
||||
);
|
||||
expect(tooljetdbQuery).to.exist;
|
||||
expect(tooljetdbQuery.options.operation).to.equal("list_rows");
|
||||
|
||||
// Ensure appVersions exists
|
||||
const appVersions = appData.app[0].definition.appV2.appVersions;
|
||||
expect(appVersions).to.exist;
|
||||
|
||||
// Map and verify app version names
|
||||
const versionNames = appVersions.map((version) => version.name);
|
||||
expect(versionNames).to.include.members(["v1", "v2", "v3"]);
|
||||
});
|
||||
});
|
||||
|
||||
cy.exec("cd ./cypress/downloads/ && rm -rf *");
|
||||
|
||||
selectAppCardOption(
|
||||
data.appName,
|
||||
commonSelectors.appCardOptions(commonText.exportAppOption)
|
||||
);
|
||||
cy.get(`[data-cy="v1-radio-button"]`).check();
|
||||
cy.get(
|
||||
commonSelectors.buttonSelector(exportAppModalText.exportSelectedVersion)
|
||||
).click();
|
||||
|
||||
cy.exec("ls ./cypress/downloads/").then((result) => {
|
||||
const downloadedAppExportFileName = result.stdout.split("\n")[0];
|
||||
const filePath = `./cypress/downloads/${downloadedAppExportFileName}`;
|
||||
|
||||
// Ensure the file name contains the expected app export name
|
||||
expect(downloadedAppExportFileName).to.contain(
|
||||
data.appName.toLowerCase()
|
||||
);
|
||||
|
||||
// Read and validate the exported JSON file
|
||||
cy.readFile(filePath).then((appData) => {
|
||||
// Validate the app name
|
||||
const appNameFromFile = appData.app[0].definition.appV2.name;
|
||||
expect(appNameFromFile).to.equal(data.appName);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it.skip("Verify 'Export app' functionality of an application inside app editor", () => {
|
||||
data.appName2 = `${fake.companyName}-App`;
|
||||
cy.apiCreateApp(data.appName2);
|
||||
cy.openApp(data.appName2);
|
||||
|
||||
cy.dragAndDropWidget("Text Input", 50, 50);
|
||||
|
||||
cy.get('[data-cy="left-sidebar-settings-button"]').click();
|
||||
cy.get('[data-cy="button-user-status-change"]').click();
|
||||
|
||||
verifyElementsOfExportModal("v1");
|
||||
|
||||
exportAllVersionsAndVerify(data.appName1, "v1");
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,230 @@
|
|||
import { fake } from "Fixtures/fake";
|
||||
import { commonSelectors, commonWidgetSelector } from "Selectors/common";
|
||||
import { appVersionSelectors, importSelectors } from "Selectors/exportImport";
|
||||
import { dashboardSelector } from "Selectors/dashboard";
|
||||
import { buttonText } from "Texts/button";
|
||||
|
||||
import { importText } from "Texts/exportImport";
|
||||
import { importAndVerifyApp } from "Support/utils/exportImport";
|
||||
import { switchVersionAndVerify } from "Support/utils/version";
|
||||
|
||||
describe("App Import Functionality", () => {
|
||||
const TEST_DATA = {
|
||||
toolJetImage: "cypress/fixtures/Image/tooljet.png",
|
||||
invalidApp: "cypress/fixtures/templates/invalid_app.json",
|
||||
invalidFile: "cypress/fixtures/templates/invalid_file.json",
|
||||
appFiles: {
|
||||
multiVersion: "cypress/fixtures/templates/three-versions.json",
|
||||
singleVersion: "cypress/fixtures/templates/one_version.json",
|
||||
},
|
||||
};
|
||||
|
||||
let data;
|
||||
|
||||
beforeEach(() => {
|
||||
cy.viewport(1200, 1300);
|
||||
data = {
|
||||
workspaceName: fake.firstName,
|
||||
workspaceSlug: fake.firstName.toLowerCase().replace(/\s+/g, "-"),
|
||||
appName: `${fake.companyName}-IE-App`,
|
||||
appReName: `${fake.companyName}-${fake.companyName}-IE-App`,
|
||||
dsName: fake.lastName.toLowerCase().replaceAll("[^A-Za-z]", ""),
|
||||
};
|
||||
|
||||
cy.apiLogin();
|
||||
cy.apiCreateWorkspace(data.workspaceName, data.workspaceSlug);
|
||||
cy.apiLogout();
|
||||
});
|
||||
|
||||
it("should verify app import functionality", () => {
|
||||
cy.apiLogin();
|
||||
cy.visit(`${data.workspaceSlug}`);
|
||||
|
||||
// Test invalid file import
|
||||
cy.get(dashboardSelector.importAppButton).click();
|
||||
importAndVerifyApp(
|
||||
TEST_DATA.toolJetImage,
|
||||
importText.couldNotImportAppToastMessage
|
||||
);
|
||||
|
||||
cy.wait(500);
|
||||
cy.get(dashboardSelector.importAppButton).click();
|
||||
importAndVerifyApp(
|
||||
TEST_DATA.invalidApp,
|
||||
"Could not import: SyntaxError: Expected ',' or '}' after property value in JSON at position 246 (line 11 column 13)"
|
||||
);
|
||||
|
||||
cy.wait(500);
|
||||
|
||||
// Test valid app import
|
||||
cy.get(importSelectors.dropDownMenu).should("be.visible").click();
|
||||
cy.get(importSelectors.importOptionLabel).verifyVisibleElement(
|
||||
"have.text",
|
||||
importText.importOption
|
||||
);
|
||||
|
||||
cy.intercept("POST", "/api/v2/resources/import").as("importApp");
|
||||
cy.get(importSelectors.importOptionInput)
|
||||
.eq(0)
|
||||
.selectFile(TEST_DATA.appFiles.multiVersion, {
|
||||
force: true,
|
||||
});
|
||||
cy.wait(1500);
|
||||
|
||||
cy.get(importSelectors.importAppTitle).verifyVisibleElement(
|
||||
"have.text",
|
||||
"Import app"
|
||||
);
|
||||
cy.get(commonSelectors.appNameLabel).verifyVisibleElement(
|
||||
"have.text",
|
||||
"App name"
|
||||
);
|
||||
cy.get(commonSelectors.appNameInput)
|
||||
.should("be.visible")
|
||||
.and("have.value", "three-versions");
|
||||
cy.get(commonSelectors.appNameInfoLabel).verifyVisibleElement(
|
||||
"have.text",
|
||||
"App name must be unique and max 50 characters"
|
||||
);
|
||||
cy.get(commonSelectors.cancelButton)
|
||||
.should("be.visible")
|
||||
.and("have.text", "Cancel");
|
||||
cy.get(commonSelectors.importAppButton).verifyVisibleElement(
|
||||
"have.text",
|
||||
"Import app"
|
||||
);
|
||||
|
||||
cy.get(importSelectors.importAppButton).click();
|
||||
cy.get(".go3958317564")
|
||||
.should("be.visible")
|
||||
.and("have.text", importText.appImportedToastMessage);
|
||||
|
||||
// Verify imported app
|
||||
cy.get(".driver-close-btn").click();
|
||||
cy.wait(500);
|
||||
cy.get(commonSelectors.appNameInput).verifyVisibleElement(
|
||||
"contain.value",
|
||||
"three-versions"
|
||||
);
|
||||
|
||||
// Configure app
|
||||
cy.skipEditorPopover();
|
||||
cy.dragAndDropWidget(buttonText.defaultWidgetText);
|
||||
cy.get(appVersionSelectors.appVersionLabel).should("be.visible");
|
||||
cy.get(commonWidgetSelector.draggableWidget("button1")).should(
|
||||
"be.visible"
|
||||
);
|
||||
|
||||
cy.renameApp(data.appName);
|
||||
cy.get(commonSelectors.appNameInput).verifyVisibleElement(
|
||||
"contain.value",
|
||||
data.appName
|
||||
);
|
||||
cy.waitForAutoSave();
|
||||
|
||||
// Verify initial widget states
|
||||
|
||||
verifyCommonData({
|
||||
text2: "",
|
||||
textInput1: "",
|
||||
textInput2: "Leanne Graham",
|
||||
});
|
||||
|
||||
// cy.get(
|
||||
// commonWidgetSelector.draggableWidget("textInput3")
|
||||
// ).verifyVisibleElement("have.value", "");
|
||||
|
||||
// Setup database and data sources
|
||||
cy.visit(`${data.workspaceSlug}/database`);
|
||||
cy.get('[data-cy="student-table"]').verifyVisibleElement(
|
||||
"have.text",
|
||||
"student"
|
||||
);
|
||||
|
||||
// cy.apiAddDataToTable("student", {
|
||||
// name: "Paramu",
|
||||
// country: "India",
|
||||
// state: "Kerala",
|
||||
// });
|
||||
|
||||
cy.visit(`${data.workspaceSlug}/data-sources`);
|
||||
cy.get('[data-cy="postgresql-button"]').should("be.visible");
|
||||
cy.apiUpdateDataSource("postgresql", "production", {
|
||||
options: [
|
||||
{
|
||||
key: "password",
|
||||
value: `${Cypress.env("pg_password")}`,
|
||||
encrypted: true,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
cy.apiCreateWsConstant(
|
||||
"pageHeader",
|
||||
"Import and Export",
|
||||
["Global"],
|
||||
["production"]
|
||||
);
|
||||
cy.apiCreateWsConstant("db_name", "persons", ["Secret"], ["production"]);
|
||||
|
||||
// Verify app after setup
|
||||
cy.wait("@importApp").then((interception) => {
|
||||
const appId = interception.response.body.imports.app[0].id;
|
||||
cy.openApp(
|
||||
"",
|
||||
Cypress.env("workspaceId"),
|
||||
appId,
|
||||
commonWidgetSelector.draggableWidget("text2")
|
||||
);
|
||||
});
|
||||
|
||||
verifyCommonData({
|
||||
text2: "Import and Export",
|
||||
textInput1: "John",
|
||||
textInput2: "Leanne Graham",
|
||||
});
|
||||
// cy.get(
|
||||
// commonWidgetSelector.draggableWidget("textInput3")
|
||||
// ).verifyVisibleElement("have.value", "India");
|
||||
|
||||
switchVersionAndVerify("v3", "v1");
|
||||
|
||||
verifyCommonData({
|
||||
text2: "Import and Export",
|
||||
textInput1: "John",
|
||||
textInput2: "Leanne Graham",
|
||||
});
|
||||
|
||||
cy.wait(1000);
|
||||
cy.backToApps();
|
||||
|
||||
// Test single version import
|
||||
cy.get(importSelectors.dropDownMenu).click();
|
||||
importAndVerifyApp(TEST_DATA.appFiles.singleVersion);
|
||||
|
||||
// Verify final state
|
||||
cy.get(commonSelectors.appNameInput).verifyVisibleElement(
|
||||
"contain.value",
|
||||
"one_version"
|
||||
);
|
||||
|
||||
verifyCommonData({
|
||||
text2: "Import and Export",
|
||||
textInput1: "John",
|
||||
textInput2: "Leanne Graham",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
const verifyCommonData = (values) => {
|
||||
cy.get(commonWidgetSelector.draggableWidget("text2")).verifyVisibleElement(
|
||||
"have.text",
|
||||
values.text2
|
||||
);
|
||||
cy.get(
|
||||
commonWidgetSelector.draggableWidget("textInput1")
|
||||
).verifyVisibleElement("have.value", values.textInput1);
|
||||
cy.get(
|
||||
commonWidgetSelector.draggableWidget("textInput2")
|
||||
).verifyVisibleElement("have.value", values.textInput2);
|
||||
};
|
||||
|
|
@ -1,419 +0,0 @@
|
|||
import { fake } from "Fixtures/fake";
|
||||
import { commonSelectors, commonWidgetSelector } from "Selectors/common";
|
||||
import { appVersionSelectors, importSelectors } from "Selectors/exportImport";
|
||||
import { commonText } from "Texts/common";
|
||||
import { dashboardSelector } from "Selectors/dashboard";
|
||||
import { buttonText } from "Texts/button";
|
||||
|
||||
import { exportAppModalText, importText } from "Texts/exportImport";
|
||||
import {
|
||||
clickOnExportButtonAndVerify,
|
||||
exportAllVersionsAndVerify,
|
||||
verifyElementsOfExportModal,
|
||||
importAndVerifyApp,
|
||||
} from "Support/utils/exportImport";
|
||||
import { selectAppCardOption, closeModal } from "Support/utils/common";
|
||||
import { switchVersionAndVerify } from "Support/utils/version";
|
||||
|
||||
describe("App Import Functionality", () => {
|
||||
const TEST_DATA = {
|
||||
toolJetImage: "cypress/fixtures/Image/tooljet.png",
|
||||
invalidApp: "cypress/fixtures/templates/invalid_app.json",
|
||||
invalidFile: "cypress/fixtures/templates/invalid_file.json",
|
||||
appFiles: {
|
||||
multiVersion: "cypress/fixtures/templates/three-versions.json",
|
||||
singleVersion: "cypress/fixtures/templates/one_version.json",
|
||||
},
|
||||
};
|
||||
|
||||
let data;
|
||||
|
||||
const initializeData = () => {
|
||||
const firstName = fake.firstName;
|
||||
return {
|
||||
workspaceName: firstName,
|
||||
workspaceSlug: firstName.toLowerCase().replace(/\s+/g, "-"),
|
||||
appName: `${fake.companyName}-IE-App`,
|
||||
appReName: `${fake.companyName}-${fake.companyName}-IE-App`,
|
||||
dsName: fake.lastName.toLowerCase().replaceAll("[^A-Za-z]", ""),
|
||||
};
|
||||
};
|
||||
|
||||
data = initializeData();
|
||||
|
||||
before(() => {
|
||||
cy.exec("mkdir -p ./cypress/downloads/");
|
||||
cy.wait(3000);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
cy.viewport(1200, 1300);
|
||||
cy.apiLogin();
|
||||
});
|
||||
|
||||
it("should verify app import functionality", () => {
|
||||
cy.apiCreateWorkspace(data.workspaceName, data.workspaceSlug);
|
||||
cy.apiLogout();
|
||||
cy.apiLogin();
|
||||
cy.visit(`${data.workspaceSlug}`);
|
||||
|
||||
// Test invalid file import
|
||||
cy.get(dashboardSelector.importAppButton).click();
|
||||
importAndVerifyApp(
|
||||
TEST_DATA.toolJetImage,
|
||||
importText.couldNotImportAppToastMessage
|
||||
);
|
||||
|
||||
cy.wait(500);
|
||||
cy.get(dashboardSelector.importAppButton).click();
|
||||
importAndVerifyApp(
|
||||
TEST_DATA.invalidApp,
|
||||
"Could not import: SyntaxError: Expected ',' or '}' after property value in JSON at position 246 (line 11 column 13)"
|
||||
);
|
||||
|
||||
cy.wait(500);
|
||||
cy.get(dashboardSelector.importAppButton).click();
|
||||
cy.get(importSelectors.importOptionInput)
|
||||
.eq(0)
|
||||
.selectFile(TEST_DATA.invalidFile, {
|
||||
force: true,
|
||||
});
|
||||
cy.get(importSelectors.importAppTitle).should("be.visible");
|
||||
cy.get(importSelectors.importAppButton).click();
|
||||
cy.verifyToastMessage(
|
||||
commonSelectors.toastMessage,
|
||||
"tooljet_version must be a string"
|
||||
);
|
||||
cy.wait(500);
|
||||
|
||||
// Test valid app import
|
||||
cy.get(importSelectors.dropDownMenu).should("be.visible").click();
|
||||
cy.get(importSelectors.importOptionLabel).verifyVisibleElement(
|
||||
"have.text",
|
||||
importText.importOption
|
||||
);
|
||||
|
||||
cy.intercept("POST", "/api/v2/resources/import").as("importApp");
|
||||
cy.get(importSelectors.importOptionInput)
|
||||
.eq(0)
|
||||
.selectFile(TEST_DATA.appFiles.multiVersion, {
|
||||
force: true,
|
||||
});
|
||||
cy.wait(1500);
|
||||
|
||||
cy.get(importSelectors.importAppTitle).verifyVisibleElement(
|
||||
"have.text",
|
||||
"Import app"
|
||||
);
|
||||
cy.get(commonSelectors.appNameLabel).verifyVisibleElement(
|
||||
"have.text",
|
||||
"App name"
|
||||
);
|
||||
cy.get(commonSelectors.appNameInput)
|
||||
.should("be.visible")
|
||||
.and("have.value", "three-versions");
|
||||
cy.get(commonSelectors.appNameInfoLabel).verifyVisibleElement(
|
||||
"have.text",
|
||||
"App name must be unique and max 50 characters"
|
||||
);
|
||||
cy.get(commonSelectors.cancelButton)
|
||||
.should("be.visible")
|
||||
.and("have.text", "Cancel");
|
||||
cy.get(commonSelectors.importAppButton).verifyVisibleElement(
|
||||
"have.text",
|
||||
"Import app"
|
||||
);
|
||||
|
||||
cy.get(importSelectors.importAppButton).click();
|
||||
cy.get(".go3958317564")
|
||||
.should("be.visible")
|
||||
.and("have.text", importText.appImportedToastMessage);
|
||||
|
||||
// Verify imported app
|
||||
cy.get(".driver-close-btn").click();
|
||||
cy.wait(500);
|
||||
cy.get(commonSelectors.appNameInput).verifyVisibleElement(
|
||||
"contain.value",
|
||||
"three-versions"
|
||||
);
|
||||
|
||||
// Configure app
|
||||
cy.skipEditorPopover();
|
||||
cy.dragAndDropWidget(buttonText.defaultWidgetText);
|
||||
cy.get(appVersionSelectors.appVersionLabel).should("be.visible");
|
||||
cy.get(commonWidgetSelector.draggableWidget("button1")).should(
|
||||
"be.visible"
|
||||
);
|
||||
|
||||
cy.renameApp(data.appName);
|
||||
cy.get(commonSelectors.appNameInput).verifyVisibleElement(
|
||||
"contain.value",
|
||||
data.appName
|
||||
);
|
||||
cy.waitForAutoSave();
|
||||
|
||||
// Verify initial widget states
|
||||
|
||||
verifyCommonData({
|
||||
text2: "",
|
||||
textInput1: "",
|
||||
textInput2: "Leanne Graham",
|
||||
});
|
||||
|
||||
cy.get(
|
||||
commonWidgetSelector.draggableWidget("textInput3")
|
||||
).verifyVisibleElement("have.value", "");
|
||||
|
||||
// Setup database and data sources
|
||||
cy.visit(`${data.workspaceSlug}/database`);
|
||||
cy.get('[data-cy="student-table"]').verifyVisibleElement(
|
||||
"have.text",
|
||||
"student"
|
||||
);
|
||||
|
||||
cy.apiAddDataToTable("student", {
|
||||
name: "Paramu",
|
||||
country: "India",
|
||||
state: "Kerala",
|
||||
});
|
||||
|
||||
cy.visit(`${data.workspaceSlug}/data-sources`);
|
||||
cy.get('[data-cy="postgresql-button"]').should("be.visible");
|
||||
cy.apiUpdateDataSource("postgresql", "production", {
|
||||
options: [
|
||||
{
|
||||
key: "password",
|
||||
value: `${Cypress.env("pg_password")}`,
|
||||
encrypted: true,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
cy.apiCreateWsConstant(
|
||||
"pageHeader",
|
||||
"Import and Export",
|
||||
["Global"],
|
||||
["production"]
|
||||
);
|
||||
cy.apiCreateWsConstant("db_name", "persons", ["Secret"], ["production"]);
|
||||
|
||||
// Verify app after setup
|
||||
cy.wait("@importApp").then((interception) => {
|
||||
const appId = interception.response.body.imports.app[0].id;
|
||||
cy.openApp(
|
||||
"",
|
||||
Cypress.env("workspaceId"),
|
||||
appId,
|
||||
commonWidgetSelector.draggableWidget("text2")
|
||||
);
|
||||
});
|
||||
|
||||
verifyCommonData({
|
||||
text2: "Import and Export",
|
||||
textInput1: "John",
|
||||
textInput2: "Leanne Graham",
|
||||
});
|
||||
cy.get(
|
||||
commonWidgetSelector.draggableWidget("textInput3")
|
||||
).verifyVisibleElement("have.value", "India");
|
||||
|
||||
switchVersionAndVerify("v3", "v1");
|
||||
|
||||
verifyCommonData({
|
||||
text2: "Import and Export",
|
||||
textInput1: "John",
|
||||
textInput2: "Leanne Graham",
|
||||
});
|
||||
|
||||
cy.wait(1000);
|
||||
cy.backToApps();
|
||||
|
||||
// Test single version import
|
||||
cy.get(importSelectors.dropDownMenu).click();
|
||||
importAndVerifyApp(TEST_DATA.appFiles.singleVersion);
|
||||
|
||||
// Verify final state
|
||||
cy.get(commonSelectors.appNameInput).verifyVisibleElement(
|
||||
"contain.value",
|
||||
"one_version"
|
||||
);
|
||||
|
||||
verifyCommonData({
|
||||
text2: "Import and Export",
|
||||
textInput1: "John",
|
||||
textInput2: "Leanne Graham",
|
||||
});
|
||||
});
|
||||
|
||||
it("Verify the elements of export dialog box", () => {
|
||||
cy.exec("cd ./cypress/downloads/ && rm -rf *");
|
||||
|
||||
cy.visit(`${data.workspaceSlug}`);
|
||||
|
||||
// Select the app card option to export the app
|
||||
selectAppCardOption(
|
||||
data.appName,
|
||||
commonSelectors.appCardOptions(commonText.exportAppOption)
|
||||
);
|
||||
|
||||
// Verify the elements of the export modal
|
||||
verifyElementsOfExportModal("v3", ["v2", "v1"], [true, false, false]);
|
||||
|
||||
// Close the modal
|
||||
closeModal(exportAppModalText.modalCloseButton);
|
||||
|
||||
// Ensure the modal title is no longer visible
|
||||
cy.get(
|
||||
commonSelectors.modalTitle(exportAppModalText.selectVersionTitle)
|
||||
).should("not.exist");
|
||||
|
||||
// Re-open the export modal and click the export button
|
||||
selectAppCardOption(
|
||||
data.appName,
|
||||
commonSelectors.appCardOptions(commonText.exportAppOption)
|
||||
);
|
||||
clickOnExportButtonAndVerify(exportAppModalText.exportAll, data.appName);
|
||||
|
||||
cy.exec("ls ./cypress/downloads/").then((result) => {
|
||||
const downloadedAppExportFileName = result.stdout.split("\n")[0];
|
||||
const filePath = `./cypress/downloads/${downloadedAppExportFileName}`;
|
||||
|
||||
// Ensure the file name contains the expected app export name
|
||||
expect(downloadedAppExportFileName).to.contain(
|
||||
data.appName.toLowerCase()
|
||||
);
|
||||
|
||||
// Read and validate the exported JSON file
|
||||
cy.readFile(filePath).then((appData) => {
|
||||
// Validate the app name
|
||||
const appNameFromFile = appData.app[0].definition.appV2.name;
|
||||
expect(appNameFromFile).to.equal(data.appName);
|
||||
|
||||
// Validate the schema for the student table in tooljetdb
|
||||
const tooljetDatabase = appData.tooljet_database.find(
|
||||
(db) => db.table_name === "student"
|
||||
);
|
||||
expect(tooljetDatabase).to.exist;
|
||||
expect(tooljetDatabase.schema).to.exist;
|
||||
|
||||
// Validate components and queries
|
||||
const components = appData.app[0].definition.appV2.components;
|
||||
|
||||
const text2Component = components.find(
|
||||
(component) => component.name === "text2"
|
||||
);
|
||||
expect(text2Component).to.exist;
|
||||
expect(text2Component.properties.text.value).to.equal(
|
||||
"{{constants.pageHeader}}"
|
||||
);
|
||||
|
||||
const textinput1 = components.find(
|
||||
(component) => component.name === "textinput1"
|
||||
);
|
||||
expect(textinput1).to.exist;
|
||||
expect(textinput1.properties.value.value).to.include("queries");
|
||||
|
||||
const textinput2 = components.find(
|
||||
(component) => component.name === "textinput2"
|
||||
);
|
||||
expect(textinput2).to.exist;
|
||||
expect(textinput2.properties.value.value).to.include("queries");
|
||||
|
||||
const textinput3 = components.find(
|
||||
(component) => component.name === "textinput3"
|
||||
);
|
||||
expect(textinput3).to.exist;
|
||||
expect(textinput3.properties.value.value).to.include("queries");
|
||||
|
||||
// Validate the data queries
|
||||
const dataQueries = appData.app[0].definition.appV2.dataQueries;
|
||||
|
||||
const postgresqlQuery = dataQueries.find(
|
||||
(query) => query.name === "postgresql1"
|
||||
);
|
||||
expect(postgresqlQuery).to.exist;
|
||||
expect(postgresqlQuery.options.query).to.include(
|
||||
"Select * from {{secrets.db_name}}"
|
||||
);
|
||||
|
||||
const restapiQuery = dataQueries.find(
|
||||
(query) => query.name === "restapi1"
|
||||
);
|
||||
expect(restapiQuery).to.exist;
|
||||
expect(restapiQuery.options.url).to.equal(
|
||||
"https://jsonplaceholder.typicode.com/users/1"
|
||||
);
|
||||
|
||||
const tooljetdbQuery = dataQueries.find(
|
||||
(query) => query.name === "tooljetdb1"
|
||||
);
|
||||
expect(tooljetdbQuery).to.exist;
|
||||
expect(tooljetdbQuery.options.operation).to.equal("list_rows");
|
||||
|
||||
// Ensure appVersions exists
|
||||
const appVersions = appData.app[0].definition.appV2.appVersions;
|
||||
expect(appVersions).to.exist;
|
||||
|
||||
// Map and verify app version names
|
||||
const versionNames = appVersions.map((version) => version.name);
|
||||
expect(versionNames).to.include.members(["v1", "v2", "v3"]);
|
||||
});
|
||||
});
|
||||
|
||||
cy.exec("cd ./cypress/downloads/ && rm -rf *");
|
||||
|
||||
selectAppCardOption(
|
||||
data.appName,
|
||||
commonSelectors.appCardOptions(commonText.exportAppOption)
|
||||
);
|
||||
cy.get(`[data-cy="v1-radio-button"]`).check();
|
||||
cy.get(
|
||||
commonSelectors.buttonSelector(exportAppModalText.exportSelectedVersion)
|
||||
).click();
|
||||
|
||||
cy.exec("ls ./cypress/downloads/").then((result) => {
|
||||
const downloadedAppExportFileName = result.stdout.split("\n")[0];
|
||||
const filePath = `./cypress/downloads/${downloadedAppExportFileName}`;
|
||||
|
||||
// Ensure the file name contains the expected app export name
|
||||
expect(downloadedAppExportFileName).to.contain(
|
||||
data.appName.toLowerCase()
|
||||
);
|
||||
|
||||
// Read and validate the exported JSON file
|
||||
cy.readFile(filePath).then((appData) => {
|
||||
// Validate the app name
|
||||
const appNameFromFile = appData.app[0].definition.appV2.name;
|
||||
expect(appNameFromFile).to.equal(data.appName);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it.skip("Verify 'Export app' functionality of an application inside app editor", () => {
|
||||
data.appName2 = `${fake.companyName}-App`;
|
||||
cy.apiCreateApp(data.appName2);
|
||||
cy.openApp(data.appName2);
|
||||
|
||||
cy.dragAndDropWidget("Text Input", 50, 50);
|
||||
|
||||
cy.get('[data-cy="left-sidebar-settings-button"]').click();
|
||||
cy.get('[data-cy="button-user-status-change"]').click();
|
||||
|
||||
verifyElementsOfExportModal("v1");
|
||||
|
||||
exportAllVersionsAndVerify(data.appName1, "v1");
|
||||
});
|
||||
});
|
||||
|
||||
const verifyCommonData = (values) => {
|
||||
cy.get(commonWidgetSelector.draggableWidget("text2")).verifyVisibleElement(
|
||||
"have.text",
|
||||
values.text2
|
||||
);
|
||||
cy.get(
|
||||
commonWidgetSelector.draggableWidget("textInput1")
|
||||
).verifyVisibleElement("have.value", values.textInput1);
|
||||
cy.get(
|
||||
commonWidgetSelector.draggableWidget("textInput2")
|
||||
).verifyVisibleElement("have.value", values.textInput2);
|
||||
};
|
||||
|
|
@ -15,7 +15,6 @@ describe("App Slug", () => {
|
|||
beforeEach(() => {
|
||||
data.slug = `${fake.companyName.toLowerCase()}-app`;
|
||||
data.appName = `${fake.companyName} App`;
|
||||
cy.log(Cypress.env("workspaceId"));
|
||||
cy.defaultWorkspaceLogin();
|
||||
});
|
||||
|
||||
|
|
@ -25,133 +24,137 @@ describe("App Slug", () => {
|
|||
cy.apiCreateApp(data.appName);
|
||||
cy.wait(1000);
|
||||
cy.apiLogout();
|
||||
cy.log(Cypress.env("workspaceId"));
|
||||
|
||||
});
|
||||
|
||||
it("Verify app slug cases in global settings", () => {
|
||||
cy.apiLogin("dev@tooljet.io", "password").then(() => {
|
||||
const workspaceId = Cypress.env("workspaceId");
|
||||
const appId = Cypress.env("appId");
|
||||
cy.apiLogin();
|
||||
const workspaceId = Cypress.env("workspaceId");
|
||||
const appId = Cypress.env("appId");
|
||||
|
||||
cy.openApp("my-workspace");
|
||||
cy.get(commonSelectors.leftSideBarSettingsButton).click();
|
||||
cy.visit("/my-workspace");
|
||||
cy.wait(1000);
|
||||
|
||||
// Verify initial state
|
||||
cy.get(commonWidgetSelector.appSlugLabel).verifyVisibleElement(
|
||||
"have.text",
|
||||
"Unique app slug"
|
||||
);
|
||||
cy.get(commonWidgetSelector.appSlugInput).verifyVisibleElement(
|
||||
"have.value",
|
||||
Cypress.env("appId")
|
||||
);
|
||||
cy.get(commonWidgetSelector.appSlugInfoLabel).verifyVisibleElement(
|
||||
"have.text",
|
||||
"URL-friendly 'slug' consists of lowercase letters, numbers, and hyphens"
|
||||
);
|
||||
|
||||
cy.get(commonWidgetSelector.appLinkLabel).verifyVisibleElement(
|
||||
"have.text",
|
||||
"App link"
|
||||
);
|
||||
|
||||
cy.get(commonWidgetSelector.appLinkField).verifyVisibleElement(
|
||||
"have.text",
|
||||
`${host}/${workspaceId}/apps/${appId}`
|
||||
);
|
||||
|
||||
// Validate all error cases
|
||||
verifySlugValidations(commonWidgetSelector.appSlugInput);
|
||||
|
||||
// Verify successful slug update
|
||||
cy.clearAndType(commonWidgetSelector.appSlugInput, data.slug);
|
||||
verifySuccessfulSlugUpdate(workspaceId, data.slug);
|
||||
|
||||
// Verify persistence
|
||||
cy.get('[data-cy="left-sidebar-debugger-button"]').click();
|
||||
cy.get(commonSelectors.leftSideBarSettingsButton).click();
|
||||
cy.get(commonWidgetSelector.appSlugInput).should("have.value", data.slug);
|
||||
|
||||
// Release and verify URLs
|
||||
releaseApp();
|
||||
verifyURLs(workspaceId, data.slug, false);
|
||||
|
||||
// Verify duplicate slug validation
|
||||
cy.visit("/my-workspace");
|
||||
cy.apiCreateApp(data.slug);
|
||||
cy.openApp("my-workspace");
|
||||
cy.get(commonSelectors.leftSideBarSettingsButton).click();
|
||||
cy.get(commonWidgetSelector.appSlugInput).clear();
|
||||
cy.clearAndType(commonWidgetSelector.appSlugInput, data.slug);
|
||||
cy.get(commonWidgetSelector.appSlugErrorLabel).verifyVisibleElement(
|
||||
"have.text",
|
||||
"This app slug is already taken."
|
||||
);
|
||||
cy.window({ log: false }).then((win) => {
|
||||
win.localStorage.setItem("walkthroughCompleted", "true");
|
||||
});
|
||||
cy.visit(`/${Cypress.env("workspaceId")}/apps/${Cypress.env("appId")}/`);
|
||||
cy.wait(1000);
|
||||
|
||||
cy.get(commonSelectors.leftSideBarSettingsButton).click();
|
||||
|
||||
// Verify initial state
|
||||
cy.get(commonWidgetSelector.appSlugLabel).verifyVisibleElement(
|
||||
"have.text",
|
||||
"Unique app slug"
|
||||
);
|
||||
cy.get(commonWidgetSelector.appSlugInput).verifyVisibleElement(
|
||||
"have.value",
|
||||
Cypress.env("appId")
|
||||
);
|
||||
cy.get(commonWidgetSelector.appSlugInfoLabel).verifyVisibleElement(
|
||||
"have.text",
|
||||
"URL-friendly 'slug' consists of lowercase letters, numbers, and hyphens"
|
||||
);
|
||||
|
||||
cy.get(commonWidgetSelector.appLinkLabel).verifyVisibleElement(
|
||||
"have.text",
|
||||
"App link"
|
||||
);
|
||||
|
||||
cy.get(commonWidgetSelector.appLinkField).verifyVisibleElement(
|
||||
"have.text",
|
||||
`${host}/${workspaceId}/apps/${appId}`
|
||||
);
|
||||
|
||||
// Validate all error cases
|
||||
verifySlugValidations(commonWidgetSelector.appSlugInput);
|
||||
|
||||
// Verify successful slug update
|
||||
cy.clearAndType(commonWidgetSelector.appSlugInput, data.slug);
|
||||
verifySuccessfulSlugUpdate(workspaceId, data.slug);
|
||||
|
||||
// Verify persistence
|
||||
cy.get('[data-cy="left-sidebar-debugger-button"]').click();
|
||||
cy.get(commonSelectors.leftSideBarSettingsButton).click();
|
||||
cy.get(commonWidgetSelector.appSlugInput).should("have.value", data.slug);
|
||||
|
||||
// Release and verify URLs
|
||||
releaseApp();
|
||||
verifyURLs(workspaceId, data.slug, false);
|
||||
|
||||
// Verify duplicate slug validation
|
||||
cy.visit("/my-workspace");
|
||||
cy.apiCreateApp(data.slug);
|
||||
cy.openApp("my-workspace");
|
||||
cy.get(commonSelectors.leftSideBarSettingsButton).click();
|
||||
cy.get(commonWidgetSelector.appSlugInput).clear();
|
||||
cy.clearAndType(commonWidgetSelector.appSlugInput, data.slug);
|
||||
cy.get(commonWidgetSelector.appSlugErrorLabel).verifyVisibleElement(
|
||||
"have.text",
|
||||
"This app slug is already taken."
|
||||
);
|
||||
});
|
||||
|
||||
it("Verify app slug cases in share modal", () => {
|
||||
cy.apiLogin("dev@tooljet.io", "password").then(() => {
|
||||
const workspaceId = Cypress.env("workspaceId");
|
||||
cy.apiLogin();
|
||||
const workspaceId = Cypress.env("workspaceId");
|
||||
|
||||
cy.apiCreateApp(data.appName);
|
||||
cy.openApp("my-workspace");
|
||||
cy.apiCreateApp(data.appName);
|
||||
cy.openApp("my-workspace");
|
||||
|
||||
// Set up initial slug
|
||||
cy.get(commonSelectors.leftSideBarSettingsButton).click();
|
||||
cy.get(commonWidgetSelector.appSlugInput).clear();
|
||||
cy.clearAndType(commonWidgetSelector.appSlugInput, data.slug);
|
||||
// Set up initial slug
|
||||
cy.get(commonSelectors.leftSideBarSettingsButton).click();
|
||||
cy.get(commonWidgetSelector.appSlugInput).clear();
|
||||
cy.clearAndType(commonWidgetSelector.appSlugInput, data.slug);
|
||||
|
||||
releaseApp();
|
||||
releaseApp();
|
||||
|
||||
// Verify share modal
|
||||
cy.get(commonWidgetSelector.shareAppButton).click();
|
||||
cy.get(commonWidgetSelector.appLink).verifyVisibleElement(
|
||||
"have.text",
|
||||
`${host}/applications/`
|
||||
);
|
||||
cy.get(commonWidgetSelector.appNameSlugInput).should(
|
||||
"have.value",
|
||||
data.slug
|
||||
);
|
||||
// Verify share modal
|
||||
cy.get(commonWidgetSelector.shareAppButton).click();
|
||||
cy.get(commonWidgetSelector.appLink).verifyVisibleElement(
|
||||
"have.text",
|
||||
`${host}/applications/`
|
||||
);
|
||||
cy.get(commonWidgetSelector.appNameSlugInput).should(
|
||||
"have.value",
|
||||
data.slug
|
||||
);
|
||||
|
||||
// Validate all error cases in share modal
|
||||
verifySlugValidations(commonWidgetSelector.appNameSlugInput);
|
||||
// Validate all error cases in share modal
|
||||
verifySlugValidations(commonWidgetSelector.appNameSlugInput);
|
||||
|
||||
cy.wait(500);
|
||||
cy.clearAndType(commonWidgetSelector.appNameSlugInput, data.slug);
|
||||
cy.get('[data-cy="app-slug-info-label"]')
|
||||
.invoke("text")
|
||||
.then((text) => {
|
||||
expect(text.trim()).to.eq(
|
||||
"URL-friendly 'slug' consists of lowercase letters, numbers, and hyphens"
|
||||
);
|
||||
});
|
||||
cy.wait(500);
|
||||
cy.clearAndType(commonWidgetSelector.appNameSlugInput, data.slug);
|
||||
cy.get('[data-cy="app-slug-info-label"]')
|
||||
.invoke("text")
|
||||
.then((text) => {
|
||||
expect(text.trim()).to.eq(
|
||||
"URL-friendly 'slug' consists of lowercase letters, numbers, and hyphens"
|
||||
);
|
||||
});
|
||||
|
||||
// Verify successful slug update in share modal
|
||||
data.slug = `${fake.companyName.toLowerCase()}-app`;
|
||||
cy.clearAndType(commonWidgetSelector.appNameSlugInput, data.slug);
|
||||
cy.get('[data-cy="app-slug-accepted-label"]').verifyVisibleElement(
|
||||
"have.text",
|
||||
"Slug accepted!"
|
||||
);
|
||||
// Verify successful slug update in share modal
|
||||
data.slug = `${fake.companyName.toLowerCase()}-app`;
|
||||
cy.clearAndType(commonWidgetSelector.appNameSlugInput, data.slug);
|
||||
cy.get('[data-cy="app-slug-accepted-label"]').verifyVisibleElement(
|
||||
"have.text",
|
||||
"Slug accepted!"
|
||||
);
|
||||
|
||||
// Close modal and verify URLs
|
||||
cy.get(commonWidgetSelector.modalCloseButton).click();
|
||||
verifyURLs(workspaceId, data.slug, true);
|
||||
// Close modal and verify URLs
|
||||
cy.get(commonWidgetSelector.modalCloseButton).click();
|
||||
verifyURLs(workspaceId, data.slug, true);
|
||||
|
||||
// Verify duplicate slug validation in share modal
|
||||
cy.visit("/my-workspace");
|
||||
cy.apiCreateApp(data.slug);
|
||||
cy.openApp("my-workspace");
|
||||
releaseApp();
|
||||
cy.get(commonWidgetSelector.shareAppButton).click();
|
||||
cy.clearAndType(commonWidgetSelector.appNameSlugInput, data.slug);
|
||||
cy.get(commonWidgetSelector.appSlugErrorLabel).verifyVisibleElement(
|
||||
"have.text",
|
||||
"This app slug is already taken."
|
||||
);
|
||||
});
|
||||
// Verify duplicate slug validation in share modal
|
||||
cy.visit("/my-workspace");
|
||||
cy.apiCreateApp(data.slug);
|
||||
cy.openApp("my-workspace");
|
||||
releaseApp();
|
||||
cy.get(commonWidgetSelector.shareAppButton).click();
|
||||
cy.clearAndType(commonWidgetSelector.appNameSlugInput, data.slug);
|
||||
cy.get(commonWidgetSelector.appSlugErrorLabel).verifyVisibleElement(
|
||||
"have.text",
|
||||
"This app slug is already taken."
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -20,20 +20,20 @@ import {
|
|||
describe("Private and Public apps", {
|
||||
retries: { runMode: 2 },
|
||||
}, () => {
|
||||
const data = {};
|
||||
let data;
|
||||
|
||||
beforeEach(() => {
|
||||
data.appName = `${fake.companyName} P P App`;
|
||||
data.slug = data.appName.toLowerCase().replace(/\s+/g, "-");
|
||||
data.firstName = fake.firstName;
|
||||
data.email = fake.email.toLowerCase();
|
||||
data.workspaceName = fake.firstName;
|
||||
data.workspaceSlug = fake.firstName.toLowerCase().replace(/\s+/g, "-");
|
||||
data = {
|
||||
appName: `${fake.companyName} P P App`,
|
||||
slug: `${fake.companyName} P P App`.toLowerCase().replace(/\s+/g, "-"),
|
||||
firstName: fake.firstName,
|
||||
email: fake.email.toLowerCase(),
|
||||
workspaceName: fake.firstName,
|
||||
workspaceSlug: fake.firstName.toLowerCase().replace(/\s+/g, "-"),
|
||||
}
|
||||
|
||||
cy.defaultWorkspaceLogin();
|
||||
cy.skipWalkthrough();
|
||||
cy.log(data.appName, "text1")
|
||||
|
||||
});
|
||||
|
||||
it("Verify private and public app share functionality", () => {
|
||||
|
|
@ -85,9 +85,9 @@ describe("Private and Public apps", {
|
|||
});
|
||||
cy.get(onboardingSelectors.signInButton, { timeout: 20000 }).should("be.visible");
|
||||
cy.wait(2000);
|
||||
cy.loginWithCredentials("dev@tooljet.io", "password");
|
||||
// cy.get(commonWidgetSelector.draggableWidget("text1")).should("be.visible");
|
||||
cy.get('.text-widget-section > div').should("be.visible");
|
||||
cy.appUILogin();
|
||||
cy.get(commonWidgetSelector.draggableWidget("text1")).should("be.visible");
|
||||
|
||||
|
||||
// Test public access
|
||||
cy.get(commonSelectors.viewerPageLogo).click();
|
||||
|
|
@ -106,8 +106,8 @@ describe("Private and Public apps", {
|
|||
cy.visitSlug({
|
||||
actualUrl: `${Cypress.config("baseUrl")}/applications/${data.slug}`,
|
||||
});
|
||||
// cy.get(commonWidgetSelector.draggableWidget("text1")).should("be.visible");
|
||||
cy.get('.text-widget-section > div').should("be.visible");
|
||||
cy.get(commonWidgetSelector.draggableWidget("text1")).should("be.visible");
|
||||
|
||||
|
||||
});
|
||||
|
||||
|
|
@ -123,30 +123,30 @@ describe("Private and Public apps", {
|
|||
});
|
||||
|
||||
cy.wait(2000);
|
||||
cy.loginWithCredentials(data.email, "password");
|
||||
cy.appUILogin(data.email, "password");
|
||||
|
||||
// cy.get(commonWidgetSelector.draggableWidget("text1")).should("be.visible");
|
||||
cy.get('.text-widget-section > div').should("be.visible", { timeout: 20000 });
|
||||
cy.get(commonWidgetSelector.draggableWidget("text1")).should("be.visible");
|
||||
|
||||
// Test with private app valid session
|
||||
cy.visitSlug({
|
||||
actualUrl: `${Cypress.config("baseUrl")}/applications/${data.slug}`,
|
||||
});
|
||||
// cy.get(commonWidgetSelector.draggableWidget("text1")).should("be.visible");
|
||||
cy.get('.text-widget-section > div').should("be.visible");
|
||||
cy.get(commonWidgetSelector.draggableWidget("text1")).should("be.visible");
|
||||
|
||||
|
||||
cy.get(commonSelectors.viewerPageLogo).click();
|
||||
|
||||
// Test public access
|
||||
cy.defaultWorkspaceLogin();
|
||||
cy.wait(1000);
|
||||
cy.apiMakeAppPublic();
|
||||
logout();
|
||||
|
||||
cy.visitSlug({
|
||||
actualUrl: `${Cypress.config("baseUrl")}/applications/${data.slug}`,
|
||||
});
|
||||
// cy.get(commonWidgetSelector.draggableWidget("text1")).should("be.visible");
|
||||
cy.get('.text-widget-section > div').should("be.visible");
|
||||
cy.get(commonWidgetSelector.draggableWidget("text1")).should("be.visible");
|
||||
|
||||
|
||||
|
||||
// Test with public app with valid session
|
||||
|
|
@ -154,8 +154,8 @@ describe("Private and Public apps", {
|
|||
cy.visitSlug({
|
||||
actualUrl: `${Cypress.config("baseUrl")}/applications/${data.slug}`,
|
||||
});
|
||||
// cy.get(commonWidgetSelector.draggableWidget("text1")).should("be.visible");
|
||||
cy.get('.text-widget-section > div').should("be.visible");
|
||||
cy.get(commonWidgetSelector.draggableWidget("text1")).should("be.visible");
|
||||
|
||||
|
||||
});
|
||||
|
||||
|
|
@ -180,8 +180,8 @@ describe("Private and Public apps", {
|
|||
cy.visitSlug({
|
||||
actualUrl: `${Cypress.config("baseUrl")}/applications/${data.slug}`,
|
||||
});
|
||||
// cy.get(commonWidgetSelector.draggableWidget("text1")).should("be.visible");
|
||||
cy.get('.text-widget-section > div').should("be.visible");
|
||||
cy.get(commonWidgetSelector.draggableWidget("text1")).should("be.visible");
|
||||
|
||||
|
||||
|
||||
// Verify public app with valid session
|
||||
|
|
@ -189,8 +189,8 @@ describe("Private and Public apps", {
|
|||
cy.visitSlug({
|
||||
actualUrl: `${Cypress.config("baseUrl")}/applications/${data.slug}`,
|
||||
});
|
||||
// cy.get(commonWidgetSelector.draggableWidget("text1")).should("be.visible");
|
||||
cy.get('.text-widget-section > div').should("be.visible");
|
||||
cy.get(commonWidgetSelector.draggableWidget("text1")).should("be.visible");
|
||||
|
||||
|
||||
});
|
||||
|
||||
|
|
@ -224,8 +224,8 @@ describe("Private and Public apps", {
|
|||
// Process invitation
|
||||
onboardUserFromAppLink(data.email, data.slug);
|
||||
|
||||
// cy.get(commonWidgetSelector.draggableWidget("text1")).should("be.visible");
|
||||
cy.get('.text-widget-section > div').should("be.visible");
|
||||
cy.get(commonWidgetSelector.draggableWidget("text1")).should("be.visible");
|
||||
|
||||
|
||||
cy.get('[data-cy="viewer-page-logo"]').click();
|
||||
logout();
|
||||
|
|
@ -269,8 +269,8 @@ describe("Private and Public apps", {
|
|||
});
|
||||
|
||||
onboardUserFromAppLink(data.email, data.slug, data.workspaceName, false);
|
||||
// cy.get(commonWidgetSelector.draggableWidget("text1")).should("be.visible");
|
||||
cy.get('.text-widget-section > div').should("be.visible");
|
||||
cy.get(commonWidgetSelector.draggableWidget("text1")).should("be.visible");
|
||||
|
||||
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -114,7 +114,7 @@ describe("App Version", () => {
|
|||
cy.wait(3000);
|
||||
|
||||
// cy.reload();
|
||||
// cy.get(commonWidgetSelector.draggableWidget("text1")).should("be.visible", { timeout: 10000 });
|
||||
cy.get(commonWidgetSelector.draggableWidget("text1")).should("be.visible", { timeout: 10000 });
|
||||
|
||||
// Preview and release verification
|
||||
cy.openInCurrentTab(commonWidgetSelector.previewButton);
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ describe("Datasource Manager", () => {
|
|||
data.dsName1 = fake.lastName.toLowerCase().replaceAll("[^A-Za-z]", "");
|
||||
data.dsName2 = fake.lastName.toLowerCase().replaceAll("[^A-Za-z]", "");
|
||||
|
||||
const allDataSources = host.includes("8082") ? "All data sources (42)" : "All data sources (44)";
|
||||
const allDataSources = host.includes("8082") ? "All data sources (43)" : "All data sources (45)";
|
||||
const allDatabase = host.includes("8082") ? "Databases (18)" : "Databases (20)";
|
||||
|
||||
cy.get(commonSelectors.globalDataSourceIcon).click();
|
||||
|
|
@ -214,7 +214,7 @@ describe("Datasource Manager", () => {
|
|||
|
||||
cy.get(commonWidgetSelector.sidebarinspector).click();
|
||||
cy.get(dataSourceSelector.queryCreateAndRunButton).click();
|
||||
verifyValueOnInspector("table_preview", "7 items ");
|
||||
verifyValueOnInspector("table_preview", "10 items ");
|
||||
cy.get('[data-cy="show-ds-popover-button"]').click();
|
||||
|
||||
cy.get(".p-2 > .tj-base-btn")
|
||||
|
|
@ -275,7 +275,7 @@ describe("Datasource Manager", () => {
|
|||
pinInspector();
|
||||
|
||||
cy.get(dataSourceSelector.queryCreateAndRunButton).click();
|
||||
verifyValueOnInspector("table_preview", "7 items ");
|
||||
verifyValueOnInspector("table_preview", "10 items ");
|
||||
|
||||
//scope changing is pending
|
||||
});
|
||||
|
|
|
|||
|
|
@ -18,10 +18,17 @@ import { roleBasedOnboarding } from "Support/utils/onboarding";
|
|||
const data = {};
|
||||
data.groupName = fake.firstName.replaceAll("[^A-Za-z]", "");
|
||||
data.appName = `${fake.companyName}-App`;
|
||||
const workspaceName = fake.firstName;
|
||||
const workspaceSlug = fake.firstName.toLowerCase().replace(/[^A-Za-z]/g, "");
|
||||
|
||||
describe("Groups duplication", () => {
|
||||
beforeEach(() => {
|
||||
cy.defaultWorkspaceLogin();
|
||||
cy.apiCreateWorkspace(workspaceName, workspaceSlug);
|
||||
cy.visit(`${workspaceSlug}`);
|
||||
cy.apiLogout();
|
||||
cy.apiLogin();
|
||||
cy.visit(`${workspaceSlug}`);
|
||||
groupPermission(
|
||||
[
|
||||
"appsCreateCheck",
|
||||
|
|
@ -32,15 +39,18 @@ describe("Groups duplication", () => {
|
|||
"Admin"
|
||||
);
|
||||
cy.apiCreateApp(data.appName);
|
||||
|
||||
});
|
||||
|
||||
it("Should verify the group duplication feature", () => {
|
||||
data.firstName = fake.firstName;
|
||||
data.email = fake.email.toLowerCase().replaceAll("[^A-Za-z]", "");
|
||||
cy.visit(`${workspaceSlug}`);
|
||||
roleBasedOnboarding(data.firstName, data.email, "builder");
|
||||
cy.apiLogout();
|
||||
|
||||
cy.defaultWorkspaceLogin();
|
||||
cy.apiLogin();
|
||||
cy.visit(`${workspaceSlug}`);
|
||||
navigateToManageGroups();
|
||||
verifyGroupCardOptions("Admin");
|
||||
cy.wait(3000);
|
||||
|
|
@ -105,15 +115,19 @@ describe("Groups duplication", () => {
|
|||
cy.apiLogout();
|
||||
|
||||
cy.apiLogin(data.email, "password");
|
||||
cy.visit("/my-workspace");
|
||||
cy.visit(`${workspaceSlug}`);
|
||||
cy.wait(2000);
|
||||
cy.get(commonSelectors.appCreateButton).should("be.visible");
|
||||
cy.get(commonSelectors.createNewFolderButton).should("be.visible");
|
||||
cy.wait(2000);
|
||||
cy.reload();
|
||||
viewAppCardOptions(data.appName);
|
||||
cy.contains("Delete app").should("exist");
|
||||
cy.get(commonSelectors.workspaceConstantsIcon).should("be.visible");
|
||||
cy.apiLogout();
|
||||
|
||||
cy.defaultWorkspaceLogin();
|
||||
cy.apiLogin();
|
||||
cy.visit(`${workspaceSlug}`);
|
||||
navigateToManageGroups();
|
||||
OpenGroupCardOption(`${data.groupName}_copy`);
|
||||
cy.get(groupsSelector.deleteGroupOption).click();
|
||||
|
|
@ -121,7 +135,7 @@ describe("Groups duplication", () => {
|
|||
cy.apiLogout();
|
||||
|
||||
cy.apiLogin(data.email, "password");
|
||||
cy.visit("/my-workspace");
|
||||
cy.visit(`${workspaceSlug}`);
|
||||
cy.get(commonSelectors.appCreateButton).should("not.exist");
|
||||
cy.get(commonSelectors.createNewFolderButton).should("not.exist");
|
||||
cy.get(commonSelectors.workspaceConstantsIcon).should("not.exist");
|
||||
|
|
|
|||
|
|
@ -124,7 +124,8 @@ describe("Workspace constants", () => {
|
|||
|
||||
//verify global constant is resolved in static query url
|
||||
cy.get('[data-cy="list-query-restapistaticg"]').click();
|
||||
cy.get('.rest-api-methods-select-element-container .codehinter-container').click();
|
||||
cy.get('.rest-api-methods-select-element-container .codehinter-container').eq(0).click();
|
||||
cy.wait(500)
|
||||
cy.get('.text-secondary').should('have.text', Cypress.env("constants_host"));
|
||||
|
||||
//Verify global constant is resolved in static query preview
|
||||
|
|
|
|||
|
|
@ -200,7 +200,7 @@ describe("user invite flow cases", () => {
|
|||
});
|
||||
});
|
||||
|
||||
it.skip("Should verify the user onboarding with groups", () => {
|
||||
it("Should verify the user onboarding with groups", () => {
|
||||
data.firstName = fake.firstName;
|
||||
data.email = fake.email.toLowerCase().replaceAll("[^A-Za-z]", "");
|
||||
data.groupName1 = fake.firstName.replaceAll("[^A-Za-z]", "");
|
||||
|
|
|
|||
|
|
@ -58,7 +58,8 @@ describe("inviteflow edge cases", () => {
|
|||
cy.verifyToastMessage(commonSelectors.toastMessage, usersText.inviteToast);
|
||||
logout();
|
||||
|
||||
cy.defaultWorkspaceLogin();
|
||||
cy.apiLogin();
|
||||
cy.visit(workspaceName);
|
||||
navigateToManageUsers();
|
||||
searchUser(data.email);
|
||||
cy.contains("td", data.email)
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import { importText } from "Texts/exportImport";
|
|||
|
||||
describe("App creation", () => {
|
||||
const data = {};
|
||||
const appFile = "cypress/fixtures/templates/test-app.json";
|
||||
const appFile = "cypress/fixtures/templates/one_version.json";
|
||||
|
||||
beforeEach(() => {
|
||||
cy.defaultWorkspaceLogin();
|
||||
|
|
@ -200,7 +200,7 @@ describe("App creation", () => {
|
|||
force: true,
|
||||
});
|
||||
|
||||
cy.get(commonSelectors.importAppTitle).verifyVisibleElement(
|
||||
cy.get(importSelectors.importAppTitle).verifyVisibleElement(
|
||||
"have.text",
|
||||
"Import app"
|
||||
);
|
||||
|
|
@ -210,7 +210,7 @@ describe("App creation", () => {
|
|||
);
|
||||
cy.get(commonSelectors.appNameInput).verifyVisibleElement(
|
||||
"have.value",
|
||||
"test-app"
|
||||
"one_version"
|
||||
);
|
||||
cy.get(commonSelectors.appNameInfoLabel).verifyVisibleElement(
|
||||
"have.text",
|
||||
|
|
@ -236,7 +236,7 @@ describe("App creation", () => {
|
|||
});
|
||||
cy.get(commonSelectors.appNameInput).verifyVisibleElement(
|
||||
"have.value",
|
||||
"test-app"
|
||||
"one_version"
|
||||
);
|
||||
cy.clearAndType(commonSelectors.appNameInput, data.appName);
|
||||
cy.get(commonSelectors.cancelButton).click();
|
||||
|
|
@ -247,7 +247,7 @@ describe("App creation", () => {
|
|||
});
|
||||
cy.get(commonSelectors.appNameInput).verifyVisibleElement(
|
||||
"have.value",
|
||||
"test-app"
|
||||
"one_version"
|
||||
);
|
||||
cy.clearAndType(commonSelectors.appNameInput, data.appName);
|
||||
cy.get(commonSelectors.importAppButton).should("be.enabled").click();
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ import { fake } from "Fixtures/fake";
|
|||
import {
|
||||
createFolder,
|
||||
deleteFolder,
|
||||
deleteDownloadsFolder,
|
||||
navigateToAppEditor,
|
||||
viewAppCardOptions,
|
||||
verifyModal,
|
||||
|
|
@ -14,49 +13,38 @@ import {
|
|||
} from "Support/utils/common";
|
||||
import {
|
||||
modifyAndVerifyAppCardIcon,
|
||||
login,
|
||||
verifyAppDelete,
|
||||
} from "Support/utils/dashboard";
|
||||
import { profileSelector } from "Selectors/profile";
|
||||
import { profileText } from "Texts/profile";
|
||||
import { commonSelectors } from "Selectors/common";
|
||||
import { dashboardSelector } from "Selectors/dashboard";
|
||||
import { commonText } from "Texts/common";
|
||||
import { dashboardText } from "Texts/dashboard";
|
||||
import {
|
||||
navigateToManageUsers,
|
||||
logout,
|
||||
searchUser,
|
||||
navigateToManageGroups,
|
||||
} from "Support/utils/common";
|
||||
import { roleBasedOnboarding } from "Support/utils/onboarding";
|
||||
import { logout } from "Support/utils/common";
|
||||
|
||||
describe("dashboard", () => {
|
||||
const data = {};
|
||||
data.appName = `${fake.companyName}-App`;
|
||||
data.folderName = `${fake.companyName.toLowerCase()}-folder`;
|
||||
data.cloneAppName = `cloned-${data.appName}`;
|
||||
data.updatedFolderName = `new-${data.folderName}`;
|
||||
data.firstName = fake.firstName;
|
||||
data.email = fake.email.toLowerCase().replaceAll("[^A-Za-z]", "");
|
||||
data.workspaceName = fake.firstName;
|
||||
data.workspaceSlug = fake.firstName.toLowerCase().replaceAll("[^A-Za-z]", "");
|
||||
let data = {};
|
||||
|
||||
beforeEach(() => {
|
||||
data = {
|
||||
appName: `${fake.companyName}-App`,
|
||||
folderName: `${fake.companyName.toLowerCase()}-folder`,
|
||||
cloneAppName: `cloned-${fake.companyName}-App`,
|
||||
updatedFolderName: `new-${fake.companyName.toLowerCase()}-folder`,
|
||||
workspaceName: fake.firstName,
|
||||
workspaceSlug: fake.firstName.toLowerCase().replaceAll("[^A-Za-z]", ""),
|
||||
};
|
||||
cy.intercept("GET", "/api/library_apps").as("appLibrary");
|
||||
cy.intercept("DELETE", "/api/folders/*").as("folderDeleted");
|
||||
cy.skipWalkthrough();
|
||||
|
||||
cy.apiLogin();
|
||||
cy.apiCreateWorkspace(data.workspaceName, data.workspaceSlug);
|
||||
cy.apiLogout();
|
||||
cy.apiLogin();
|
||||
cy.visit(`${data.workspaceSlug}`);
|
||||
});
|
||||
|
||||
it("should verify the elements on empty dashboard", () => {
|
||||
cy.intercept("GET", "/api/apps?page=1&folder=&searchKey=&type=front-end", {
|
||||
fixture: "intercept/emptyDashboard.json",
|
||||
}).as("emptyDashboard");
|
||||
|
||||
cy.intercept("GET", "/api/folder-apps?searchKey=&type=front-end", {
|
||||
body: { folders: [] },
|
||||
}).as("folders");
|
||||
|
||||
cy.intercept("GET", "/api/metadata", {
|
||||
body: {
|
||||
installed_version: "2.9.2",
|
||||
|
|
@ -64,15 +52,10 @@ describe("dashboard", () => {
|
|||
},
|
||||
}).as("version");
|
||||
|
||||
cy.defaultWorkspaceLogin();
|
||||
cy.wait("@emptyDashboard");
|
||||
cy.wait("@folders");
|
||||
cy.wait("@version");
|
||||
|
||||
cy.get(commonSelectors.homePageLogo).should("be.visible");
|
||||
cy.get(commonSelectors.workspaceName).verifyVisibleElement(
|
||||
"have.text",
|
||||
"My workspace"
|
||||
data.workspaceName
|
||||
);
|
||||
cy.get(commonSelectors.workspaceName).click();
|
||||
// cy.get(commonSelectors.editRectangleIcon).should("be.visible");
|
||||
|
|
@ -193,7 +176,7 @@ describe("dashboard", () => {
|
|||
desktop: { top: 100, left: 20 },
|
||||
mobile: { width: 8, height: 50 },
|
||||
};
|
||||
cy.apiLogin();
|
||||
|
||||
cy.apiCreateApp(data.appName);
|
||||
cy.openApp();
|
||||
cy.apiAddComponentToApp(data.appName, "text1", customLayout);
|
||||
|
|
@ -276,6 +259,7 @@ describe("dashboard", () => {
|
|||
|
||||
cancelModal(commonText.cancelButton);
|
||||
|
||||
cy.wait(3000);
|
||||
viewAppCardOptions(data.appName);
|
||||
cy.get(
|
||||
commonSelectors.appCardOptions(commonText.removeFromFolderOption)
|
||||
|
|
@ -296,6 +280,7 @@ describe("dashboard", () => {
|
|||
|
||||
cy.get(commonSelectors.allApplicationsLink).click();
|
||||
|
||||
cy.wait(3000);
|
||||
viewAppCardOptions(data.appName);
|
||||
cy.get(commonSelectors.appCardOptions(commonText.cloneAppOption)).click();
|
||||
cy.get('[data-cy="clone-app"]').click();
|
||||
|
|
@ -312,7 +297,10 @@ describe("dashboard", () => {
|
|||
|
||||
cy.get(commonSelectors.appCard(data.cloneAppName)).should("be.visible");
|
||||
|
||||
cy.wait(3000)
|
||||
cy.get(commonSelectors.globalDataSourceIcon).click();
|
||||
cy.get(commonSelectors.dashboardIcon).click();
|
||||
cy.wait(3000);
|
||||
cy.reloadAppForTheElement(data.cloneAppName);
|
||||
viewAppCardOptions(data.cloneAppName);
|
||||
cy.get(commonSelectors.appCardOptions(commonText.exportAppOption)).click();
|
||||
cy.get(commonSelectors.exportAllButton).click();
|
||||
|
|
@ -322,6 +310,7 @@ describe("dashboard", () => {
|
|||
expect(downloadedAppExportFileName).to.contain.string("app");
|
||||
});
|
||||
|
||||
cy.wait(3000);
|
||||
cy.reloadAppForTheElement(data.cloneAppName);
|
||||
viewAppCardOptions(data.cloneAppName);
|
||||
cy.get(commonSelectors.deleteAppOption).click();
|
||||
|
|
@ -337,6 +326,7 @@ describe("dashboard", () => {
|
|||
).verifyVisibleElement("have.text", commonText.modalYesButton);
|
||||
cancelModal(commonText.cancelButton);
|
||||
|
||||
cy.wait(3000);
|
||||
cy.reloadAppForTheElement(data.cloneAppName);
|
||||
viewAppCardOptions(data.cloneAppName);
|
||||
cy.get(commonSelectors.deleteAppOption).click();
|
||||
|
|
@ -362,9 +352,6 @@ describe("dashboard", () => {
|
|||
mobile: { width: 8, height: 50 },
|
||||
};
|
||||
|
||||
cy.skipWalkthrough();
|
||||
data.appName = `${fake.companyName}-App`;
|
||||
cy.defaultWorkspaceLogin();
|
||||
cy.createApp(data.appName);
|
||||
cy.apiAddComponentToApp(data.appName, "text1", customLayout);
|
||||
|
||||
|
|
@ -395,12 +382,8 @@ describe("dashboard", () => {
|
|||
mobile: { width: 8, height: 50 },
|
||||
};
|
||||
|
||||
data.appName = `${fake.companyName}-App`;
|
||||
cy.defaultWorkspaceLogin();
|
||||
cy.createApp(data.appName);
|
||||
|
||||
cy.apiAddComponentToApp(data.appName, "text1", customLayout);
|
||||
|
||||
cy.backToApps();
|
||||
|
||||
cy.get(commonSelectors.createNewFolderButton).click();
|
||||
|
|
@ -517,13 +500,4 @@ describe("dashboard", () => {
|
|||
verifyAppDelete(data.appName);
|
||||
logout();
|
||||
});
|
||||
|
||||
it("should verify the elements on empty dashboard for end user", () => {
|
||||
cy.defaultWorkspaceLogin();
|
||||
cy.intercept("GET", "/api/apps?page=1&folder=&searchKey=&type=front-end", {
|
||||
fixture: "intercept/emptyDashboard.json",
|
||||
}).as("emptyDashboard")
|
||||
roleBasedOnboarding(data.firstName, data.email, "end-user");
|
||||
cy.get(commonSelectors.dashboardAppCreateButton).should("be.disabled");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -2127,7 +2127,7 @@
|
|||
"encrypted": false
|
||||
},
|
||||
"host": {
|
||||
"value": "35.202.183.199",
|
||||
"value": "35.238.9.114",
|
||||
"encrypted": false
|
||||
},
|
||||
"port": {
|
||||
|
|
|
|||
|
|
@ -585,7 +585,7 @@
|
|||
"encrypted": false
|
||||
},
|
||||
"host": {
|
||||
"value": "35.202.183.199",
|
||||
"value": "35.238.9.114",
|
||||
"encrypted": false
|
||||
},
|
||||
"port": {
|
||||
|
|
|
|||
|
|
@ -1701,7 +1701,7 @@
|
|||
]
|
||||
},
|
||||
"list_rows": {},
|
||||
"runOnPageLoad": true
|
||||
"runOnPageLoad": false
|
||||
},
|
||||
"dataSourceId": "f4cf0089-aec2-4713-800e-3560e678220b",
|
||||
"appVersionId": "b74fcff1-8cf1-40f8-a13d-c2d2a0b1ebf1",
|
||||
|
|
@ -1862,7 +1862,7 @@
|
|||
"encrypted": false
|
||||
},
|
||||
"host": {
|
||||
"value": "35.202.183.199",
|
||||
"value": "35.238.9.114",
|
||||
"encrypted": false
|
||||
},
|
||||
"port": {
|
||||
|
|
|
|||
|
|
@ -25,7 +25,10 @@ export const verifySuccessfulSlugUpdate = (workspaceId, slug) => {
|
|||
"have.text",
|
||||
"Slug accepted!"
|
||||
);
|
||||
cy.get(commonWidgetSelector.appLinkSucessLabel).verifyVisibleElement(
|
||||
|
||||
cy.wait(500);
|
||||
// cy.get(commonWidgetSelector.appLinkSucessLabel).should('be.visible');
|
||||
cy.get(commonWidgetSelector.appLinkSucessLabel).should(
|
||||
"have.text",
|
||||
"Link updated successfully!"
|
||||
);
|
||||
|
|
|
|||
|
|
@ -32,10 +32,10 @@ export const deleteComponentAndVerify = (widgetName) => {
|
|||
.last()
|
||||
.realClick();
|
||||
});
|
||||
cy.verifyToastMessage(
|
||||
`[class=go3958317564]`,
|
||||
"Component deleted! (Ctrl + Z to undo)"
|
||||
);
|
||||
// cy.verifyToastMessage(
|
||||
// `[class=go3958317564]`,
|
||||
// "Component deleted! (Ctrl + Z to undo)"
|
||||
// );
|
||||
cy.notVisible(commonWidgetSelector.draggableWidget(widgetName));
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -16,9 +16,7 @@ export const navigateToProfile = () => {
|
|||
export const logout = () => {
|
||||
cy.get(commonSelectors.settingsIcon).click();
|
||||
cy.get(commonSelectors.logoutLink).click();
|
||||
cy.intercept("GET", "/api/metadata").as("publicConfig");
|
||||
cy.wait("@publicConfig");
|
||||
cy.wait(500);
|
||||
cy.wait(1000);
|
||||
};
|
||||
|
||||
export const navigateToManageUsers = () => {
|
||||
|
|
@ -183,10 +181,9 @@ export const manageUsersPagination = (email) => {
|
|||
|
||||
export const searchUser = (email) => {
|
||||
cy.clearAndType(commonSelectors.inputUserSearch, email);
|
||||
cy.wait(1000)
|
||||
cy.wait(1000);
|
||||
};
|
||||
|
||||
|
||||
export const selectAppCardOption = (appName, appCardOption) => {
|
||||
viewAppCardOptions(appName);
|
||||
cy.get(appCardOption).should("be.visible").click({ force: true });
|
||||
|
|
@ -221,7 +218,6 @@ export const pinInspector = () => {
|
|||
}
|
||||
});
|
||||
cy.hideTooltip();
|
||||
|
||||
};
|
||||
|
||||
export const navigateToworkspaceConstants = () => {
|
||||
|
|
@ -243,24 +239,3 @@ export const verifyTooltipDisabled = (selector, message) => {
|
|||
cy.get(".tooltip-inner").last().should("have.text", message);
|
||||
});
|
||||
};
|
||||
|
||||
export const deleteAllGroupChips = () => {
|
||||
cy.get('body').then(($body) => {
|
||||
if ($body.find('[data-cy="group-chip"]').length > 0) {
|
||||
cy.get('[data-cy="group-chip"]').then(($groupChip) => {
|
||||
if ($groupChip.is(':visible')) {
|
||||
cy.get('[data-cy="group-chip"]').first().click();
|
||||
cy.get('[data-cy="delete-button"]').click();
|
||||
cy.get('[data-cy="yes-button"]').click();
|
||||
|
||||
cy.wait(2000);
|
||||
deleteAllGroupChips(); // Recursive call to delete next chip
|
||||
} else {
|
||||
cy.log("Group chip is present but not visible, skipping deletion");
|
||||
}
|
||||
});
|
||||
} else {
|
||||
cy.log("No group chips left to delete");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -646,7 +646,7 @@ export const createGroupAddAppAndUserToGroup = (groupName, email) => {
|
|||
|
||||
cy.request({
|
||||
method: "POST",
|
||||
url: `${Cypress.env("server_host")}/api/v2/group_permissions`,
|
||||
url: `${Cypress.env("server_host")}/api/v2/group-permissions`,
|
||||
headers: headers,
|
||||
body: {
|
||||
name: groupName,
|
||||
|
|
@ -658,14 +658,14 @@ export const createGroupAddAppAndUserToGroup = (groupName, email) => {
|
|||
|
||||
cy.request({
|
||||
method: "POST",
|
||||
url: `${Cypress.env("server_host")}/api/v2/group_permissions/granular-permissions`,
|
||||
url: `${Cypress.env("server_host")}/api/v2/group-permissions/${groupId}/granular-permissions`,
|
||||
headers: headers,
|
||||
body: {
|
||||
name: "Apps",
|
||||
type: "app",
|
||||
groupId: groupId,
|
||||
isAll: false,
|
||||
createAppsPermissionsObject: {
|
||||
createResourcePermissionObject: {
|
||||
canEdit: true,
|
||||
canView: false,
|
||||
hideFromDashboard: false,
|
||||
|
|
@ -676,19 +676,22 @@ export const createGroupAddAppAndUserToGroup = (groupName, email) => {
|
|||
],
|
||||
},
|
||||
},
|
||||
|
||||
}).then((response) => {
|
||||
expect(response.status).to.equal(201);
|
||||
});
|
||||
|
||||
cy.wait(2000);
|
||||
cy.task("dbConnection", {
|
||||
dbconfig: Cypress.env("app_db"),
|
||||
sql: `select id from users where email='${email}';`,
|
||||
}).then((resp) => {
|
||||
const userId = resp.rows[0].id;
|
||||
cy.log(userId);
|
||||
|
||||
cy.request({
|
||||
method: "POST",
|
||||
url: `${Cypress.env("server_host")}/api/v2/group_permissions/group-user`,
|
||||
url: `${Cypress.env("server_host")}/api/v2/group-permissions/${groupId}/users`,
|
||||
headers: headers,
|
||||
body: {
|
||||
userIds: [userId],
|
||||
|
|
@ -720,7 +723,7 @@ export const OpenGroupCardOption = (groupName) => {
|
|||
export const duplicateMultipleGroups = (groupNames) => {
|
||||
groupNames.forEach((groupName) => {
|
||||
OpenGroupCardOption(groupName);
|
||||
cy.wait(3000);
|
||||
cy.wait(2000);
|
||||
cy.get(commonSelectors.duplicateOption).click(); // Click on the duplicate option
|
||||
cy.get(commonSelectors.confirmDuplicateButton).click(); // Confirm duplication if needed
|
||||
});
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ export const generalSettings = () => {
|
|||
|
||||
cy.get(ssoSelector.workspaceLoginPage.defaultSSO).click();
|
||||
cy.get(ssoSelector.defaultGoogle).verifyVisibleElement("have.text", "Google");
|
||||
cy.get(ssoSelector.defaultGithub).verifyVisibleElement("have.text", "Github");
|
||||
cy.get(ssoSelector.defaultGithub).verifyVisibleElement("have.text", "Git");
|
||||
|
||||
cy.clearAndType(ssoSelector.allowedDomainInput, ssoText.allowedDomain);
|
||||
cy.get(ssoSelector.saveButton).click();
|
||||
|
|
@ -416,7 +416,7 @@ export const resetDomain = () => {
|
|||
cy.request(
|
||||
{
|
||||
method: "PATCH",
|
||||
url: `${Cypress.env("server_host")}/api/organizations`,
|
||||
url: `${Cypress.env("server_host")}/api/login-configs/organization-general`,
|
||||
headers: {
|
||||
"Tj-Workspace-Id": Cypress.env("workspaceId"),
|
||||
Cookie: `tj_auth_token=${cookie.value}`,
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
3.9.0
|
||||
3.10.0
|
||||
|
|
|
|||
2
frontend/assets/csv/sample_upload_ce.csv
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
First Name,Last Name,Email,User Role,Group
|
||||
test,user,test@gmail.com,"Assign each user a role: Admin, Builder or End User. User role value should be exact same","For multiple groups separate using pipe (|) operator e.g. Groups1|Group2 or leave blank if no group assign"
|
||||
|
|
|
@ -1 +1 @@
|
|||
Subproject commit 96d68bb9801411de58e6ec62c9d0e84bba631fdd
|
||||
Subproject commit b83575a36da2f7e44f5c1bdf35928018bb951014
|
||||
|
|
@ -41,6 +41,8 @@ import {
|
|||
import { shallow } from 'zustand/shallow';
|
||||
import useStore from '@/AppBuilder/_stores/store';
|
||||
import { checkIfToolJetCloud } from '@/_helpers/utils';
|
||||
import { BasicPlanMigrationBanner } from '@/HomePage/BasicPlanMigrationBanner/BasicPlanMigrationBanner';
|
||||
import { licenseService } from '@/_services';
|
||||
|
||||
const AppWrapper = (props) => {
|
||||
const { isAppDarkMode } = useAppDarkMode();
|
||||
|
|
@ -68,12 +70,24 @@ class AppComponent extends React.Component {
|
|||
currentUser: null,
|
||||
fetchedMetadata: false,
|
||||
darkMode: localStorage.getItem('darkMode') === 'true',
|
||||
showBanner: false,
|
||||
// isEditorOrViewer: '',
|
||||
};
|
||||
}
|
||||
updateSidebarNAV = (val) => {
|
||||
this.setState({ sidebarNav: val });
|
||||
};
|
||||
updateMargin() {
|
||||
const isAdmin = authenticationService?.currentSessionValue?.admin;
|
||||
const isBuilder = authenticationService?.currentSessionValue?.is_builder;
|
||||
const setupDate = authenticationService?.currentSessionValue?.consultation_banner_date;
|
||||
const showBannerCondition =
|
||||
(isAdmin || isBuilder) && setupDate && this.isExistingPlanUser(setupDate) && this.state.showBanner;
|
||||
const marginValue = showBannerCondition ? '25' : '0';
|
||||
const marginValueLayout = showBannerCondition ? '35' : '0';
|
||||
document.documentElement.style.setProperty('--dynamic-margin', `${marginValue}px`);
|
||||
document.documentElement.style.setProperty('--dynamic-margin-2', `${marginValueLayout}px`);
|
||||
}
|
||||
|
||||
fetchMetadata = () => {
|
||||
tooljetService.fetchMetaData().then((data) => {
|
||||
|
|
@ -89,11 +103,15 @@ class AppComponent extends React.Component {
|
|||
});
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
async componentDidMount() {
|
||||
setFaviconAndTitle();
|
||||
authorizeWorkspace();
|
||||
this.fetchMetadata();
|
||||
setInterval(this.fetchMetadata, 1000 * 60 * 60 * 1);
|
||||
this.updateMargin(); // Set initial margin
|
||||
const featureAccess = await licenseService.getFeatureAccess();
|
||||
const isBasicPlan = !featureAccess?.licenseStatus?.isLicenseValid || featureAccess?.licenseStatus?.isExpired;
|
||||
this.setState({ showBanner: isBasicPlan });
|
||||
}
|
||||
// check if its getting routed from editor
|
||||
checkPreviousRoute = (route) => {
|
||||
|
|
@ -114,6 +132,8 @@ class AppComponent extends React.Component {
|
|||
// Reload the page for clearing already set intervals
|
||||
window.location.reload();
|
||||
}
|
||||
// Update margin when showBanner changes
|
||||
this.updateMargin();
|
||||
}
|
||||
|
||||
switchDarkMode = (newMode) => {
|
||||
|
|
@ -130,8 +150,14 @@ class AppComponent extends React.Component {
|
|||
}
|
||||
return '';
|
||||
};
|
||||
closeBasicPlanMigrationBanner = () => {
|
||||
this.setState({ showBanner: false });
|
||||
};
|
||||
isExistingPlanUser = (date) => {
|
||||
return new Date(date) < new Date('2025-04-24'); //show banner if user created before 2 april (24 for testing)
|
||||
};
|
||||
render() {
|
||||
const { updateAvailable, darkMode, isEditorOrViewer } = this.state;
|
||||
const { updateAvailable, darkMode, isEditorOrViewer, showBanner } = this.state;
|
||||
const mergedProps = {
|
||||
...this.props,
|
||||
switchDarkMode: this.switchDarkMode,
|
||||
|
|
@ -156,220 +182,236 @@ class AppComponent extends React.Component {
|
|||
}
|
||||
const { sidebarNav } = this.state;
|
||||
const { updateSidebarNAV } = this;
|
||||
const isApplicationsPath = window.location.pathname.includes('/applications/');
|
||||
const isAdmin = authenticationService?.currentSessionValue?.admin;
|
||||
const isBuilder = authenticationService?.currentSessionValue?.is_builder;
|
||||
const setupDate = authenticationService?.currentSessionValue?.consultation_banner_date;
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={cx('main-wrapper', {
|
||||
'theme-dark dark-theme': !this.isEditorOrViewerFromPath() && darkMode,
|
||||
})}
|
||||
data-cy="main-wrapper"
|
||||
>
|
||||
{updateAvailable && (
|
||||
<div className="alert alert-info alert-dismissible" role="alert">
|
||||
<h3 className="mb-1">Update available</h3>
|
||||
<p>A new version of ToolJet has been released.</p>
|
||||
<div className="btn-list">
|
||||
<a
|
||||
href="https://docs.tooljet.io/docs/setup/updating"
|
||||
target="_blank"
|
||||
className="btn btn-info"
|
||||
rel="noreferrer"
|
||||
>
|
||||
Read release notes & update
|
||||
</a>
|
||||
<a
|
||||
onClick={() => {
|
||||
tooljetService.skipVersion();
|
||||
this.setState({ updateAvailable: false });
|
||||
}}
|
||||
className="btn"
|
||||
>
|
||||
Skip this version
|
||||
</a>
|
||||
<div className={!isApplicationsPath && (isAdmin || isBuilder) ? 'banner-layout-wrapper' : ''}>
|
||||
{!isApplicationsPath &&
|
||||
(isAdmin || isBuilder) &&
|
||||
showBanner &&
|
||||
setupDate &&
|
||||
this.isExistingPlanUser(setupDate) && (
|
||||
<BasicPlanMigrationBanner darkMode={darkMode} closeBanner={this.closeBasicPlanMigrationBanner} />
|
||||
)}
|
||||
<div
|
||||
className={cx('main-wrapper', {
|
||||
'theme-dark dark-theme': !this.isEditorOrViewerFromPath() && darkMode,
|
||||
})}
|
||||
data-cy="main-wrapper"
|
||||
>
|
||||
{updateAvailable && (
|
||||
<div className="alert alert-info alert-dismissible" role="alert">
|
||||
<h3 className="mb-1">Update available</h3>
|
||||
<p>A new version of ToolJet has been released.</p>
|
||||
<div className="btn-list">
|
||||
<a
|
||||
href="https://docs.tooljet.io/docs/setup/updating"
|
||||
target="_blank"
|
||||
className="btn btn-info"
|
||||
rel="noreferrer"
|
||||
>
|
||||
Read release notes & update
|
||||
</a>
|
||||
<a
|
||||
onClick={() => {
|
||||
tooljetService.skipVersion();
|
||||
this.setState({ updateAvailable: false });
|
||||
}}
|
||||
className="btn"
|
||||
>
|
||||
Skip this version
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<BreadCrumbContext.Provider value={{ sidebarNav, updateSidebarNAV }}>
|
||||
<Routes>
|
||||
{onboarding(this.props)}
|
||||
{auth(this.props)}
|
||||
<Route path="/sso/:origin/:configId" exact element={<Oauth {...this.props} />} />
|
||||
<Route path="/sso/:origin" exact element={<Oauth {...this.props} />} />
|
||||
<Route
|
||||
exact
|
||||
path="/:workspaceId/apps/:slug/:pageHandle?/*"
|
||||
element={
|
||||
<AppsRoute componentType="editor">
|
||||
<AppLoader switchDarkMode={this.switchDarkMode} darkMode={darkMode} />
|
||||
</AppsRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path="/:workspaceId/workspace-constants"
|
||||
element={
|
||||
<PrivateRoute>
|
||||
<WorkspaceConstants switchDarkMode={this.switchDarkMode} darkMode={darkMode} />
|
||||
</PrivateRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path="/applications/:slug/:pageHandle?"
|
||||
element={
|
||||
<AppsRoute componentType="viewer">
|
||||
<Viewer switchDarkMode={this.switchDarkMode} darkMode={this.props.isAppDarkMode} />
|
||||
</AppsRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path="/applications/:slug/versions/:versionId/environments/:environmentId/:pageHandle?"
|
||||
element={
|
||||
<AppsRoute componentType="viewer">
|
||||
<Viewer switchDarkMode={this.switchDarkMode} darkMode={this.props.isAppDarkMode} />
|
||||
</AppsRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path="/oauth2/authorize"
|
||||
element={
|
||||
<PrivateRoute>
|
||||
<Authorize switchDarkMode={this.switchDarkMode} darkMode={darkMode} />
|
||||
</PrivateRoute>
|
||||
}
|
||||
/>
|
||||
{window.public_config?.ENABLE_WORKFLOWS_FEATURE === 'true' && (
|
||||
)}
|
||||
<BreadCrumbContext.Provider value={{ sidebarNav, updateSidebarNAV }}>
|
||||
<Routes>
|
||||
{onboarding(this.props)}
|
||||
{auth(this.props)}
|
||||
<Route path="/sso/:origin/:configId" exact element={<Oauth {...this.props} />} />
|
||||
<Route path="/sso/:origin" exact element={<Oauth {...this.props} />} />
|
||||
<Route
|
||||
exact
|
||||
path="/:workspaceId/workflows/*"
|
||||
path="/:workspaceId/apps/:slug/:pageHandle?/*"
|
||||
element={
|
||||
<AdminRoute {...this.props}>
|
||||
<Workflows switchDarkMode={this.switchDarkMode} darkMode={this.darkMode} />
|
||||
</AdminRoute>
|
||||
<AppsRoute componentType="editor">
|
||||
<AppLoader switchDarkMode={this.switchDarkMode} darkMode={darkMode} />
|
||||
</AppsRoute>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<Route path="/:workspaceId/workspace-settings/*" element={<WorkspaceSettings {...mergedProps} />}></Route>
|
||||
<Route path="settings/*" element={<InstanceSettings {...this.props} />}></Route>
|
||||
<Route path="/:workspaceId/settings/*" element={<Settings {...this.props} />}></Route>
|
||||
|
||||
{getAuditLogsRoutes(this.props)}
|
||||
<Route
|
||||
exact
|
||||
path="/:workspaceId/profile-settings"
|
||||
element={
|
||||
<PrivateRoute>
|
||||
<SettingsPage switchDarkMode={this.switchDarkMode} darkMode={darkMode} />
|
||||
</PrivateRoute>
|
||||
}
|
||||
/>
|
||||
{getDataSourcesRoutes(mergedProps)}
|
||||
<Route
|
||||
exact
|
||||
path="/applications/:id/versions/:versionId/:pageHandle?"
|
||||
element={
|
||||
<PrivateRoute>
|
||||
<Viewer switchDarkMode={this.switchDarkMode} darkMode={darkMode} />
|
||||
</PrivateRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path="/applications/:slug/:pageHandle?"
|
||||
element={
|
||||
<PrivateRoute>
|
||||
<Viewer switchDarkMode={this.switchDarkMode} darkMode={darkMode} />
|
||||
</PrivateRoute>
|
||||
}
|
||||
/>
|
||||
|
||||
<Route
|
||||
exact
|
||||
path="/:workspaceId/database"
|
||||
element={
|
||||
<PrivateRoute>
|
||||
<TooljetDatabase switchDarkMode={this.switchDarkMode} darkMode={darkMode} />
|
||||
</PrivateRoute>
|
||||
}
|
||||
/>
|
||||
|
||||
{this.state.tooljetVersion && !checkIfToolJetCloud(this.state.tooljetVersion) && (
|
||||
<Route
|
||||
exact
|
||||
path="/integrations"
|
||||
path="/:workspaceId/workspace-constants"
|
||||
element={
|
||||
<AdminRoute {...this.props}>
|
||||
<MarketplacePage switchDarkMode={this.switchDarkMode} darkMode={darkMode} />
|
||||
</AdminRoute>
|
||||
<PrivateRoute>
|
||||
<WorkspaceConstants switchDarkMode={this.switchDarkMode} darkMode={darkMode} />
|
||||
</PrivateRoute>
|
||||
}
|
||||
>
|
||||
<Route path="installed" element={<InstalledPlugins />} />
|
||||
<Route path="marketplace" element={<MarketplacePlugins />} />/
|
||||
</Route>
|
||||
)}
|
||||
|
||||
<Route exact path="/" element={<Navigate to="/:workspaceId" />} />
|
||||
<Route
|
||||
exact
|
||||
path="/error/:errorType"
|
||||
element={<ErrorPage switchDarkMode={this.switchDarkMode} darkMode={darkMode} />}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path="/app-url-archived"
|
||||
element={
|
||||
<SwitchWorkspacePage
|
||||
switchDarkMode={this.switchDarkMode}
|
||||
darkMode={darkMode}
|
||||
archived={true}
|
||||
isAppUrl={true}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path="/applications/:slug/:pageHandle?"
|
||||
element={
|
||||
<AppsRoute componentType="viewer">
|
||||
<Viewer switchDarkMode={this.switchDarkMode} darkMode={this.props.isAppDarkMode} />
|
||||
</AppsRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path="/applications/:slug/versions/:versionId/environments/:environmentId/:pageHandle?"
|
||||
element={
|
||||
<AppsRoute componentType="viewer">
|
||||
<Viewer switchDarkMode={this.switchDarkMode} darkMode={this.props.isAppDarkMode} />
|
||||
</AppsRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path="/oauth2/authorize"
|
||||
element={
|
||||
<PrivateRoute>
|
||||
<Authorize switchDarkMode={this.switchDarkMode} darkMode={darkMode} />
|
||||
</PrivateRoute>
|
||||
}
|
||||
/>
|
||||
{window.public_config?.ENABLE_WORKFLOWS_FEATURE === 'true' && (
|
||||
<Route
|
||||
exact
|
||||
path="/:workspaceId/workflows/*"
|
||||
element={
|
||||
<AdminRoute {...this.props}>
|
||||
<Workflows switchDarkMode={this.switchDarkMode} darkMode={this.darkMode} />
|
||||
</AdminRoute>
|
||||
}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path="/switch-workspace"
|
||||
element={
|
||||
<SwitchWorkspaceRoute>
|
||||
<SwitchWorkspacePage switchDarkMode={this.switchDarkMode} darkMode={darkMode} />
|
||||
</SwitchWorkspaceRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path="/switch-workspace-archived"
|
||||
element={
|
||||
<SwitchWorkspaceRoute>
|
||||
<SwitchWorkspacePage switchDarkMode={this.switchDarkMode} darkMode={darkMode} archived={true} />
|
||||
</SwitchWorkspaceRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path="/:workspaceId"
|
||||
element={
|
||||
<PrivateRoute>
|
||||
<HomePage switchDarkMode={this.switchDarkMode} darkMode={darkMode} appType={'front-end'} />
|
||||
</PrivateRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="*"
|
||||
render={() => {
|
||||
if (authenticationService?.currentSessionValue?.current_organization_id) {
|
||||
return <Navigate to="/:workspaceId" />;
|
||||
}
|
||||
return <Navigate to="/login" />;
|
||||
}}
|
||||
/>
|
||||
</Routes>
|
||||
</BreadCrumbContext.Provider>
|
||||
<div id="modal-div"></div>
|
||||
</div>
|
||||
)}
|
||||
<Route
|
||||
path="/:workspaceId/workspace-settings/*"
|
||||
element={<WorkspaceSettings {...mergedProps} />}
|
||||
></Route>
|
||||
<Route path="settings/*" element={<InstanceSettings {...this.props} />}></Route>
|
||||
<Route path="/:workspaceId/settings/*" element={<Settings {...this.props} />}></Route>
|
||||
|
||||
<Toast toastOptions={toastOptions} />
|
||||
{getAuditLogsRoutes(this.props)}
|
||||
<Route
|
||||
exact
|
||||
path="/:workspaceId/profile-settings"
|
||||
element={
|
||||
<PrivateRoute>
|
||||
<SettingsPage switchDarkMode={this.switchDarkMode} darkMode={darkMode} />
|
||||
</PrivateRoute>
|
||||
}
|
||||
/>
|
||||
{getDataSourcesRoutes(mergedProps)}
|
||||
<Route
|
||||
exact
|
||||
path="/applications/:id/versions/:versionId/:pageHandle?"
|
||||
element={
|
||||
<PrivateRoute>
|
||||
<Viewer switchDarkMode={this.switchDarkMode} darkMode={darkMode} />
|
||||
</PrivateRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path="/applications/:slug/:pageHandle?"
|
||||
element={
|
||||
<PrivateRoute>
|
||||
<Viewer switchDarkMode={this.switchDarkMode} darkMode={darkMode} />
|
||||
</PrivateRoute>
|
||||
}
|
||||
/>
|
||||
|
||||
<Route
|
||||
exact
|
||||
path="/:workspaceId/database"
|
||||
element={
|
||||
<PrivateRoute>
|
||||
<TooljetDatabase switchDarkMode={this.switchDarkMode} darkMode={darkMode} />
|
||||
</PrivateRoute>
|
||||
}
|
||||
/>
|
||||
|
||||
{this.state.tooljetVersion && !checkIfToolJetCloud(this.state.tooljetVersion) && (
|
||||
<Route
|
||||
exact
|
||||
path="/integrations"
|
||||
element={
|
||||
<AdminRoute {...this.props}>
|
||||
<MarketplacePage switchDarkMode={this.switchDarkMode} darkMode={darkMode} />
|
||||
</AdminRoute>
|
||||
}
|
||||
>
|
||||
<Route path="installed" element={<InstalledPlugins />} />
|
||||
<Route path="marketplace" element={<MarketplacePlugins />} />/
|
||||
</Route>
|
||||
)}
|
||||
|
||||
<Route exact path="/" element={<Navigate to="/:workspaceId" />} />
|
||||
<Route
|
||||
exact
|
||||
path="/error/:errorType"
|
||||
element={<ErrorPage switchDarkMode={this.switchDarkMode} darkMode={darkMode} />}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path="/app-url-archived"
|
||||
element={
|
||||
<SwitchWorkspacePage
|
||||
switchDarkMode={this.switchDarkMode}
|
||||
darkMode={darkMode}
|
||||
archived={true}
|
||||
isAppUrl={true}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path="/switch-workspace"
|
||||
element={
|
||||
<SwitchWorkspaceRoute>
|
||||
<SwitchWorkspacePage switchDarkMode={this.switchDarkMode} darkMode={darkMode} />
|
||||
</SwitchWorkspaceRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path="/switch-workspace-archived"
|
||||
element={
|
||||
<SwitchWorkspaceRoute>
|
||||
<SwitchWorkspacePage switchDarkMode={this.switchDarkMode} darkMode={darkMode} archived={true} />
|
||||
</SwitchWorkspaceRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path="/:workspaceId"
|
||||
element={
|
||||
<PrivateRoute>
|
||||
<HomePage switchDarkMode={this.switchDarkMode} darkMode={darkMode} appType={'front-end'} />
|
||||
</PrivateRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="*"
|
||||
render={() => {
|
||||
if (authenticationService?.currentSessionValue?.current_organization_id) {
|
||||
return <Navigate to="/:workspaceId" />;
|
||||
}
|
||||
return <Navigate to="/login" />;
|
||||
}}
|
||||
/>
|
||||
</Routes>
|
||||
</BreadCrumbContext.Provider>
|
||||
<div id="modal-div"></div>
|
||||
</div>
|
||||
|
||||
<Toast toastOptions={toastOptions} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ const CreateVersionModal = ({
|
|||
canCommit,
|
||||
orgGit,
|
||||
fetchingOrgGit,
|
||||
handleCommitOnVersionCreation = () => {},
|
||||
handleCommitOnVersionCreation = () => { },
|
||||
}) => {
|
||||
const [isCreatingVersion, setIsCreatingVersion] = useState(false);
|
||||
const [versionName, setVersionName] = useState('');
|
||||
|
|
@ -94,12 +94,15 @@ const CreateVersionModal = ({
|
|||
handleCommitOnVersionCreation(data);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log({ error });
|
||||
toast.error(error);
|
||||
});
|
||||
},
|
||||
(error) => {
|
||||
toast.error(error?.error);
|
||||
if (error?.data?.code === "23505") {
|
||||
toast.error("Version name already exists.");
|
||||
} else {
|
||||
toast.error(error?.error);
|
||||
}
|
||||
setIsCreatingVersion(false);
|
||||
}
|
||||
);
|
||||
|
|
|
|||
|
|
@ -150,11 +150,7 @@ export const CustomSelect = ({ currentEnvironment, onSelectVersion, ...props })
|
|||
{/* When we merge this code to EE update the defaultAppEnvironments object with rest of default environments (then delete this comment)*/}
|
||||
<ConfirmDialog
|
||||
show={deleteVersion.showModal}
|
||||
message={`${
|
||||
defaultAppEnvironments.length > 1
|
||||
? 'Deleting a version will permanently remove it from all environments.'
|
||||
: ''
|
||||
}Are you sure you want to delete this version - ${decodeEntities(deleteVersion.versionName)}?`}
|
||||
message={`Are you sure you want to delete this version - ${decodeEntities(deleteVersion.versionName)}?`}
|
||||
onConfirm={() => deleteAppVersion(deleteVersion.versionId, deleteVersion.versionName)}
|
||||
onCancel={resetDeleteModal}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -1,63 +0,0 @@
|
|||
import React, { useState } from 'react';
|
||||
import { ButtonSolid } from '@/_ui/AppButton/AppButton';
|
||||
import { shallow } from 'zustand/shallow';
|
||||
import { ToolTip } from '@/_components/ToolTip';
|
||||
import PromoteConfirmationModal from './PromoteConfirmationModal';
|
||||
import useStore from '@/AppBuilder/_stores/store';
|
||||
|
||||
const PromoteVersionButton = () => {
|
||||
const [promoteModalData, setPromoteModalData] = useState(null);
|
||||
const { isSaving, editingVersion, appVersionEnvironment, environments, selectedEnvironment } = useStore(
|
||||
(state) => ({
|
||||
isSaving: state.app.isSaving,
|
||||
editingVersion: state.currentVersionId,
|
||||
selectedEnvironment: state.selectedEnvironment,
|
||||
environments: state.environments,
|
||||
appVersionEnvironment: state.appVersionEnvironment,
|
||||
}),
|
||||
shallow
|
||||
);
|
||||
|
||||
const shouldDisablePromote = isSaving || selectedEnvironment?.priority < appVersionEnvironment?.priority;
|
||||
|
||||
const handlePromote = () => {
|
||||
const curentEnvIndex = environments.findIndex((env) => env.id === appVersionEnvironment.id);
|
||||
setPromoteModalData({
|
||||
current: appVersionEnvironment,
|
||||
target: environments[curentEnvIndex + 1],
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<ButtonSolid
|
||||
variant="primary"
|
||||
onClick={handlePromote}
|
||||
size="md"
|
||||
disabled={shouldDisablePromote}
|
||||
data-cy="promote-button"
|
||||
>
|
||||
<ToolTip message="Promote this version to the next environment" placement="bottom" show={!shouldDisablePromote}>
|
||||
<div style={{ fontSize: '14px' }}>Promote </div>
|
||||
</ToolTip>
|
||||
<svg width="8" height="8" viewBox="0 0 8 8" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M0.276332 7.02113C0.103827 7.23676 0.138788 7.55141 0.354419 7.72391C0.57005 7.89642 0.884696 7.86146 1.0572 7.64583L3.72387 4.31249C3.86996 4.12988 3.86996 3.87041 3.72387 3.6878L1.0572 0.354464C0.884696 0.138833 0.57005 0.103872 0.354419 0.276377C0.138788 0.448881 0.103827 0.763528 0.276332 0.979158L2.69312 4.00014L0.276332 7.02113ZM4.27633 7.02113C4.10383 7.23676 4.13879 7.55141 4.35442 7.72391C4.57005 7.89642 4.8847 7.86146 5.0572 7.64583L7.72387 4.31249C7.86996 4.12988 7.86996 3.87041 7.72387 3.6878L5.0572 0.354463C4.8847 0.138832 4.57005 0.103871 4.35442 0.276377C4.13879 0.448881 4.10383 0.763527 4.27633 0.979158L6.69312 4.00014L4.27633 7.02113Z"
|
||||
fill={shouldDisablePromote ? '#C1C8CD' : '#FDFDFE'}
|
||||
/>
|
||||
</svg>
|
||||
</ButtonSolid>
|
||||
|
||||
<PromoteConfirmationModal
|
||||
data={promoteModalData}
|
||||
editingVersion={editingVersion}
|
||||
onClose={() => setPromoteModalData(null)}
|
||||
fetchEnvironments={() => {}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default PromoteVersionButton;
|
||||
|
|
@ -36,16 +36,23 @@ function DataSourcePicker({ darkMode }) {
|
|||
(gds) => gds.type === DATA_SOURCE_TYPE.STATIC
|
||||
);
|
||||
//StaicDataSources DIDNT HAVE ID
|
||||
const updatedStaticDataSources = staticDataSources.map((source) => {
|
||||
// Find a matching object from staticDataSourcesFullObject based on the 'kind' field
|
||||
const matchingObject = staticDataSourcesFullObject?.find((gds) => gds.kind === source.kind);
|
||||
const updatedStaticDataSources = staticDataSources
|
||||
.filter((source) => {
|
||||
if (source.kind === 'workflows') {
|
||||
return staticDataSourcesFullObject?.some((gds) => gds.kind === 'workflows');
|
||||
}
|
||||
return true;
|
||||
})
|
||||
.map((source) => {
|
||||
// Find a matching object from staticDataSourcesFullObject based on the 'kind' field
|
||||
const matchingObject = staticDataSourcesFullObject?.find((gds) => gds.kind === source.kind);
|
||||
|
||||
// Replace the 'id' with the one from the matching object, or keep the existing one if no match
|
||||
return {
|
||||
...source,
|
||||
id: matchingObject?.id || source.id,
|
||||
};
|
||||
});
|
||||
// Replace the 'id' with the one from the matching object, or keep the existing one if no match
|
||||
return {
|
||||
...source,
|
||||
id: matchingObject?.id || source.id,
|
||||
};
|
||||
});
|
||||
|
||||
const docLink = 'sampledb.com';
|
||||
|
||||
|
|
|
|||
|
|
@ -274,7 +274,7 @@ export const BaseQueryManagerBody = ({ darkMode, activeTab, renderCopilot = () =
|
|||
const renderChangeDataSource = () => {
|
||||
const selectableDataSources = [...dataSources, ...globalDataSources, !!sampleDataSource && sampleDataSource]
|
||||
.filter(Boolean)
|
||||
.filter((ds) => ds.kind === selectedQuery?.kind);
|
||||
.filter((ds) => ds.kind === selectedQuery?.kind && ds.type !== DATA_SOURCE_TYPE.STATIC);
|
||||
if (isEmpty(selectableDataSources)) {
|
||||
return '';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ const TooljetBanner = ({ isDarkMode }) => {
|
|||
<div
|
||||
className="powered-with-tj"
|
||||
onClick={() => {
|
||||
const url = `https://tooljet.com/?utm_source=powered_by_banner&utm_medium=${instanceId}&utm_campaign=self_hosted`;
|
||||
const url = `https://tooljet.com`;
|
||||
window.open(url, '_blank');
|
||||
}}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -90,9 +90,9 @@ export const Modal = function Modal({
|
|||
const onHideSideEffects = () => {
|
||||
const canvasElement = document.querySelector('.page-container.canvas-container');
|
||||
const realCanvasEl = document.getElementsByClassName('real-canvas')[0];
|
||||
const allModalContainers = realCanvasEl.querySelectorAll('.modal');
|
||||
const modalContainer = allModalContainers[allModalContainers.length - 1];
|
||||
const hasManyModalsOpen = allModalContainers.length > 1;
|
||||
const allModalContainers = realCanvasEl?.querySelectorAll('.modal');
|
||||
const modalContainer = allModalContainers?.[allModalContainers.length - 1];
|
||||
const hasManyModalsOpen = allModalContainers?.length > 1;
|
||||
|
||||
if (canvasElement && realCanvasEl && modalContainer) {
|
||||
modalContainer.style.height = ``;
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ const TooljetBanner = ({ isDarkMode }) => {
|
|||
<div
|
||||
className="powered-with-tj"
|
||||
onClick={() => {
|
||||
const url = `https://tooljet.com/?utm_source=powered_by_banner&utm_medium=${instanceId}&utm_campaign=self_hosted`;
|
||||
const url = `https://tooljet.com`;
|
||||
window.open(url, '_blank');
|
||||
}}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,34 @@
|
|||
import React, { useState } from 'react';
|
||||
import './BasicPlanMigrationBanner.scss';
|
||||
import CloseIcon from '@/_ui/Icon/bulkIcons/CloseIcon';
|
||||
|
||||
export const BasicPlanMigrationBanner = ({ closeBanner, darkMode }) => {
|
||||
return (
|
||||
<div className={`${darkMode ? 'theme-dark dark-theme' : ''} basic-plan-migration-banner`}>
|
||||
<div style={{ marginLeft: 'auto' }}>
|
||||
<p className="banner-text">
|
||||
We've updated your plan limits to align with our{' '}
|
||||
<a href="https://www.tooljet.ai/pricing" className="banner-link" target="_blank" rel="noopener noreferrer">
|
||||
new pricing.
|
||||
</a>{' '}
|
||||
For help in retrieving data or any inquiries,{' '}
|
||||
<a
|
||||
href="https://docs.tooljet.ai/docs/tj-setup/licensing/self-hosted/"
|
||||
className="banner-link"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
read docs
|
||||
</a>{' '}
|
||||
or{' '}
|
||||
<a href="mailto:hello@tooljet.com" className="banner-link" target="_blank" rel="noopener noreferrer">
|
||||
contact us
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
<div onClick={closeBanner} type="button">
|
||||
<CloseIcon width="12" fill="#3E63DD" opacity="1" secondaryFill="#ffffff" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
.basic-plan-migration-banner {
|
||||
background-color: var(--background-accent-weak);
|
||||
padding: 12px 16px;
|
||||
width: 100%;
|
||||
height: 30px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
.banner-text {
|
||||
color: var(--text-accent, #4368E3);
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.banner-text{
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.banner-link {
|
||||
color: var(--primary-brand);
|
||||
text-decoration: underline;
|
||||
font-weight: 500;
|
||||
|
||||
&:hover {
|
||||
color: var(--indigo-100);
|
||||
}
|
||||
}
|
||||
|
||||
div[type="button"] {
|
||||
margin-left: auto;
|
||||
}
|
||||
}
|
||||
|
||||
// banner css changes
|
||||
.banner-layout-wrapper {
|
||||
height: 100vh !important;
|
||||
overflow: hidden;
|
||||
/* prevents scrolling beyond this height */
|
||||
position: relative;
|
||||
background-color: var(--background-accent-weak);
|
||||
/* content background */
|
||||
}
|
||||
|
|
@ -21,6 +21,7 @@ export const BlankPage = function BlankPage({
|
|||
viewTemplateLibraryModal,
|
||||
appType,
|
||||
canCreateApp,
|
||||
workflowsLimit,
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const whiteLabelText = retrieveWhiteLabelText();
|
||||
|
|
@ -43,6 +44,8 @@ export const BlankPage = function BlankPage({
|
|||
}
|
||||
|
||||
const appCreationDisabled = !canCreateApp() || (!appsLimit?.canAddUnlimited && appsLimit?.percentage >= 100);
|
||||
const workflowsCreationDisabled =
|
||||
!canCreateApp() || (!workflowsLimit?.canAddUnlimited && workflowsLimit?.percentage >= 100);
|
||||
|
||||
const templateOptionsView = (
|
||||
<>
|
||||
|
|
@ -133,12 +136,12 @@ export const BlankPage = function BlankPage({
|
|||
<div className="row mt-4">
|
||||
<div className="col-6">
|
||||
<ButtonSolid
|
||||
disabled={appCreationDisabled}
|
||||
leftIcon="plus"
|
||||
onClick={openCreateAppModal}
|
||||
isLoading={creatingApp}
|
||||
data-cy="button-new-app-from-scratch"
|
||||
className="col"
|
||||
disabled={appType !== 'workflow' ? appCreationDisabled : workflowsCreationDisabled}
|
||||
fill={'#FDFDFE'}
|
||||
>
|
||||
Create new {appType !== 'workflow' ? 'application' : 'workflow'}
|
||||
|
|
|
|||
|
|
@ -511,7 +511,12 @@ class HomePageComponent extends React.Component {
|
|||
this.state.currentFolder.id
|
||||
);
|
||||
this.fetchFolders();
|
||||
this.fetchAppsLimit();
|
||||
if (this.props.appType === 'workflow') {
|
||||
this.fetchWorkflowsInstanceLimit();
|
||||
this.fetchWorkflowsWorkspaceLimit();
|
||||
} else {
|
||||
this.fetchAppsLimit();
|
||||
}
|
||||
})
|
||||
.catch(({ error }) => {
|
||||
toast.error('Could not delete the app.');
|
||||
|
|
@ -522,6 +527,10 @@ class HomePageComponent extends React.Component {
|
|||
});
|
||||
};
|
||||
|
||||
isExistingPlanUser = (date) => {
|
||||
return new Date(date) < new Date('2025-04-01');
|
||||
};
|
||||
|
||||
pageCount = () => {
|
||||
return this.state.currentFolder.id ? this.state.meta.folder_count : this.state.meta.total_count;
|
||||
};
|
||||
|
|
@ -928,6 +937,8 @@ class HomePageComponent extends React.Component {
|
|||
dependentPlugins: dependentPlugins,
|
||||
},
|
||||
};
|
||||
const isAdmin = authenticationService?.currentSessionValue?.admin;
|
||||
const isBuilder = authenticationService?.currentSessionValue?.is_builder;
|
||||
return (
|
||||
<Layout switchDarkMode={this.props.switchDarkMode} darkMode={this.props.darkMode}>
|
||||
<div className="wrapper home-page">
|
||||
|
|
@ -1231,31 +1242,6 @@ class HomePageComponent extends React.Component {
|
|||
</Dropdown>
|
||||
</div>
|
||||
</LicenseTooltip>
|
||||
{this.props.appType === 'front-end' && (
|
||||
<LicenseBanner classes="mb-3 small" limits={appsLimit} type="apps" size="small" />
|
||||
)}
|
||||
{this.props.appType === 'workflow' &&
|
||||
(workflowInstanceLevelLimit.current >= workflowInstanceLevelLimit.total ||
|
||||
100 > workflowInstanceLevelLimit.percentage >= 90 ||
|
||||
workflowInstanceLevelLimit.current === workflowInstanceLevelLimit.total - 1 ||
|
||||
workflowWorkspaceLevelLimit.current >= workflowWorkspaceLevelLimit.total ||
|
||||
100 > workflowWorkspaceLevelLimit.percentage >= 90 ||
|
||||
workflowWorkspaceLevelLimit.current === workflowWorkspaceLevelLimit.total - 1) && (
|
||||
<>
|
||||
<LicenseBanner
|
||||
classes="mb-3 small"
|
||||
limits={
|
||||
workflowInstanceLevelLimit.current >= workflowInstanceLevelLimit.total ||
|
||||
100 > workflowInstanceLevelLimit.percentage >= 90 ||
|
||||
workflowInstanceLevelLimit.current === workflowInstanceLevelLimit.total - 1
|
||||
? workflowInstanceLevelLimit
|
||||
: workflowWorkspaceLevelLimit
|
||||
}
|
||||
type="workflow"
|
||||
size="small"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<Folders
|
||||
|
|
@ -1271,6 +1257,31 @@ class HomePageComponent extends React.Component {
|
|||
canCreateApp={this.canCreateApp()}
|
||||
appType={this.props.appType}
|
||||
/>
|
||||
{this.props.appType === 'front-end' && (
|
||||
<LicenseBanner classes="mb-3 small" limits={appsLimit} type="apps" size="small" />
|
||||
)}
|
||||
{this.props.appType === 'workflow' &&
|
||||
(workflowInstanceLevelLimit.current >= workflowInstanceLevelLimit.total ||
|
||||
100 > workflowInstanceLevelLimit.percentage >= 90 ||
|
||||
workflowInstanceLevelLimit.current === workflowInstanceLevelLimit.total - 1 ||
|
||||
workflowWorkspaceLevelLimit.current >= workflowWorkspaceLevelLimit.total ||
|
||||
100 > workflowWorkspaceLevelLimit.percentage >= 90 ||
|
||||
workflowWorkspaceLevelLimit.current === workflowWorkspaceLevelLimit.total - 1) && (
|
||||
<>
|
||||
<LicenseBanner
|
||||
classes="mb-3 small"
|
||||
limits={
|
||||
workflowInstanceLevelLimit.current >= workflowInstanceLevelLimit.total ||
|
||||
100 > workflowInstanceLevelLimit.percentage >= 90 ||
|
||||
workflowInstanceLevelLimit.current === workflowInstanceLevelLimit.total - 1
|
||||
? workflowInstanceLevelLimit
|
||||
: workflowWorkspaceLevelLimit
|
||||
}
|
||||
type="workflow"
|
||||
size="small"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{authenticationService.currentSessionValue?.super_admin &&
|
||||
this.isWithinSevenDaysOfSignUp(authenticationService.currentSessionValue?.consultation_banner_date) && (
|
||||
<ConsultationBanner
|
||||
|
|
@ -1284,7 +1295,7 @@ class HomePageComponent extends React.Component {
|
|||
/>
|
||||
)}
|
||||
|
||||
<OrganizationList />
|
||||
<OrganizationList customStyle={{ marginBottom: isAdmin || isBuilder ? '' : '0px' }} />
|
||||
</div>
|
||||
|
||||
<div
|
||||
|
|
@ -1359,6 +1370,13 @@ class HomePageComponent extends React.Component {
|
|||
viewTemplateLibraryModal={this.showTemplateLibraryModal}
|
||||
hideTemplateLibraryModal={this.hideTemplateLibraryModal}
|
||||
appType={this.props.appType}
|
||||
workflowsLimit={
|
||||
workflowInstanceLevelLimit.current >= workflowInstanceLevelLimit.total ||
|
||||
100 > workflowInstanceLevelLimit.percentage >= 90 ||
|
||||
workflowInstanceLevelLimit.current === workflowInstanceLevelLimit.total - 1
|
||||
? workflowInstanceLevelLimit
|
||||
: workflowWorkspaceLevelLimit
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{!isLoading && apps?.length === 0 && appSearchKey && (
|
||||
|
|
|
|||
|
|
@ -88,10 +88,11 @@ function SettingsPage(props) {
|
|||
|
||||
const handlePasswordInput = (input) => {
|
||||
setNewPassword(input);
|
||||
if (input.length > 100) {
|
||||
const trimmedInput = input.trim();
|
||||
if (trimmedInput.length > 100) {
|
||||
setHelperText('Password should be Max 100 characters');
|
||||
setValidPassword(false);
|
||||
} else if (input.length < 5 && input.length > 0) {
|
||||
} else if (trimmedInput.length < 5 && trimmedInput.length > 0) {
|
||||
setHelperText('Password should be at least 5 characters');
|
||||
setValidPassword(false);
|
||||
} else {
|
||||
|
|
@ -100,11 +101,19 @@ function SettingsPage(props) {
|
|||
}
|
||||
};
|
||||
|
||||
const handleConfirmPasswordInput = (input) => {
|
||||
setConfirmPassword(input);
|
||||
};
|
||||
|
||||
const changePassword = async () => {
|
||||
const trimmedCurrentPassword = currentpassword.trim();
|
||||
const trimmedNewPassword = newPassword.trim();
|
||||
const trimmedConfirmPassword = confirmPassword.trim();
|
||||
|
||||
const errorMsg =
|
||||
(currentpassword.match(/^ *$/) !== null && 'Current password') ||
|
||||
(newPassword.match(/^ *$/) !== null && 'New password') ||
|
||||
(confirmPassword.match(/^ *$/) !== null && 'Confirm new password');
|
||||
(trimmedCurrentPassword.length === 0 && 'Current password') ||
|
||||
(trimmedNewPassword.length === 0 && 'New password') ||
|
||||
(trimmedConfirmPassword.length === 0 && 'Confirm new password');
|
||||
|
||||
if (errorMsg) {
|
||||
toast.error(errorMsg + " can't be empty!", {
|
||||
|
|
@ -112,13 +121,13 @@ function SettingsPage(props) {
|
|||
});
|
||||
return;
|
||||
}
|
||||
if (currentpassword === newPassword) {
|
||||
if (trimmedCurrentPassword === trimmedNewPassword) {
|
||||
toast.error("New password can't be the same as the current one!", {
|
||||
duration: 3000,
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (newPassword !== confirmPassword) {
|
||||
if (trimmedNewPassword !== trimmedConfirmPassword) {
|
||||
toast.error('New password and confirm new password should be same', {
|
||||
duration: 3000,
|
||||
});
|
||||
|
|
@ -127,7 +136,7 @@ function SettingsPage(props) {
|
|||
|
||||
setPasswordChangeInProgress(true);
|
||||
try {
|
||||
await userService.changePassword(currentpassword, newPassword);
|
||||
await userService.changePassword(trimmedCurrentPassword, trimmedNewPassword);
|
||||
toast.success('Password updated successfully', {
|
||||
duration: 3000,
|
||||
});
|
||||
|
|
@ -293,7 +302,7 @@ function SettingsPage(props) {
|
|||
placeholder={t('header.profileSettingPage.confirmNewPassword', 'Confirm new password')}
|
||||
value={confirmPassword}
|
||||
ref={focusRef}
|
||||
onChange={(event) => setConfirmPassword(event.target.value)}
|
||||
onChange={(event) => handleConfirmPasswordInput(event.target.value)}
|
||||
onKeyPress={confirmPasswordKeyPressHandler}
|
||||
data-cy="confirm-password-input"
|
||||
/>
|
||||
|
|
@ -301,7 +310,7 @@ function SettingsPage(props) {
|
|||
</div>
|
||||
<ButtonSolid
|
||||
isLoading={passwordChangeInProgress}
|
||||
disabled={newPassword.length < 5 || confirmPassword.length < 5 || !validPassword}
|
||||
disabled={newPassword.trim().length < 5 || confirmPassword.trim().length < 5 || !validPassword}
|
||||
onClick={changePassword}
|
||||
data-cy="change-password-button"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -67,7 +67,7 @@ const LegalReasonsErrorModal = ({
|
|||
<Button className="upgrade-btn" autoFocus onClick={() => {}}>
|
||||
<a
|
||||
style={{ color: 'white', textDecoration: 'none' }}
|
||||
href={`https://www.tooljet.com/pricing?utm_source=banner&utm_medium=plg&utm_campaign=none&payment=onpremise&instance_id=${currentUser?.instance_id}`}
|
||||
href={`https://www.tooljet.com/pricing`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
data-cy="upgrade-button"
|
||||
|
|
|
|||
|
|
@ -192,8 +192,21 @@ export const returnWorkspaceIdIfNeed = (path) => {
|
|||
};
|
||||
export const getRedirectURL = (path) => {
|
||||
let redirectLoc = '/';
|
||||
const instanceLevelRoutes = [
|
||||
'/all-users',
|
||||
'/all-workspaces',
|
||||
'/manage-instance-settings',
|
||||
'/white-labelling',
|
||||
'/instance-login',
|
||||
'/smtp',
|
||||
'/license',
|
||||
];
|
||||
if (path) {
|
||||
redirectLoc = `${returnWorkspaceIdIfNeed(path)}${path !== '/' ? path : ''}`;
|
||||
if (instanceLevelRoutes.includes(path)) {
|
||||
redirectLoc = `/settings${path}`;
|
||||
} else {
|
||||
redirectLoc = `${returnWorkspaceIdIfNeed(path)}${path !== '/' ? path : ''}`;
|
||||
}
|
||||
} else {
|
||||
const redirectTo = getRedirectTo();
|
||||
const { from } = redirectTo ? { from: { pathname: redirectTo } } : { from: { pathname: '/' } };
|
||||
|
|
|
|||
|
|
@ -2,16 +2,21 @@ import HttpClient from '@/_helpers/http-client';
|
|||
|
||||
const adapter = new HttpClient();
|
||||
|
||||
//Uncomment when Comment Notifications Module is ready
|
||||
|
||||
function findAll(isRead = false) {
|
||||
return adapter.get(`/comment_notifications?isRead=${isRead}`);
|
||||
return { data: [] };
|
||||
// return adapter.get(`/comment_notifications?isRead=${isRead}`);
|
||||
}
|
||||
|
||||
function updateAll(isRead) {
|
||||
return adapter.patch(`/comment_notifications`, { isRead });
|
||||
return;
|
||||
// return adapter.patch(`/comment_notifications`, { isRead });
|
||||
}
|
||||
|
||||
function update(id, isRead) {
|
||||
return adapter.patch(`/comment_notifications/${id}`, { isRead });
|
||||
return;
|
||||
// return adapter.patch(`/comment_notifications/${id}`, { isRead });
|
||||
}
|
||||
|
||||
export const commentNotificationsService = {
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ export const organizationService = {
|
|||
|
||||
function getUsersByValue(searchInput) {
|
||||
const requestOptions = { method: 'GET', headers: authHeader(), credentials: 'include' };
|
||||
return fetch(`${config.apiUrl}/organizations/users/suggest?input=${searchInput}`, requestOptions).then(
|
||||
return fetch(`${config.apiUrl}/organization-users/users/suggest?input=${searchInput}`, requestOptions).then(
|
||||
handleResponse
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -49,6 +49,7 @@
|
|||
align-items: center;
|
||||
justify-content: center;
|
||||
border-top: 1px solid var(--slate5);
|
||||
margin-bottom: var(--dynamic-margin, 0px); //please Remove after Basicplan banner is removed..
|
||||
}
|
||||
|
||||
.tj-org-select {
|
||||
|
|
|
|||
|
|
@ -771,6 +771,7 @@
|
|||
padding-bottom: 8px;
|
||||
width: 44px;
|
||||
max-height: 230px;
|
||||
margin-bottom: var(--dynamic-margin-2, 0px); //please Remove after Basicplan banner is removed..
|
||||
}
|
||||
|
||||
.tj-leftsidebar-icon-items {
|
||||
|
|
|
|||
|
|
@ -11604,10 +11604,6 @@ tbody {
|
|||
cursor: pointer;
|
||||
}
|
||||
|
||||
.user-csv-template-wrap {
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
|
||||
.manage-users-drawer-content-bulk {
|
||||
margin: 24px 15px;
|
||||
|
|
|
|||
20
frontend/src/_ui/Icon/solidIcons/WorkflowV3.jsx
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
import React from 'react';
|
||||
|
||||
const WorkflowV3 = (props) => (
|
||||
<svg width="25" height="25" viewBox="0 0 25 25" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
|
||||
<path
|
||||
d="M12.5196 3.20508L17.0196 5.70508V10.7051L12.5196 13.2051L8.01958 10.7051V5.70508L12.5196 3.20508Z"
|
||||
fill="#CCD1D5"
|
||||
/>
|
||||
<path
|
||||
d="M7.01958 12.2051L11.5195 14.7051V19.7051L7.01958 22.2051L2.51953 19.7051V14.7051L7.01958 12.2051Z"
|
||||
fill="#CCD1D5"
|
||||
/>
|
||||
<path
|
||||
d="M22.5195 14.7051L18.0196 12.2051L13.5195 14.7051V19.7051L18.0196 22.2051L22.5195 19.7051V14.7051Z"
|
||||
fill="#CCD1D5"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export default WorkflowV3;
|
||||
21
frontend/src/_ui/Icon/solidIcons/WorkspaceV3.jsx
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import React from 'react';
|
||||
|
||||
const WorkspaceV3 = (props) => (
|
||||
<svg width="25" height="25" viewBox="0 0 25 25" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
|
||||
<g clipPath="url(#clip0_3060_11830)">
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M7.32657 2.22682C7.88918 1.66422 8.65224 1.34814 9.44789 1.34814H14.5908C15.3864 1.34814 16.1495 1.66422 16.7121 2.22682C17.2746 2.78943 17.5908 3.55249 17.5908 4.34814V5.63386H20.5908C22.0109 5.63386 23.1622 6.78512 23.1622 8.20529V9.87509L12.9641 14.3367C12.3618 14.6003 11.6768 14.6003 11.0745 14.3367L0.876465 9.87509V8.20529C0.876465 6.78512 2.02773 5.63386 3.44789 5.63386H6.44789V4.34814C6.44789 3.55249 6.76396 2.78943 7.32657 2.22682ZM0.876465 12.2141V21.0624C0.876465 22.4825 2.02773 23.6339 3.44789 23.6339H20.5908C22.0109 23.6339 23.1622 22.4825 23.1622 21.0624V12.2141L13.823 16.2999C12.6732 16.803 11.3655 16.803 10.2156 16.2999L0.876465 12.2141ZM15.0193 4.34814V5.63386H9.01932V4.34814C9.01932 4.23449 9.06448 4.12548 9.14484 4.04509C9.22522 3.96473 9.33424 3.91957 9.44789 3.91957H14.5908C14.7044 3.91957 14.8134 3.96473 14.8938 4.04509C14.9742 4.12548 15.0193 4.23449 15.0193 4.34814Z"
|
||||
fill="#CCD1D5"
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_3060_11830">
|
||||
<rect width="24" height="24" fill="white" transform="translate(0.0195312 0.491211)" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export default WorkspaceV3;
|
||||
|
|
@ -230,6 +230,8 @@ import UserGroupsGrey from './UserGroupsGrey.jsx';
|
|||
import AppLimitSvg from './AppLimitSvg.jsx';
|
||||
import NewTabSmall from './NewTabSmall.jsx';
|
||||
import Code from './Code.jsx';
|
||||
import WorkflowV3 from './WorkflowV3.jsx';
|
||||
import WorkspaceV3 from './WorkspaceV3.jsx';
|
||||
|
||||
const Icon = (props) => {
|
||||
switch (props.name) {
|
||||
|
|
@ -575,6 +577,10 @@ const Icon = (props) => {
|
|||
return <Warning {...props} />;
|
||||
case 'warning-user-notfound':
|
||||
return <WarningUserNotFound {...props} />;
|
||||
case 'workflowv3':
|
||||
return <WorkflowV3 {...props} />;
|
||||
case 'workspacev3':
|
||||
return <WorkspaceV3 {...props} />;
|
||||
case 'workspaceconstants':
|
||||
return <WorkspaceConstants {...props} />;
|
||||
case 'zoomin':
|
||||
|
|
|
|||
|
|
@ -409,7 +409,7 @@ class BaseSSOConfigurationList extends React.Component {
|
|||
!this.isInstanceOptionEnabled(sso.sso) ||
|
||||
(sso.sso === 'openid' && !featureAccess?.openid)
|
||||
} // Disable the item if defaultSSO is false
|
||||
data-cy={`dropdwon-options-${sso.sso}`}
|
||||
data-cy={`dropdown-options-${sso.sso}`}
|
||||
>
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||
{this.getSSOIcon(sso.sso)}
|
||||
|
|
|
|||
|
|
@ -192,7 +192,9 @@ const ConstantForm = ({
|
|||
{error['name']}
|
||||
</span>
|
||||
{!error['name'] && (
|
||||
<small style={{ color: 'var(--text-placeholder)' }}>Name must be unique and max 50 characters</small>
|
||||
<small style={{ color: 'var(--text-placeholder)' }} data-cy="name-info">
|
||||
Name must be unique and max 50 characters
|
||||
</small>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ const EmptyState = ({ canCreateVariable, setIsManageVarDrawerOpen, isLoading, se
|
|||
</p>
|
||||
{canCreateVariable && searchTerm === '' && (
|
||||
<ButtonSolid
|
||||
data-cy="add-new-constant-button"
|
||||
data-cy="table-add-new-constant-button"
|
||||
vaiant="primary"
|
||||
onClick={() => setIsManageVarDrawerOpen(true)}
|
||||
className="add-new-constant-button"
|
||||
|
|
|
|||
|
|
@ -804,7 +804,7 @@ class BaseManageGroupPermissions extends React.Component {
|
|||
classes="group-banner"
|
||||
size="xsmall"
|
||||
type={featureAccess?.licenseStatus?.licenseType}
|
||||
customMessage={'Custom groups & permissions are available in our paid plans.'}
|
||||
customMessage={'Custom groups & permissions are paid features'}
|
||||
showCustomGroupBanner={true}
|
||||
/>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import { EDIT_ROLE_MESSAGE } from '@/modules/common/constants';
|
|||
import ModalBase from '@/_ui/Modal';
|
||||
import { UserMetadata } from './components';
|
||||
import LicenseBanner from '@/modules/common/components/LicenseBanner';
|
||||
import { fetchEdition } from '@/modules/common/helpers/utils';
|
||||
|
||||
function InviteUsersForm({
|
||||
onClose,
|
||||
|
|
@ -33,7 +34,6 @@ function InviteUsersForm({
|
|||
}) {
|
||||
const { t } = useTranslation();
|
||||
const [activeTab, setActiveTab] = useState(1);
|
||||
const [userLimits, setUserLimits] = useState({});
|
||||
const [existingGroups, setExistingGroups] = useState([]);
|
||||
const [newRole, setNewRole] = useState(null);
|
||||
const customGroups = groups.filter((group) => group.groupType === 'custom' && group?.disabled !== true);
|
||||
|
|
@ -59,6 +59,7 @@ function InviteUsersForm({
|
|||
},
|
||||
];
|
||||
const [selectedGroups, setSelectedGroups] = useState([]);
|
||||
const [limitReachedType, setLimitReachedType] = useState({});
|
||||
useEffect(() => {
|
||||
setFileUpload(false);
|
||||
}, [activeTab]);
|
||||
|
|
@ -68,6 +69,7 @@ function InviteUsersForm({
|
|||
|
||||
const { super_admin } = authenticationService.currentSessionValue;
|
||||
const [featureAccess, setFeatureAccess] = useState({});
|
||||
const edition = fetchEdition();
|
||||
|
||||
useEffect(() => {
|
||||
fetchFeatureAccess();
|
||||
|
|
@ -81,8 +83,18 @@ function InviteUsersForm({
|
|||
};
|
||||
|
||||
const fetchUserLimits = () => {
|
||||
userService.getUserLimits('total').then((data) => {
|
||||
setUserLimits(data);
|
||||
userService.getUserLimits('all').then((data) => {
|
||||
const licenseBannerObject = {
|
||||
builderPercentage: data?.editorsCount?.percentage,
|
||||
endUserPercentage: data?.viewersCount?.percentage,
|
||||
builderTotal: data?.editorsCount?.total,
|
||||
endUserTotal: data?.viewersCount?.total,
|
||||
canAddUnlimitedBuilder: data?.editorsCount?.canAddUnlimited,
|
||||
canAddUnlimitedEndUser: data?.viewersCount?.canAddUnlimited,
|
||||
currentBuilder: data?.editorsCount?.current,
|
||||
currentEndUser: data?.viewersCount?.current,
|
||||
};
|
||||
setLimitReachedType(licenseBannerObject);
|
||||
});
|
||||
};
|
||||
|
||||
|
|
@ -325,8 +337,11 @@ function InviteUsersForm({
|
|||
</div>
|
||||
{activeTab == 1 ? (
|
||||
<div className="manage-users-drawer-content">
|
||||
<LicenseBanner classes="mb-3" limits={userLimits} type="users" size="small" />
|
||||
<div className={`invite-user-by-email ${isEditing && 'enable-edit-fields'}`}>
|
||||
<LicenseBanner classes="mb-3" userLimits={limitReachedType} size="small" type={'user-limits'} />
|
||||
<div
|
||||
className={`invite-user-by-email ${isEditing && 'enable-edit-fields'}`}
|
||||
style={{ flexDirection: 'row' }}
|
||||
>
|
||||
<form
|
||||
onSubmit={isEditing ? handleEditUser : handleCreateUser}
|
||||
noValidate
|
||||
|
|
@ -410,7 +425,7 @@ function InviteUsersForm({
|
|||
</div>
|
||||
) : (
|
||||
<div className="manage-users-drawer-content-bulk">
|
||||
<LicenseBanner limits={userLimits} type="users" size="small" />
|
||||
<LicenseBanner classes="mb-3" userLimits={limitReachedType} size="small" type={'user-limits'} />
|
||||
<div className="manage-users-drawer-content-bulk-download-prompt">
|
||||
<div className="user-csv-template-wrap">
|
||||
<div>
|
||||
|
|
@ -424,7 +439,7 @@ function InviteUsersForm({
|
|||
<ButtonSolid
|
||||
href={`${window.public_config?.TOOLJET_HOST}${
|
||||
window.public_config?.SUB_PATH ? window.public_config?.SUB_PATH : '/'
|
||||
}assets/csv/sample_upload.csv`}
|
||||
}assets/csv/${edition === 'ce' ? 'sample_upload_ce.csv' : 'sample_upload.csv'}`}
|
||||
download="sample_upload.csv"
|
||||
variant="tertiary"
|
||||
className="download-template-btn"
|
||||
|
|
@ -471,6 +486,7 @@ function InviteUsersForm({
|
|||
width="20"
|
||||
fill={'#FDFDFE'}
|
||||
isLoading={uploadingUsers || creatingUser}
|
||||
iconCustomClass="icon-color"
|
||||
>
|
||||
{!isEditing
|
||||
? activeTab == 1
|
||||
|
|
|
|||
|
|
@ -17,7 +17,9 @@ const ForgotPasswordInfoScreen = ({ email }) => {
|
|||
<div className="forgot-password-info-wrapper info-screen">
|
||||
<OnboardingUIWrapper>
|
||||
<FormHeader>Password has been reset</FormHeader>
|
||||
<p className="message">{message}</p>
|
||||
<p className="message" data-cy="reset-password-page-description">
|
||||
{message}
|
||||
</p>
|
||||
<div className="action-buttons pt-3">
|
||||
<button
|
||||
onClick={() =>
|
||||
|
|
|
|||
|
|
@ -44,22 +44,16 @@ const BaseLogoNavDropdown = ({ darkMode, showWorkflows = false, type = 'apps' })
|
|||
<span>Back to {isWorkflows ? 'workflows' : 'apps'}</span>
|
||||
</Link>
|
||||
<div className="divider"></div>
|
||||
{isWorkflows || !showWorkflows ? (
|
||||
<Link target="_blank" to={getPrivateRoute('dashboard')} className="dropdown-item tj-text tj-text-xsm">
|
||||
<SolidIcon name={'apps'} width="20" fill="#C1C8CD" />
|
||||
<span>{'Apps'}</span>
|
||||
</Link>
|
||||
) : (
|
||||
workflowsEnabled &&
|
||||
showWorkflows &&
|
||||
admin && (
|
||||
<Link target="_blank" to={getPrivateRoute('workflows')} className="dropdown-item tj-text tj-text-xsm">
|
||||
<SolidIcon name={'workflows'} width="20" fill="#C1C8CD" />
|
||||
<span>{'Workflows'}</span>
|
||||
</Link>
|
||||
)
|
||||
)}
|
||||
|
||||
{isWorkflows || !showWorkflows
|
||||
? null
|
||||
: workflowsEnabled &&
|
||||
showWorkflows &&
|
||||
admin && (
|
||||
<Link target="_blank" to={getPrivateRoute('workflows')} className="dropdown-item tj-text tj-text-xsm">
|
||||
<SolidIcon name={'workflows'} width="20" fill="#C1C8CD" />
|
||||
<span>{'Workflows'}</span>
|
||||
</Link>
|
||||
)}
|
||||
{(admin || isBuilder) && (
|
||||
<Link
|
||||
target="_blank"
|
||||
|
|
|
|||
|
|
@ -348,10 +348,10 @@ const BaseManageOrgConstants = ({
|
|||
toast.success('Constant updated successfully');
|
||||
onCancelBtnClicked();
|
||||
})
|
||||
.catch(({ error }) => {
|
||||
.catch(({ error, data }) => {
|
||||
setErrors(error);
|
||||
toast.error(error);
|
||||
if (error === NoPermissionMessage) {
|
||||
toast.error(data?.statusCode === 403 ? 'You do not have permissions to perform this action' : data?.message);
|
||||
if (error === NoPermissionMessage || data?.statusCode === 403) {
|
||||
redirectToWorkspace();
|
||||
}
|
||||
})
|
||||
|
|
@ -364,10 +364,10 @@ const BaseManageOrgConstants = ({
|
|||
toast.success(`${variable.type} constant created successfully!`);
|
||||
onCancelBtnClicked();
|
||||
})
|
||||
.catch(({ error }) => {
|
||||
.catch(({ error, data }) => {
|
||||
setErrors(error);
|
||||
toast.error(error || 'Constant could not be created');
|
||||
if (error === NoPermissionMessage) {
|
||||
toast.error(data?.statusCode === 403 ? 'You do not have permissions to perform this action' : data?.message);
|
||||
if (error === NoPermissionMessage || data?.statusCode === 403) {
|
||||
redirectToWorkspace();
|
||||
}
|
||||
})
|
||||
|
|
@ -390,9 +390,10 @@ const BaseManageOrgConstants = ({
|
|||
setSelectedConstant(null);
|
||||
setMode(MODES.NULL);
|
||||
})
|
||||
.catch(({ error }) => {
|
||||
toast.error(error);
|
||||
if (error === NoPermissionMessage) {
|
||||
.catch(({ error, data }) => {
|
||||
setErrors(error);
|
||||
toast.error(data?.statusCode === 403 ? 'You do not have permissions to perform this action' : data?.message);
|
||||
if (error === NoPermissionMessage || data?.statusCode === 403) {
|
||||
redirectToWorkspace();
|
||||
}
|
||||
})
|
||||
|
|
@ -472,8 +473,10 @@ const BaseManageOrgConstants = ({
|
|||
featureAceess={featureAccess}
|
||||
licenseType={featureAccess?.licenseStatus?.licenseType}
|
||||
/>
|
||||
<div style={{ marginTop: '850px' }}>
|
||||
<OrganizationList />
|
||||
</div>
|
||||
</div>
|
||||
<OrganizationList />
|
||||
</div>
|
||||
<div className="page-wrapper mt-4">
|
||||
<div className="container-xl" style={{ width: '880px' }}>
|
||||
|
|
|
|||
|
|
@ -7,24 +7,25 @@ import useStore from '@/AppBuilder/_stores/store';
|
|||
|
||||
const PromoteVersionButton = () => {
|
||||
const [promoteModalData, setPromoteModalData] = useState(null);
|
||||
const { isSaving, editingVersion, appVersionEnvironment, environments, selectedEnvironment } = useStore(
|
||||
const { isSaving, editingVersion, appVersionEnvironment, environments, selectedEnvironment, currentEnvIndex } = useStore(
|
||||
(state) => ({
|
||||
isSaving: state.app.isSaving,
|
||||
editingVersion: state.currentVersionId,
|
||||
selectedEnvironment: state.selectedEnvironment,
|
||||
environments: state.environments,
|
||||
appVersionEnvironment: state.appVersionEnvironment,
|
||||
currentEnvIndex: state.environments?.findIndex((env) => env?.id === state.appVersionEnvironment?.id),
|
||||
}),
|
||||
shallow
|
||||
);
|
||||
|
||||
const shouldDisablePromote = isSaving || selectedEnvironment?.priority < appVersionEnvironment?.priority;
|
||||
// enable only after the environment details are loaded
|
||||
const shouldDisablePromote = isSaving || selectedEnvironment?.priority < appVersionEnvironment?.priority || !appVersionEnvironment || !environments?.[currentEnvIndex + 1];
|
||||
|
||||
const handlePromote = () => {
|
||||
const curentEnvIndex = environments.findIndex((env) => env.id === appVersionEnvironment.id);
|
||||
setPromoteModalData({
|
||||
current: appVersionEnvironment,
|
||||
target: environments[curentEnvIndex + 1],
|
||||
target: environments[currentEnvIndex + 1],
|
||||
});
|
||||
};
|
||||
|
||||
|
|
@ -54,7 +55,7 @@ const PromoteVersionButton = () => {
|
|||
data={promoteModalData}
|
||||
editingVersion={editingVersion}
|
||||
onClose={() => setPromoteModalData(null)}
|
||||
fetchEnvironments={() => {}}
|
||||
fetchEnvironments={() => { }}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -126,7 +126,7 @@ const PromoteConfirmationModal = React.memo(({ data, onClose }) => {
|
|||
FROM
|
||||
</div>
|
||||
<div className="env-name" data-cy="current-env-name">
|
||||
{capitalize(data?.current.name)}
|
||||
{capitalize(data?.current?.name)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="arrow-container">
|
||||
|
|
@ -144,11 +144,11 @@ const PromoteConfirmationModal = React.memo(({ data, onClose }) => {
|
|||
TO
|
||||
</div>
|
||||
<div className="env-name" data-cy="target-env-name">
|
||||
{capitalize(data?.target.name)}
|
||||
{capitalize(data?.target?.name)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{data?.current.name === 'development' && (
|
||||
{data?.current?.name === 'development' && (
|
||||
<div className="env-change-info" data-cy="env-change-info-text">
|
||||
You won't be able to edit this version after promotion. Are you sure you want to continue?
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -11,6 +11,8 @@ const BaseWorkspaceActions = ({
|
|||
}) => {
|
||||
//If License ToolTip component is not passed from version specific component--> We will show normal ToolTip component
|
||||
const isDefaultLicenseTooltip = LicenseTooltip === DefaultLicenseTooltip;
|
||||
const isAllowPersonalWorkspace = window.public_config?.ALLOW_PERSONAL_WORKSPACE === 'true';
|
||||
|
||||
return (
|
||||
<div
|
||||
className="d-flex"
|
||||
|
|
@ -24,21 +26,23 @@ const BaseWorkspaceActions = ({
|
|||
</div>
|
||||
</ToolTip>
|
||||
) : (
|
||||
<LicenseTooltip
|
||||
limits={workspacesLimit}
|
||||
feature={'workspaces'}
|
||||
placement="top"
|
||||
customTitle="Add new workspace"
|
||||
isAvailable={true}
|
||||
>
|
||||
<div
|
||||
disabled={!workspacesLimit.canAddUnlimited && workspacesLimit?.percentage >= 100}
|
||||
onClick={handleAddWorkspace}
|
||||
style={{ marginLeft: super_admin ? '0px' : '10px' }}
|
||||
>
|
||||
<SolidIcon name="plus" fill="var(--icon-strong)" dataCy="add-new-workspace-link" width="17" />
|
||||
</div>
|
||||
</LicenseTooltip>
|
||||
(isAllowPersonalWorkspace || super_admin) && (
|
||||
<LicenseTooltip
|
||||
limits={workspacesLimit}
|
||||
feature={'workspaces'}
|
||||
placement="top"
|
||||
customTitle="Add new workspace"
|
||||
isAvailable={true}
|
||||
>
|
||||
<div
|
||||
disabled={!workspacesLimit.canAddUnlimited && workspacesLimit?.percentage >= 100}
|
||||
onClick={handleAddWorkspace}
|
||||
style={{ marginLeft: super_admin ? '0px' : '10px' }}
|
||||
>
|
||||
<SolidIcon name="plus" fill="var(--icon-strong)" dataCy="add-new-workspace-link" width="17" />
|
||||
</div>
|
||||
</LicenseTooltip>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -37,20 +37,26 @@ const UsersTable = ({
|
|||
const [selectedUser, setSelectedUser] = useState(null);
|
||||
const [showNoActiveWorkspaceModal, setShowNoActiveWorkspaceModal] = useState(false);
|
||||
const hideAccountSetupLink = window.public_config?.HIDE_ACCOUNT_SETUP_LINK == 'true';
|
||||
|
||||
// Check if user has metadata
|
||||
const shouldShowMetadataColumn = wsSettings && Array.isArray(users) && users.some((user) => user.user_metadata);
|
||||
|
||||
function showMetadataIcon(metadata) {
|
||||
if (!metadata) return false;
|
||||
for (const [key, value] of Object.entries(metadata)) {
|
||||
// Check if both key and value are not empty
|
||||
if (key.trim() !== '' && value.trim() !== '') {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false; // Return false if no completely filled key-value pair is found
|
||||
return false;
|
||||
}
|
||||
|
||||
const handleResetPasswordClick = (user) => {
|
||||
setSelectedUser(user);
|
||||
setIsResetPasswordModalVisible(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="workspace-settings-table-wrap mb-4">
|
||||
<NoActiveWorkspaceModal
|
||||
|
|
@ -68,7 +74,7 @@ const UsersTable = ({
|
|||
<th data-cy="users-table-name-column-header" data-name="name-header">
|
||||
{translator('header.organization.menus.manageUsers.name', 'Name')}
|
||||
</th>
|
||||
{wsSettings && (
|
||||
{shouldShowMetadataColumn && (
|
||||
<th data-cy="users-table-metadata-column-header" data-name="meta-header">
|
||||
Metadata
|
||||
</th>
|
||||
|
|
@ -145,7 +151,7 @@ const UsersTable = ({
|
|||
</span>
|
||||
</div>
|
||||
</td>
|
||||
{wsSettings && (
|
||||
{shouldShowMetadataColumn && (
|
||||
<td data-name="meta-header">
|
||||
<span className="text-muted user-type">
|
||||
<div className={`metadata ${showMetadataIcon(user?.user_metadata) ? '' : 'empty'}`}>
|
||||
|
|
|
|||
|
|
@ -73,6 +73,7 @@ const UsersFilter = ({ filterList, resetSearch }) => {
|
|||
useMenuPortal={true}
|
||||
closeMenuOnSelect={true}
|
||||
width="161.25px"
|
||||
placeholder="All"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -7,6 +7,12 @@
|
|||
|
||||
.onboarding-form-width {
|
||||
width: 308px;
|
||||
|
||||
.submit-button {
|
||||
margin: 0 auto;
|
||||
border: none;
|
||||
width: 308px;
|
||||
}
|
||||
}
|
||||
|
||||
.free-space {
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ const OnboardingForm = ({
|
|||
<div className={iconClasses} onClick={handleBackClick} data-cy="back-button">
|
||||
<LeftArow />
|
||||
</div>
|
||||
<span>Step {currentStep}</span> of {totalSteps}
|
||||
<span>Step {currentStep == 0 ? currentStep + 1 : currentStep}</span> of {totalSteps}
|
||||
</div>
|
||||
<FormHeader>{title}</FormHeader>
|
||||
{description && <FormDescription>{description}</FormDescription>}
|
||||
|
|
|
|||
|
|
@ -34,6 +34,12 @@
|
|||
opacity: 0.5;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.submit-button {
|
||||
margin: 0 auto;
|
||||
border: none;
|
||||
width: 308px;
|
||||
}
|
||||
}
|
||||
|
||||
.__ce {
|
||||
|
|
|
|||
|
|
@ -83,7 +83,9 @@ const WorkspaceNameForm = () => {
|
|||
setFormData({ workspaceName: defaultWorkspaceName });
|
||||
setIsFormValid(true);
|
||||
};
|
||||
handleDefaultWorkspaceName();
|
||||
if (!formData.workspaceName || formData.workspaceName === '') {
|
||||
handleDefaultWorkspaceName();
|
||||
}
|
||||
}, [adminDetails.email, inviteeEmail]);
|
||||
|
||||
const isWorkspaceNameUnique = async (value) => {
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
3.9.0
|
||||
3.10.0
|
||||
|
|
|
|||
|
|
@ -0,0 +1,180 @@
|
|||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
import { AppModule } from '@modules/app/module';
|
||||
import { NestFactory } from '@nestjs/core';
|
||||
import { LicenseCountsService } from '@ee/licensing/services/count.service';
|
||||
import { USER_STATUS, USER_TYPE, WORKSPACE_USER_STATUS } from '@modules/users/constants/lifecycle';
|
||||
import { USER_ROLE } from '@modules/group-permissions/constants';
|
||||
import { LicenseInitService } from '@modules/licensing/interfaces/IService';
|
||||
import { getTooljetEdition } from '@helpers/utils.helper';
|
||||
import { TOOLJET_EDITIONS } from '@modules/app/constants';
|
||||
|
||||
export class EnforceNewBasicPlanLimits1742369617678 implements MigrationInterface {
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
const edition: TOOLJET_EDITIONS = getTooljetEdition() as TOOLJET_EDITIONS;
|
||||
if (edition !== TOOLJET_EDITIONS.EE) {
|
||||
console.log('Skipping migration for edition other than EE');
|
||||
return;
|
||||
}
|
||||
const manager = queryRunner.manager;
|
||||
const nestApp = await NestFactory.createApplicationContext(await AppModule.register({ IS_GET_CONTEXT: true }));
|
||||
const licenseInitService = nestApp.get(LicenseInitService);
|
||||
|
||||
const { isValid } = await licenseInitService.initForMigration(manager);
|
||||
if (!isValid) {
|
||||
const licenseCountsService = nestApp.get(LicenseCountsService);
|
||||
|
||||
const statusList = [WORKSPACE_USER_STATUS.INVITED, WORKSPACE_USER_STATUS.ACTIVE];
|
||||
const statusListStr = statusList.map((status) => `'${status}'`).join(',');
|
||||
|
||||
// Get users with edit permission using native query - FIXED table name from group_permissions to permission_groups
|
||||
const usersWithEditPermissionQuery = `
|
||||
SELECT DISTINCT users.id
|
||||
FROM users
|
||||
INNER JOIN organization_users ON users.id = organization_users.user_id AND organization_users.status IN (${statusListStr})
|
||||
INNER JOIN group_users ON users.id = group_users.user_id
|
||||
INNER JOIN permission_groups ON group_users.group_id = permission_groups.id AND organization_users.organization_id = permission_groups.organization_id
|
||||
WHERE users.status != '${USER_STATUS.ARCHIVED}'
|
||||
AND (permission_groups.name = '${USER_ROLE.ADMIN}' OR permission_groups.name = '${USER_ROLE.BUILDER}')
|
||||
`;
|
||||
|
||||
const usersWithEditPermissionResult = await manager.query(usersWithEditPermissionQuery);
|
||||
const usersWithEditPermission = usersWithEditPermissionResult.map((record) => record.id);
|
||||
|
||||
// More than 2 Editors
|
||||
if (usersWithEditPermission?.length > 2) {
|
||||
// Get admin users directly with native query (excluding instance users) - FIXED table name
|
||||
const adminsQuery = `
|
||||
SELECT DISTINCT users.id
|
||||
FROM users
|
||||
INNER JOIN group_users ON users.id = group_users.user_id
|
||||
INNER JOIN permission_groups ON group_users.group_id = permission_groups.id
|
||||
WHERE users.id IN (${usersWithEditPermission.map((id) => `'${id}'`).join(',')})
|
||||
AND users.user_type != '${USER_TYPE.INSTANCE}'
|
||||
AND permission_groups.name = '${USER_ROLE.ADMIN}'
|
||||
`;
|
||||
|
||||
const adminsResult = await manager.query(adminsQuery);
|
||||
const admins = adminsResult.map((record) => record.id);
|
||||
|
||||
// Get builder users directly with native query (excluding instance users) - FIXED table name
|
||||
const buildersQuery = `
|
||||
SELECT DISTINCT users.id
|
||||
FROM users
|
||||
INNER JOIN group_users ON users.id = group_users.user_id
|
||||
INNER JOIN permission_groups ON group_users.group_id = permission_groups.id
|
||||
WHERE users.id IN (${usersWithEditPermission.map((id) => `'${id}'`).join(',')})
|
||||
AND users.user_type != '${USER_TYPE.INSTANCE}'
|
||||
AND permission_groups.name = '${USER_ROLE.BUILDER}'
|
||||
`;
|
||||
|
||||
const buildersResult = await manager.query(buildersQuery);
|
||||
const builders = buildersResult.map((record) => record.id);
|
||||
|
||||
// If more than 2 admins, archive rest of the admins and all other builders
|
||||
if (admins?.length > 1) {
|
||||
const adminIdsToArchive = admins.slice(1);
|
||||
|
||||
// Archive admins at workspace level
|
||||
if (adminIdsToArchive.length > 0) {
|
||||
const archiveAdminsWorkspaceQuery = `
|
||||
UPDATE organization_users
|
||||
SET status = '${WORKSPACE_USER_STATUS.ARCHIVED}', invitation_token = NULL
|
||||
WHERE user_id IN (${adminIdsToArchive.map((id) => `'${id}'`).join(',')})
|
||||
`;
|
||||
await manager.query(archiveAdminsWorkspaceQuery);
|
||||
|
||||
// Archive admins at instance level
|
||||
const archiveAdminsInstanceQuery = `
|
||||
UPDATE users
|
||||
SET status = '${USER_STATUS.ARCHIVED}'
|
||||
WHERE id IN (${adminIdsToArchive.map((id) => `'${id}'`).join(',')})
|
||||
`;
|
||||
await manager.query(archiveAdminsInstanceQuery);
|
||||
}
|
||||
|
||||
// Archive all builders
|
||||
if (builders?.length > 0) {
|
||||
const archiveBuildersWorkspaceQuery = `
|
||||
UPDATE organization_users
|
||||
SET status = '${WORKSPACE_USER_STATUS.ARCHIVED}', invitation_token = NULL
|
||||
WHERE user_id IN (${builders.map((id) => `'${id}'`).join(',')})
|
||||
`;
|
||||
await manager.query(archiveBuildersWorkspaceQuery);
|
||||
|
||||
const archiveBuildersInstanceQuery = `
|
||||
UPDATE users
|
||||
SET status = '${USER_STATUS.ARCHIVED}'
|
||||
WHERE id IN (${builders.map((id) => `'${id}'`).join(',')})
|
||||
`;
|
||||
await manager.query(archiveBuildersInstanceQuery);
|
||||
}
|
||||
}
|
||||
// If 0 admin and more than 1 builder, archive all builders except the first one
|
||||
else if (admins?.length === 0 && builders?.length > 1) {
|
||||
const buildersToArchive = builders.slice(1);
|
||||
|
||||
if (buildersToArchive.length > 0) {
|
||||
const archiveBuildersWorkspaceQuery = `
|
||||
UPDATE organization_users
|
||||
SET status = '${WORKSPACE_USER_STATUS.ARCHIVED}', invitation_token = NULL
|
||||
WHERE user_id IN (${buildersToArchive.map((id) => `'${id}'`).join(',')})
|
||||
`;
|
||||
await manager.query(archiveBuildersWorkspaceQuery);
|
||||
|
||||
const archiveBuildersInstanceQuery = `
|
||||
UPDATE users
|
||||
SET status = '${USER_STATUS.ARCHIVED}'
|
||||
WHERE id IN (${buildersToArchive.map((id) => `'${id}'`).join(',')})
|
||||
`;
|
||||
await manager.query(archiveBuildersInstanceQuery);
|
||||
}
|
||||
}
|
||||
// Only 1 admin and 1 super admin, archive all builders
|
||||
else if (builders?.length > 0) {
|
||||
const archiveBuildersWorkspaceQuery = `
|
||||
UPDATE organization_users
|
||||
SET status = '${WORKSPACE_USER_STATUS.ARCHIVED}', invitation_token = NULL
|
||||
WHERE user_id IN (${builders.map((id) => `'${id}'`).join(',')})
|
||||
`;
|
||||
await manager.query(archiveBuildersWorkspaceQuery);
|
||||
|
||||
const archiveBuildersInstanceQuery = `
|
||||
UPDATE users
|
||||
SET status = '${USER_STATUS.ARCHIVED}'
|
||||
WHERE id IN (${builders.map((id) => `'${id}'`).join(',')})
|
||||
`;
|
||||
await manager.query(archiveBuildersInstanceQuery);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle viewers/end users limit
|
||||
const viewerIds = await licenseCountsService.getUserIdWithEndUserRole(manager);
|
||||
|
||||
// If more than 50 end users, archive the rest after the first 50
|
||||
if (viewerIds?.length > 50) {
|
||||
const viewersToArchive = viewerIds.slice(50);
|
||||
|
||||
if (viewersToArchive.length > 0) {
|
||||
const archiveViewersWorkspaceQuery = `
|
||||
UPDATE organization_users
|
||||
SET status = '${WORKSPACE_USER_STATUS.ARCHIVED}', invitation_token = NULL
|
||||
WHERE user_id IN (${viewersToArchive.map((id) => `'${id}'`).join(',')})
|
||||
`;
|
||||
await manager.query(archiveViewersWorkspaceQuery);
|
||||
|
||||
const archiveViewersInstanceQuery = `
|
||||
UPDATE users
|
||||
SET status = '${USER_STATUS.ARCHIVED}'
|
||||
WHERE id IN (${viewersToArchive.map((id) => `'${id}'`).join(',')})
|
||||
`;
|
||||
await manager.query(archiveViewersInstanceQuery);
|
||||
}
|
||||
}
|
||||
}
|
||||
await nestApp.close();
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
// No down migration implementation
|
||||
}
|
||||
}
|
||||
|
|
@ -1 +1 @@
|
|||
Subproject commit 683647f83d3efeeadbe69c40b8e8dd5ba4e8ea06
|
||||
Subproject commit b1f4bb6a8c5d6e4543452580b7d1cdf03e7c954c
|
||||
|
|
@ -19,6 +19,7 @@ import { User } from './user.entity';
|
|||
import { GroupApps } from './group_apps.entity';
|
||||
import { AppGroupPermission } from './app_group_permission.entity';
|
||||
import { AiConversation } from './ai_conversation.entity';
|
||||
import { Organization } from './organization.entity';
|
||||
|
||||
@Entity({ name: 'apps' })
|
||||
export class App extends BaseEntity {
|
||||
|
|
@ -46,6 +47,10 @@ export class App extends BaseEntity {
|
|||
@Column({ name: 'organization_id' })
|
||||
organizationId: string;
|
||||
|
||||
@ManyToOne(() => Organization)
|
||||
@JoinColumn({ name: 'organization_id' })
|
||||
organization: Organization;
|
||||
|
||||
@Column({ name: 'current_version_id' })
|
||||
currentVersionId: string;
|
||||
|
||||
|
|
|
|||
|
|
@ -24,6 +24,9 @@ export class AppBase extends BaseEntity {
|
|||
@Column({ name: 'name' })
|
||||
name: string;
|
||||
|
||||
@Column({ name: 'type' })
|
||||
type: string = 'front-end';
|
||||
|
||||
@Column({ name: 'slug', unique: true })
|
||||
slug: string;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,17 +0,0 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { EventsGateway } from './events.gateway';
|
||||
import { YjsGateway } from './yjs.gateway';
|
||||
import { SessionModule } from '@modules/session/module';
|
||||
|
||||
const providers = [];
|
||||
|
||||
providers.unshift(YjsGateway);
|
||||
if (process.env.COMMENT_FEATURE_ENABLE !== 'false') {
|
||||
providers.unshift(EventsGateway);
|
||||
}
|
||||
|
||||
@Module({
|
||||
imports: [SessionModule],
|
||||
providers,
|
||||
})
|
||||
export class EventsModule {}
|
||||
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 6.5 KiB After Width: | Height: | Size: 6.5 KiB |
BIN
server/src/mails/assets/rocket.png
Normal file
|
After Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 9.1 KiB After Width: | Height: | Size: 9.1 KiB |
|
Before Width: | Height: | Size: 4.9 KiB After Width: | Height: | Size: 4.9 KiB |
|
|
@ -18,4 +18,6 @@
|
|||
<a target="_blank" href="https://twitter.com/ToolJet">
|
||||
<img height="20" width="auto" class="social-icons social-icon-fit" alt="Company" src="cid:twitter" />
|
||||
</div>
|
||||
<br />
|
||||
<br />
|
||||
</div>
|
||||
|
|
@ -41,6 +41,15 @@
|
|||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="padding-y-10">
|
||||
<p style="font-size: 14px; color: #555;">
|
||||
If the button isn't accessible, copy and paste this invite link into your browser:
|
||||
<br />
|
||||
<span style="color: #555; word-break: break-all;">{{inviteUrl}}</span>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="padding-y-20 footer-note">
|
||||
<br />
|
||||
|
|
@ -40,6 +40,15 @@
|
|||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="padding-y-10">
|
||||
<p style="font-size: 14px; color: #555;">
|
||||
If the button isn't accessible, copy and paste this invite link into your browser:
|
||||
<br />
|
||||
<span style="color: #555; word-break: break-all;">{{inviteUrl}}</span>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="padding-y-20 footer-note">
|
||||
{{#if (eq whiteLabelText "ToolJet")}}
|
||||
|
|
@ -37,6 +37,15 @@
|
|||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="padding-y-10">
|
||||
<p style="font-size: 14px; color: #555;">
|
||||
If the button isn't accessible, copy and paste this invite link into your browser:
|
||||
<br />
|
||||
<span style="color: #555; word-break: break-all;">{{inviteUrl}}</span>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="padding-y-20 footer-note">
|
||||
{{#if (eq whiteLabelText "ToolJet")}}
|
||||
|
|
@ -10,12 +10,12 @@
|
|||
<tr>
|
||||
<td class="padding-y-20">
|
||||
<p>
|
||||
{{#if sender}}
|
||||
{{sender}} has invited you to join the workspace -
|
||||
{{else}}
|
||||
You have been invited to join the workspace -
|
||||
{{/if}}
|
||||
<span class="text-bold">{{organizationName}}</span>. Click the button below to get started!
|
||||
{{#if sender}}
|
||||
{{sender}} has invited you to join the workspace -
|
||||
{{else}}
|
||||
You have been invited to join the workspace -
|
||||
{{/if}}
|
||||
<span class="text-bold">{{organizationName}}</span>. Click the button below to get started!
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
|
|
@ -28,6 +28,15 @@
|
|||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="padding-y-10">
|
||||
<p style="font-size: 14px; color: #555;">
|
||||
If the button isn't accessible, copy and paste this invite link into your browser:
|
||||
<br />
|
||||
<span style="color: #555; word-break: break-all;">{{inviteUrl}}</span>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="padding-y-20 footer-note">
|
||||
<br />
|
||||
|
|
@ -28,6 +28,15 @@
|
|||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="padding-y-10">
|
||||
<p style="font-size: 14px; color: #555;">
|
||||
If the button isn't accessible, copy and paste this invite link into your browser:
|
||||
<br />
|
||||
<span style="color: #555; word-break: break-all;">{{inviteUrl}}</span>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="padding-y-20 footer-note">
|
||||
{{#if (eq whiteLabelText "ToolJet")}}
|
||||
|
|
@ -25,6 +25,15 @@
|
|||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="padding-y-10">
|
||||
<p style="font-size: 14px; color: #555;">
|
||||
If the button isn't accessible, copy and paste this invite link into your browser:
|
||||
<br />
|
||||
<span style="color: #555; word-break: break-all;">{{inviteUrl}}</span>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="padding-y-20 footer-note">
|
||||
{{#if (eq whiteLabelText "ToolJet")}}
|
||||
|
|
@ -28,6 +28,9 @@ import { ILicenseUtilService } from '@modules/licensing/interfaces/IUtilService'
|
|||
import { ITemporalService } from '@modules/workflows/interfaces/ITemporalService';
|
||||
import { getTooljetEdition } from '@helpers/utils.helper';
|
||||
import { validateEdition } from '@helpers/edition.helper';
|
||||
import { ResponseInterceptor } from '@modules/app/interceptors/response.interceptor';
|
||||
import { Reflector } from '@nestjs/core';
|
||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
|
||||
let appContext: INestApplicationContext = undefined;
|
||||
|
||||
|
|
@ -105,7 +108,7 @@ function setSecurityHeaders(app, configService) {
|
|||
'cdn.jsdelivr.net',
|
||||
'https://esm.sh',
|
||||
'www.googletagmanager.com',
|
||||
],
|
||||
].concat(configService.get('CSP_WHITELISTED_DOMAINS')?.split(',') || []),
|
||||
'object-src': ["'self'", 'data:'],
|
||||
'media-src': ["'self'", 'data:'],
|
||||
'default-src': [
|
||||
|
|
@ -117,7 +120,7 @@ function setSecurityHeaders(app, configService) {
|
|||
"'self'",
|
||||
'blob:',
|
||||
'www.googletagmanager.com',
|
||||
],
|
||||
].concat(configService.get('CSP_WHITELISTED_DOMAINS')?.split(',') || []),
|
||||
'connect-src': ['ws://' + domain, "'self'", '*', 'data:'],
|
||||
'frame-ancestors': ['*'],
|
||||
'frame-src': ['*'],
|
||||
|
|
@ -182,6 +185,7 @@ async function bootstrap() {
|
|||
});
|
||||
|
||||
app.useLogger(app.get(Logger));
|
||||
app.useGlobalInterceptors(new ResponseInterceptor(app.get(Reflector), app.get(Logger), app.get(EventEmitter2)));
|
||||
app.useGlobalFilters(new AllExceptionsFilter(app.get(Logger)));
|
||||
app.useGlobalPipes(new ValidationPipe({ whitelist: true, transform: true }));
|
||||
app.useWebSocketAdapter(new WsAdapter(app));
|
||||
|
|
|
|||
|
|
@ -49,3 +49,5 @@ export enum FEATURE_KEY {
|
|||
HEALTH = 'health',
|
||||
ROOT = 'root',
|
||||
}
|
||||
|
||||
export const AUDIT_LOGS_REQUEST_CONTEXT_KEY = 'tj_audit_logs_meta_data';
|
||||
|
|
|
|||
|
|
@ -31,9 +31,11 @@ import { FEATURES as TOOLJET_DATABASE_FEATURES } from '@modules/tooljet-db/const
|
|||
import { FEATURES as IMPORT_EXPORT_RESOURCES_FEATURES } from '@modules/import-export-resources/constants/feature';
|
||||
import { FEATURES as TEMPLATES_FEATURES } from '@modules/templates/constants/features';
|
||||
import { FEATURES as AI_FEATURES } from '@modules/ai/constants/feature';
|
||||
import { FEATURES as AUDIT_LOGS_FEATURES } from '@modules/audit-logs/constants/features';
|
||||
import { getTooljetEdition } from '@helpers/utils.helper';
|
||||
import { TOOLJET_EDITIONS } from '.';
|
||||
import { FEATURES as WHITE_LABELLING_FEATURES } from '@modules/white-labelling/constant/feature';
|
||||
import { FEATURES as EXTERNAL_API_FEATURES } from '@modules/external-apis/constants/feature';
|
||||
|
||||
const GROUP_PERMISSIONS_FEATURES =
|
||||
getTooljetEdition() === TOOLJET_EDITIONS.EE ? GROUP_PERMISSIONS_FEATURES_EE : GROUP_PERMISSIONS_FEATURES_CE;
|
||||
|
|
@ -73,4 +75,6 @@ export const MODULE_INFO: { [key: string]: any } = {
|
|||
...ORGANIZATION_CONSTANT,
|
||||
...AI_FEATURES,
|
||||
...WHITE_LABELLING_FEATURES,
|
||||
...AUDIT_LOGS_FEATURES,
|
||||
...EXTERNAL_API_FEATURES,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ export enum MODULES {
|
|||
USER = 'USER',
|
||||
PROFILE = 'PROFILE',
|
||||
PLUGINS = 'Plugins',
|
||||
GLOBAL_DATA_SOURCE = 'GlobalDataSource',
|
||||
GLOBAL_DATA_SOURCE = 'DATA_SOURCE',
|
||||
DATA_QUERY = 'DataQueries',
|
||||
THREAD = 'Thread',
|
||||
COMMENT = 'Comment',
|
||||
|
|
@ -36,4 +36,6 @@ export enum MODULES {
|
|||
IMPORT_EXPORT_RESOURCES = 'ImportExportResources',
|
||||
TEMPLATES = 'Templates',
|
||||
AI = 'ai',
|
||||
AUDIT_LOGS = 'auditLogs',
|
||||
EXTERNAL_APIS = 'externalApis',
|
||||
}
|
||||
|
|
|
|||