Merge remote-tracking branch 'origin/main' into test/appbuilder-gh-workflow

This commit is contained in:
emidhun 2025-04-25 14:08:17 +05:30
commit 1d9a66a692
217 changed files with 3113 additions and 17990 deletions

View file

@ -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: |

View file

@ -232,3 +232,95 @@ jobs:
# fi
# curl -X POST -H 'Content-type: application/json' --data "{\"text\":\"$message\"}" ${{ secrets.SLACK_WEBHOOK_URL }}
try-tooljet-image-build:
runs-on: ubuntu-latest
needs: build-tooljet-image-for-ee-edtion
if: ${{ needs.build-tooljet-image-for-ee-edtion.result == 'success' }}
steps:
- name: Checkout code to develop
if: "!contains(github.event.release.tag_name, 'ee-lts')"
uses: actions/checkout@v2
with:
ref: refs/heads/main
- name: Checkout code to lts-3.0
if: contains(github.event.release.tag_name, '-ee-lts')
uses: actions/checkout@v2
with:
ref: refs/heads/lts-3.0
# Create Docker Buildx builder with platform configuration
- name: Set up Docker Buildx
run: |
mkdir -p ~/.docker/cli-plugins
curl -SL https://github.com/docker/buildx/releases/download/v0.11.0/buildx-v0.11.0.linux-amd64 -o ~/.docker/cli-plugins/docker-buildx
chmod a+x ~/.docker/cli-plugins/docker-buildx
docker buildx create --name mybuilder --platform linux/arm64,linux/amd64,linux/amd64/v2,linux/riscv64,linux/ppc64le,linux/s390x,linux/386,linux/mips64le,linux/mips64,linux/arm/v7,linux/arm/v6
docker buildx use mybuilder
- name: Set DOCKER_CLI_EXPERIMENTAL
run: echo "DOCKER_CLI_EXPERIMENTAL=enabled" >> $GITHUB_ENV
- name: use mybuilder buildx
run: docker buildx use mybuilder
- name: Docker Login
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Check if Docker image is present
id: check-image-presence
run: |
response=$(curl -s "https://hub.docker.com/v2/repositories/tooljet/tooljet/tags/${{ github.event.release.tag_name }}")
if [[ $? -ne 0 ]]; then
echo "Failed to fetch JSON response. Stopping workflow execution."
exit 1
fi
if [[ $response == *"tag '${{ github.event.release.tag_name }}' not found"* ]]; then
echo "Docker image tag '${{ github.event.release.tag_name }}' not present."
exit 1
else
echo "Docker image tag '${{ github.event.release.tag_name }}' is present."
fi
- name: Build and Push Docker image for non-EE-LTS
if: "!contains(github.event.release.tag_name, '-ee-lts')"
uses: docker/build-push-action@v4
with:
context: .
file: docker/ee/ee-try-tooljet.Dockerfile
push: true
tags: tooljet/try:${{ github.event.release.tag_name }},tooljet/try:ee-latest
platforms: linux/amd64
env:
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
- name: Build and Push Docker image for EE-LTS-3.0
if: contains(github.event.release.tag_name, '-ee-lts')
uses: docker/build-push-action@v4
with:
context: .
file: docker/ee/ee-try-tooljet-lts.Dockerfile
push: true
tags: tooljet/try:${{ github.event.release.tag_name }},tooljet/try:ee-lts-latest
platforms: linux/amd64
env:
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
- name: Send Slack Notification
run: |
if [[ "${{ job.status }}" == "success" ]]; then
message="Try-ToolJet image published:\\n\`tooljet/try:${{ github.event.release.tag_name }}\`"
else
message="Job '${{ env.JOB_NAME }}' failed! tooljet/try:${{ github.event.release.tag_name }}"
fi
curl -X POST -H 'Content-type: application/json' --data "{\"text\":\"$message\"}" ${{ secrets.SLACK_WEBHOOK_URL }}

View file

@ -8,16 +8,16 @@ permissions:
issues: write
jobs:
label-stale-deploys:
label-stale-ce-deploys:
runs-on: ubuntu-latest
permissions:
pull-requests: write
pull-requests: write
steps:
- uses: akshaysasidrn/stale-label-fetch@v1.1
id: stale-label
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
stale-label: 'active-review-app'
stale-label: 'active-ce-review-app'
stale-time: '86400'
type: 'pull_request'
- name: Get stale numbers
@ -40,6 +40,42 @@ jobs:
issue_number: prNumber,
owner: context.repo.owner,
repo: context.repo.repo,
labels: ['suspend-review-app']
labels: ['suspend-ce-review-app']
})
}
label-stale-ee-deploys:
runs-on: ubuntu-latest
permissions:
pull-requests: write
steps:
- uses: akshaysasidrn/stale-label-fetch@v1.1
id: stale-label
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
stale-label: 'active-ee-review-app'
stale-time: '86400'
type: 'pull_request'
- name: Get stale numbers
run: echo "Matched PR numbers - ${{ steps.stale-label.outputs.stale-numbers }}"
- name: Add suspend label
uses: actions/github-script@v6
env:
STALE_NUMBERS: ${{ steps.stale-label.outputs.stale-numbers }}
with:
github-token: ${{ secrets.TJ_BOT_PAT }}
script: |
if (!process.env.STALE_NUMBERS) return
const prNumbers = process.env.STALE_NUMBERS.split(",")
console.log(`Adding suspend labels for: ${prNumbers}`)
for (const prNumber of prNumbers) {
github.rest.issues.addLabels({
issue_number: prNumber,
owner: context.repo.owner,
repo: context.repo.repo,
labels: ['suspend-ee-review-app']
})
}

View file

@ -1 +1 @@
3.9.0
3.10.0

View file

@ -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",
],

View file

@ -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,

View file

@ -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" },

View file

@ -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", {

View file

@ -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"]',

View file

@ -13,7 +13,7 @@ export const dataSourceText = {
? "Databases (20)"
: "Databases (18)";
},
allApis: "APIs (20)",
allApis: "APIs (21)",
allCloudStorage: "Cloud Storages (4)",
pluginsLabelAndCount: "Plugins (0)",

View file

@ -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!` },

View file

@ -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");
});
});

View file

@ -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);
};

View file

@ -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);
};

View file

@ -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."
);
});
});

View file

@ -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");
});

View file

@ -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);

View file

@ -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
});

View file

@ -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");

View file

@ -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

View file

@ -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]", "");

View file

@ -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)

View file

@ -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();

View file

@ -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");
});
});

View file

@ -2127,7 +2127,7 @@
"encrypted": false
},
"host": {
"value": "35.202.183.199",
"value": "35.238.9.114",
"encrypted": false
},
"port": {

View file

@ -585,7 +585,7 @@
"encrypted": false
},
"host": {
"value": "35.202.183.199",
"value": "35.238.9.114",
"encrypted": false
},
"port": {

View file

@ -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": {

View file

@ -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!"
);

View file

@ -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));
};

View file

@ -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");
}
});
}

View file

@ -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
});

View file

@ -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}`,

27
docker/ce-entrypoint.sh Executable file
View file

@ -0,0 +1,27 @@
#!/bin/bash
set -e
if [ -d "./server/dist" ]; then
SETUP_CMD='npm run db:setup:prod'
else
SETUP_CMD='npm run db:setup'
fi
if [ -f "./.env" ]; then
declare $(grep -v '^#' ./.env | xargs)
fi
if [ -z "$DATABASE_URL" ]; then
./server/scripts/wait-for-it.sh $PG_HOST:${PG_PORT:-5432} --strict --timeout=300 -- $SETUP_CMD
else
PG_HOST=$(echo "$DATABASE_URL" | awk -F'[/:@?]' '{print $6}')
PG_PORT=$(echo "$DATABASE_URL" | awk -F'[/:@?]' '{print $7}')
if [ -z "$DATABASE_PORT" ]; then
DATABASE_PORT="5432"
fi
./server/scripts/wait-for-it.sh "$PG_HOST:$PG_PORT" --strict --timeout=300 -- $SETUP_CMD
fi
exec "$@"

View file

@ -88,12 +88,13 @@ COPY --from=builder /app/frontend/build ./app/frontend/build
# copy server build
COPY --from=builder /app/server/package.json ./app/server/package.json
COPY --from=builder /app/server/.version ./app/server/.version
COPY --from=builder /app/server/entrypoint.sh ./app/server/entrypoint.sh
COPY --from=builder /app/server/node_modules ./app/server/node_modules
COPY --from=builder /app/server/templates ./app/server/templates
COPY --from=builder /app/server/scripts ./app/server/scripts
COPY --from=builder /app/server/dist ./app/server/dist
COPY ./docker/ce-entrypoint.sh ./app/server/entrypoint.sh
# Define non-sudo user
RUN useradd --create-home --home-dir /home/appuser appuser \
&& chown -R appuser:0 /app \
@ -111,5 +112,4 @@ WORKDIR /app
# Dependencies for scripts outside nestjs
RUN npm install dotenv@10.0.0 joi@17.4.1
ENTRYPOINT ["./server/entrypoint.sh"]

View file

@ -145,12 +145,13 @@ COPY --from=builder /app/frontend/build ./app/frontend/build
COPY --from=builder /app/server/package.json ./app/server/package.json
COPY --from=builder /app/server/.version ./app/server/.version
COPY --from=builder /app/server/ee/keys ./app/server/ee/keys
COPY --from=builder /app/server/entrypoint.sh ./app/server/entrypoint.sh
COPY --from=builder /app/server/node_modules ./app/server/node_modules
COPY --from=builder /app/server/templates ./app/server/templates
COPY --from=builder /app/server/scripts ./app/server/scripts
COPY --from=builder /app/server/dist ./app/server/dist
COPY ./docker/ee/ee-entrypoint.sh ./app/server/ee-entrypoint.sh
# Define non-sudo user
RUN useradd --create-home --home-dir /home/appuser appuser \
&& chown -R appuser:0 /app \
@ -214,4 +215,4 @@ RUN npm install dotenv@10.0.0 joi@17.4.1
RUN npm cache clean --force
ENTRYPOINT ["./server/entrypoint.sh"]
ENTRYPOINT ["./server/ee-entrypoint.sh"]

View file

@ -0,0 +1,15 @@
#!/bin/bash
set -e
# Start Redis
# service redis-server start
# redis-server /etc/redis/redis.conf
# Start Postgres
service postgresql start
# Export the PORT variable to be used by the application
export PORT=${PORT:-80}
# Start Supervisor
exec supervisord -c /etc/supervisor/conf.d/supervisord.conf

View file

@ -22,7 +22,7 @@ echo "Starting Temporal Server..."
export PORT=${PORT:-80}
# Start Supervisor
/usr/bin/supervisord -n &
exec supervisord -c /etc/supervisor/conf.d/supervisord.conf &
# Wait for Temporal Server to be ready
echo "Waiting for Temporal Server to be ready..."

View file

@ -1,21 +1,31 @@
FROM tooljet/tooljet-ce:latest
FROM tooljet/tooljet:ee-lts-latest
# copy postgrest executable
COPY --from=postgrest/postgrest:v10.1.1.20221215 /bin/postgrest /bin
# Copy PostgREST executable
COPY --from=postgrest/postgrest:v12.2.0 /bin/postgrest /bin
# Install Postgres
# Install PostgreSQL
USER root
RUN wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add -
RUN echo "deb http://apt.postgresql.org/pub/repos/apt/ bullseye-pgdg main" | tee /etc/apt/sources.list.d/pgdg.list
RUN echo "deb http://deb.debian.org/debian"
RUN apt update && apt -y install postgresql-13 postgresql-client-13 supervisor
USER postgres
RUN service postgresql start && \
psql -c "create role tooljet with login superuser password 'postgres';"
USER root
# Install Redis
RUN apt update && apt -y install redis
# Create appuser home & ensure permission for supervisord and services
RUN mkdir -p /var/log/supervisor /var/run/postgresql /var/lib/postgresql /var/lib/redis && \
chown -R appuser:appuser /etc/supervisor /var/log/supervisor /var/lib/redis && \
chown -R postgres:postgres /var/run/postgresql /var/lib/postgresql
# Configure Supervisor to manage PostgREST, ToolJet, and Redis
RUN echo "[supervisord] \n" \
"nodaemon=true \n" \
"user=root \n" \
"\n" \
"[program:postgrest] \n" \
"command=/bin/postgrest \n" \
@ -23,12 +33,23 @@ RUN echo "[supervisord] \n" \
"autorestart=true \n" \
"\n" \
"[program:tooljet] \n" \
"user=appuser \n" \
"command=/bin/bash -c '/app/server/scripts/init-db-boot.sh' \n" \
"autostart=true \n" \
"autorestart=true \n" \
"stderr_logfile=/dev/stdout \n" \
"stderr_logfile_maxbytes=0 \n" \
"stdout_logfile=/dev/stdout \n" \
"stdout_logfile_maxbytes=0 \n" \
"\n" \
"[program:redis] \n" \
"user=appuser \n" \
"command=/usr/bin/redis-server \n" \
"autostart=true \n" \
"autorestart=true \n" \
"stderr_logfile=/dev/stdout \n" \
"stderr_logfile_maxbytes=0 \n" \
"stdout_logfile=/dev/stdout \n" \
"stdout_logfile_maxbytes=0 \n" | sed 's/ //' > /etc/supervisor/conf.d/supervisord.conf
# ENV defaults
@ -49,10 +70,17 @@ ENV TOOLJET_HOST=http://localhost \
PGRST_HOST=http://localhost:3000 \
PGRST_DB_URI=postgres://tooljet:postgres@localhost/tooljet_db \
PGRST_JWT_SECRET=r9iMKoe5CRMgvJBBtp4HrqN7QiPpUToj \
PGRST_DB_PRE_CONFIG=postgrest.pre_config \
ORM_LOGGING=true \
DEPLOYMENT_PLATFORM=docker:local \
HOME=/home/appuser \
REDIS_HOST=localhost \
REDIS_PORT=6379 \
REDIS_USER=default \
REDIS_PASS= \
TERM=xterm
# Prepare DB and start application
ENTRYPOINT service postgresql start 1> /dev/null && /usr/bin/supervisord
# Set the entrypoint
COPY ./docker/ee/ee-try-entrypoint-lts.sh /ee-try-entrypoint-lts.sh
RUN chmod +x /ee-try-entrypoint-lts
ENTRYPOINT ["/ee-try-entrypoint-lts.sh"]

View file

@ -0,0 +1,117 @@
FROM tooljet/tooljet:ee-latest
# Copy postgrest executable
COPY --from=postgrest/postgrest:v12.2.0 /bin/postgrest /bin
# Install Postgres
USER root
RUN wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add -
RUN echo "deb http://apt.postgresql.org/pub/repos/apt/ bullseye-pgdg main" | tee /etc/apt/sources.list.d/pgdg.list
RUN echo "deb http://deb.debian.org/debian"
RUN apt update && apt -y install postgresql-13 postgresql-client-13 supervisor
USER postgres
RUN service postgresql start && \
psql -c "create role tooljet with login superuser password 'postgres';"
USER root
RUN apt update && apt -y install redis
# Create appuser home & ensure permission for supervisord and services
RUN mkdir -p /var/log/supervisor /var/run/postgresql /var/lib/postgresql /var/lib/redis && \
chown -R appuser:appuser /etc/supervisor /var/log/supervisor /var/lib/redis && \
chown -R postgres:postgres /var/run/postgresql /var/lib/postgresql
# Install Temporal Server Binaries
RUN curl -OL https://github.com/temporalio/temporal/releases/download/v1.24.2/temporal_1.24.2_linux_amd64.tar.gz && \
tar -xzf temporal_1.24.2_linux_amd64.tar.gz && \
mv temporal-server /usr/bin/temporal-server && \
chmod +x /usr/bin/temporal-server && \
rm temporal_1.24.2_linux_amd64.tar.gz
# Install Temporal UI Server Binaries
RUN curl -OL https://github.com/temporalio/ui-server/releases/download/v2.28.0/ui-server_2.28.0_linux_amd64.tar.gz && \
tar -xzf ui-server_2.28.0_linux_amd64.tar.gz && \
mv ui-server /usr/bin/temporal-ui-server && \
chmod +x /usr/bin/temporal-ui-server && \
rm ui-server_2.28.0_linux_amd64.tar.gz
# Copy Temporal configuration files
COPY ./docker/ee/temporal-server.yaml /etc/temporal/temporal-server.yaml
COPY ./docker/ee/temporal-ui-server.yaml /etc/temporal/temporal-ui-server.yaml
# Install grpcurl
RUN apt update && apt install -y curl \
&& curl -sSL https://github.com/fullstorydev/grpcurl/releases/download/v1.8.0/grpcurl_1.8.0_linux_x86_64.tar.gz | tar -xzv -C /usr/local/bin grpcurl
# Configure Supervisor to manage PostgREST, ToolJet, and Redis
RUN echo "[supervisord] \n" \
"nodaemon=true \n" \
"user=root \n" \
"\n" \
"[program:postgrest] \n" \
"command=/bin/postgrest \n" \
"autostart=true \n" \
"autorestart=true \n" \
"\n" \
"[program:tooljet] \n" \
"user=appuser \n" \
"command=/bin/bash -c '/app/server/scripts/init-db-boot.sh' \n" \
"autostart=true \n" \
"autorestart=true \n" \
"stderr_logfile=/dev/stdout \n" \
"stderr_logfile_maxbytes=0 \n" \
"stdout_logfile=/dev/stdout \n" \
"stdout_logfile_maxbytes=0 \n" \
"\n" \
"[program:redis] \n" \
"user=appuser \n" \
"command=/usr/bin/redis-server \n" \
"autostart=true \n" \
"autorestart=true \n" \
"stderr_logfile=/dev/stdout \n" \
"stderr_logfile_maxbytes=0 \n" \
"stdout_logfile=/dev/stdout \n" \
"stdout_logfile_maxbytes=0 \n" | sed 's/ //' > /etc/supervisor/conf.d/supervisord.conf
# ENV defaults
ENV TOOLJET_HOST=http://localhost \
TOOLJET_SERVER_URL=http://localhost \
PORT=80 \
NODE_ENV=production \
LOCKBOX_MASTER_KEY=replace_with_lockbox_master_key \
SECRET_KEY_BASE=replace_with_secret_key_base \
PG_DB=tooljet_production \
PG_USER=tooljet \
PG_PASS=postgres \
PG_HOST=localhost \
ENABLE_TOOLJET_DB=true \
TOOLJET_DB_HOST=localhost \
TOOLJET_DB_USER=tooljet \
TOOLJET_DB_PASS=postgres \
TOOLJET_DB=tooljet_db \
PGRST_HOST=http://localhost:3000 \
PGRST_DB_URI=postgres://tooljet:postgres@localhost/tooljet_db \
PGRST_JWT_SECRET=r9iMKoe5CRMgvJBBtp4HrqN7QiPpUToj \
PGRST_DB_PRE_CONFIG=postgrest.pre_config \
ORM_LOGGING=true \
DEPLOYMENT_PLATFORM=docker:local \
HOME=/home/appuser \
REDIS_HOST=localhost \
REDIS_PORT=6379 \
REDIS_USER=default \
REDIS_PASS= \
ENABLE_MARKETPLACE_FEATURE=true \
TERM=xterm \
ENABLE_WORKFLOW_SCHEDULING=true \
TEMPORAL_SERVER_ADDRESS=localhost:7233 \
TEMPORAL_TASK_QUEUE_NAME_FOR_WORKFLOWS=tooljet-workflows \
TOOLJET_WORKFLOWS_TEMPORAL_NAMESPACE=default \
TEMPORAL_ADDRESS=localhost:7233 \
TEMPORAL_CORS_ORIGINS=http://localhost:8080
# Set the entrypoint
COPY ./docker/ee/ee-try-entrypoint.sh /ee-try-entrypoint.sh
RUN chmod +x /ee-try-entrypoint.sh
ENTRYPOINT ["/ee-try-entrypoint.sh"]

View file

@ -0,0 +1,75 @@
log:
stdout: true
level: info
persistence:
defaultStore: sqlite-default
visibilityStore: sqlite-visibility
numHistoryShards: 4
datastores:
sqlite-default:
sql:
pluginName: "sqlite"
databaseName: "/etc/temporal/default.db"
connectAddr: "localhost"
connectProtocol: "tcp"
connectAttributes:
cache: "private"
setup: true
sqlite-visibility:
sql:
pluginName: "sqlite"
databaseName: "/etc/temporal/visibility.db"
connectAddr: "localhost"
connectProtocol: "tcp"
connectAttributes:
cache: "private"
setup: true
global:
membership:
maxJoinDuration: 30s
broadcastAddress: "127.0.0.1"
pprof:
port: 7936
services:
frontend:
rpc:
grpcPort: 7233
membershipPort: 6933
bindOnLocalHost: true
httpPort: 7243
matching:
rpc:
grpcPort: 7235
membershipPort: 6935
bindOnLocalHost: true
history:
rpc:
grpcPort: 7234
membershipPort: 6934
bindOnLocalHost: true
worker:
rpc:
membershipPort: 6939
clusterMetadata:
enableGlobalNamespace: false
failoverVersionIncrement: 10
masterClusterName: "active"
currentClusterName: "active"
clusterInformation:
active:
enabled: true
initialFailoverVersion: 1
rpcName: "frontend"
rpcAddress: "localhost:7236"
httpAddress: "localhost:7243"
dcRedirectionPolicy:
policy: "noop"

View file

@ -0,0 +1,8 @@
temporalGrpcAddress: 127.0.0.1:7233 # Use the correct Temporal server address
host: 0.0.0.0
port: 8080
enableUi: true
cors:
allowOrigins:
- http://localhost:8080
defaultNamespace: default

View file

@ -1 +1 @@
3.9.0
3.10.0

View 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 First Name Last Name Email User Role Group
2 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

View file

@ -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>
</>
);
}

View file

@ -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);
}
);

View file

@ -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}
/>

View file

@ -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;

View file

@ -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';

View file

@ -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 '';
}

View file

@ -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');
}}
>

View file

@ -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 = ``;

View file

@ -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');
}}
>

View file

@ -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&apos;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>
);
};

View file

@ -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 */
}

View file

@ -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'}

View file

@ -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 && (

View file

@ -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"
>

View file

@ -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"

View file

@ -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: '/' } };

View file

@ -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 = {

View file

@ -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
);
}

View file

@ -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 {

View file

@ -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 {

View file

@ -11604,10 +11604,6 @@ tbody {
cursor: pointer;
}
.user-csv-template-wrap {
margin-top: 24px;
}
.manage-users-drawer-content-bulk {
margin: 24px 15px;

View 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;

View 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;

View file

@ -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':

View file

@ -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)}

View file

@ -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>

View file

@ -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"

View file

@ -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}
/>
)}

View file

@ -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

View file

@ -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={() =>

View file

@ -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"

View file

@ -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' }}>

View file

@ -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={() => { }}
/>
</>
);

View file

@ -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&apos;t be able to edit this version after promotion. Are you sure you want to continue?
</div>

View file

@ -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>
);

View file

@ -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'}`}>

View file

@ -73,6 +73,7 @@ const UsersFilter = ({ filterList, resetSearch }) => {
useMenuPortal={true}
closeMenuOnSelect={true}
width="161.25px"
placeholder="All"
/>
</div>
</div>

View file

@ -7,6 +7,12 @@
.onboarding-form-width {
width: 308px;
.submit-button {
margin: 0 auto;
border: none;
width: 308px;
}
}
.free-space {

View file

@ -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>}

View file

@ -34,6 +34,12 @@
opacity: 0.5;
pointer-events: none;
}
.submit-button {
margin: 0 auto;
border: none;
width: 308px;
}
}
.__ce {

View file

@ -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) => {

View file

@ -1 +1 @@
3.9.0
3.10.0

View file

@ -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

View file

@ -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;

View file

@ -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;

View file

@ -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 {}

View file

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

View file

Before

Width:  |  Height:  |  Size: 6.5 KiB

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

View file

Before

Width:  |  Height:  |  Size: 9.1 KiB

After

Width:  |  Height:  |  Size: 9.1 KiB

View file

Before

Width:  |  Height:  |  Size: 4.9 KiB

After

Width:  |  Height:  |  Size: 4.9 KiB

View file

@ -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>

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