Merge branch 'main' into fix/external-epi

This commit is contained in:
gsmithun4 2025-05-14 21:41:27 +05:30
commit 81f039b481
56 changed files with 715 additions and 400 deletions

View file

@ -170,7 +170,7 @@ jobs:
"serviceDetails": {
"disk": {
"name": "tooljet-ce-pr-${{ env.PR_NUMBER }}-postgresql",
"mountPath": "/data",
"mountPath": "/var/lib/postgresql/13/main",
"sizeGB": 10
},
"env": "docker",
@ -393,6 +393,39 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Sync repo
uses: actions/checkout@v3
- name: Check if Forked Repository
id: check_repo
run: |
if [[ "${{ github.event.pull_request.head.repo.fork }}" == "true" ]]; then
echo "is_fork=true" >> $GITHUB_ENV
echo "FORKED_OWNER=${{ github.event.pull_request.head.repo.owner.login }}" >> $GITHUB_ENV
else
echo "is_fork=false" >> $GITHUB_ENV
fi
- name: Set Repository URL
run: |
if [[ "$is_fork" == "true" ]]; then
echo "REPO_URL=https://github.com/${FORKED_OWNER}/ToolJet" >> $GITHUB_ENV
else
echo "REPO_URL=https://github.com/ToolJet/ToolJet" >> $GITHUB_ENV
fi
- name: Fetch and Checkout Forked Branch
if: env.is_fork == 'true'
run: |
git fetch origin pull/${{ github.event.number }}/head:${{ env.BRANCH_NAME }}
git checkout ${{ env.BRANCH_NAME }}
- name: Checkout Default Branch
if: env.is_fork == 'false'
uses: actions/checkout@v3
- name: Creating deployment for Enterprise Edition
id: create-ee-deployment
run: |
@ -408,7 +441,7 @@ jobs:
"name": "ToolJet EE PR #${{ env.PR_NUMBER }}",
"notifyOnFail": "default",
"ownerId": "tea-caeo4bj19n072h3dddc0",
"repo": "https://github.com/ToolJet/ToolJet",
"repo": "'"$REPO_URL"'",
"slug": "tooljet-ee-pr-${{ env.PR_NUMBER }}",
"suspended": "not_suspended",
"suspenders": [],

View file

@ -1 +1 @@
3.12.0
3.12.1

View file

@ -39,11 +39,11 @@ module.exports = defineConfig({
chromeWebSecurity: false,
trashAssetsBeforeRuns: true,
e2e: {
setupNodeEvents(on, config) {
setupNodeEvents (on, config) {
config.baseUrl = environment.baseUrl;
on("task", {
readPdf(pathToPdf) {
readPdf (pathToPdf) {
return new Promise((resolve) => {
const pdfPath = path.resolve(pathToPdf);
let dataBuffer = fs.readFileSync(pdfPath);
@ -55,7 +55,7 @@ module.exports = defineConfig({
});
on("task", {
readXlsx(filePath) {
readXlsx (filePath) {
return new Promise((resolve, reject) => {
try {
let dataBuffer = fs.readFileSync(filePath);
@ -69,7 +69,7 @@ module.exports = defineConfig({
});
on("task", {
deleteFolder(folderName) {
deleteFolder (folderName) {
return new Promise((resolve, reject) => {
rmdir(folderName, { maxRetries: 10, recursive: true }, (err) => {
if (err) {
@ -83,7 +83,7 @@ module.exports = defineConfig({
});
on("task", {
dbConnection({ dbconfig, sql }) {
dbConnection ({ dbconfig, sql }) {
const client = new pg.Pool(dbconfig);
return client.query(sql);
},
@ -97,9 +97,9 @@ module.exports = defineConfig({
baseUrl: environment.baseUrl,
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/firstUser/firstUserOnboarding.cy.js",
"cypress/e2e/happyPath/platform/ceTestcases/apps/appSlug.cy.js",
"cypress/e2e/happyPath/platform/ceTestcases/**/!(*appSlug).cy.js",
"cypress/e2e/happyPath/platform/commonTestcases/**/*.cy.js",
],
numTestsKeptInMemory: 1,

View file

@ -6,6 +6,7 @@ import { passwordInputText } from "Texts/passwordInput";
import { importSelectors } from "Selectors/exportImport";
import { importText } from "Texts/exportImport";
import { onboardingSelectors } from "Selectors/onboarding";
import { selectAppCardOption } from "Support/utils/common";
const API_ENDPOINT =
Cypress.env("environment") === "Community"
@ -160,12 +161,10 @@ Cypress.Commands.add(
Cypress.Commands.add("deleteApp", (appName) => {
cy.intercept("DELETE", "/api/apps/*").as("appDeleted");
cy.get(commonSelectors.appCard(appName))
.realHover()
.find(commonSelectors.appCardOptionsButton)
.realHover()
.click();
cy.get(commonSelectors.deleteAppOption).click();
selectAppCardOption(
appName,
commonSelectors.appCardOptions(commonText.deleteAppOption)
);
cy.get(commonSelectors.buttonSelector(commonText.modalYesButton)).click();
cy.verifyToastMessage(
commonSelectors.toastMessage,
@ -227,9 +226,9 @@ Cypress.Commands.add(
.invoke("text")
.then((text) => {
cy.wrap(subject).realType(createBackspaceText(text)),
{
delay: 0,
};
{
delay: 0,
};
});
}
);
@ -398,39 +397,38 @@ Cypress.Commands.add("getPosition", (componentName) => {
});
Cypress.Commands.add("defaultWorkspaceLogin", () => {
cy.apiLogin();
cy.task("dbConnection", {
dbconfig: Cypress.env("app_db"),
sql: `
SELECT id FROM organizations WHERE name = 'My workspace';`,
}).then((resp) => {
const workspaceId = resp.rows[0].id;
// cy.intercept("GET", API_ENDPOINT).as("library_apps");
cy.visit("/my-workspace");
cy.wait(2000);
cy.get(commonSelectors.homePageLogo, { timeout: 10000 });
// cy.wait("@library_apps");
cy.apiLogin(
"dev@tooljet.io",
"password",
workspaceId,
"/my-workspace"
).then(() => {
cy.visit("/");
cy.wait(2000);
cy.get(commonSelectors.homePageLogo, { timeout: 10000 });
});
});
});
Cypress.Commands.add(
"visitSlug",
({
actualUrl,
errorUrls = [
`${Cypress.config("baseUrl")}/error/unknown`,
`${Cypress.config("baseUrl")}/error/restricted`,
],
}) => {
if (!actualUrl) {
throw new Error("actualUrl is required for visitSlug command.");
Cypress.Commands.add("visitSlug", ({ actualUrl }) => {
cy.visit(actualUrl);
cy.wait(1000);
cy.url().then((currentUrl) => {
if (currentUrl !== actualUrl) {
cy.visit(actualUrl);
cy.wait(1000);
}
});
});
cy.visit(actualUrl);
cy.url().then((url) => {
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") {
@ -551,7 +549,7 @@ Cypress.Commands.add("installMarketplacePlugin", (pluginName) => {
}
});
function installPlugin(pluginName) {
function installPlugin (pluginName) {
cy.get('[data-cy="-list-item"]').eq(1).click();
cy.wait(1000);

View file

@ -8,11 +8,7 @@ export const editVersionText = {
export const deleteVersionText = {
deleteModalText: (text) => {
// return `Are you sure you want to delete this version - ${cyParamName(
// text
// )}?`;
return `Deleting a version will permanently remove it from all environments.Are you sure you want to delete this version - ${cyParamName(
return `Are you sure you want to delete this version - ${cyParamName(
text
)}?`;
},

View file

@ -46,9 +46,7 @@ describe("App Export", () => {
});
it("Verify the elements of export dialog box", () => {
cy.window({ log: false }).then((win) => {
win.localStorage.setItem("walkthroughCompleted", "true");
});
cy.skipWalkthrough()
cy.apiLogin();
cy.visit(`${data.workspaceSlug}`);

View file

@ -34,6 +34,7 @@ describe("App Import Functionality", () => {
cy.apiLogin();
cy.apiCreateWorkspace(data.workspaceName, data.workspaceSlug);
cy.apiLogout();
cy.skipWalkthrough()
});
it("should verify app import functionality", () => {
@ -100,12 +101,13 @@ describe("App Import Functionality", () => {
.and("have.text", importText.appImportedToastMessage);
// Verify imported app
cy.get(".driver-close-btn").click();
cy.get(commonSelectors.toastCloseButton).click();
cy.wait(500);
cy.get(commonSelectors.appNameInput).verifyVisibleElement(
"contain.value",
"three-versions"
);
cy.get(appVersionSelectors.currentVersionField("v3")).should("be.visible");
// Configure app
cy.skipEditorPopover();

View file

@ -27,17 +27,21 @@ describe("App Slug", () => {
});
it("Verify app slug cases in global settings", () => {
cy.apiLogin();
const workspaceId = Cypress.env("workspaceId");
const appId = Cypress.env("appId");
const appUrl = `${host}/${Cypress.env("workspaceId")}/apps/${Cypress.env("appId")}/`;
cy.visit("/my-workspace");
cy.wait(1000);
cy.apiLogin();
cy.skipWalkthrough();
cy.window({ log: false }).then((win) => {
win.localStorage.setItem("walkthroughCompleted", "true");
cy.visit(appUrl);
cy.url().then((url) => {
if (url !== appUrl) {
cy.visit(appUrl);
}
});
cy.visit(`/${Cypress.env("workspaceId")}/apps/${Cypress.env("appId")}/`);
cy.url().should("eq", appUrl);
cy.wait(1000);
cy.get(commonSelectors.leftSideBarSettingsButton).click();

View file

@ -78,11 +78,11 @@ describe("Private and Public apps", {
// Test private access
logout();
cy.get(onboardingSelectors.signInButton, { timeout: 20000 }).should("be.visible");
cy.visitSlug({
actualUrl: `${Cypress.config("baseUrl")}/applications/${data.slug}`,
});
cy.get(onboardingSelectors.signInButton, { timeout: 20000 }).should("be.visible");
cy.wait(2000);
cy.appUILogin();
@ -116,6 +116,9 @@ describe("Private and Public apps", {
inviteUserToWorkspace(data.firstName, data.email);
logout();
cy.visit("/");
cy.wait(2000);
cy.get(onboardingSelectors.signInButton, { timeout: 20000 }).should("be.visible");
// Test private access
cy.visitSlug({
@ -141,6 +144,8 @@ describe("Private and Public apps", {
cy.wait(1000);
cy.apiMakeAppPublic();
logout();
cy.wait(1000);
cy.get(onboardingSelectors.signInButton, { timeout: 20000 }).should("be.visible");
cy.visitSlug({
actualUrl: `${Cypress.config("baseUrl")}/applications/${data.slug}`,
@ -177,6 +182,9 @@ describe("Private and Public apps", {
cy.apiMakeAppPublic();
logout();
cy.wait(1000);
cy.get(onboardingSelectors.signInButton, { timeout: 20000 }).should("be.visible");
cy.visitSlug({
actualUrl: `${Cypress.config("baseUrl")}/applications/${data.slug}`,
});
@ -229,6 +237,8 @@ describe("Private and Public apps", {
cy.get('[data-cy="viewer-page-logo"]').click();
logout();
cy.wait(1000);
cy.get(onboardingSelectors.signInButton, { timeout: 20000 }).should("be.visible");
// Setup new workspace and app
cy.defaultWorkspaceLogin();

View file

@ -123,7 +123,7 @@ describe("App Version", () => {
releasedVersionAndVerify("v2");
});
it.only("should verify version management with components and queries", () => {
it("should verify version management with components and queries", () => {
// Initial setup with component and datasource
cy.apiAddComponentToApp(
data.appName,

View file

@ -44,6 +44,164 @@ describe("dashboard", () => {
cy.visit(`${data.workspaceSlug}`);
});
// it("Should verify app card elements and app card operations", () => {
// const customLayout = {
// desktop: { top: 100, left: 20 },
// mobile: { width: 8, height: 50 },
// };
// cy.apiCreateApp(data.appName);
// cy.visit(`${data.workspaceSlug}`);
// cy.wait(2000);
// cy.get(commonSelectors.appCreationDetails).should("be.visible");
// cy.get(commonSelectors.appCard(data.appName)).should("be.visible");
// cy.get(commonSelectors.appTitle(data.appName)).verifyVisibleElement(
// "have.text",
// data.appName
// );
// viewAppCardOptions(data.appName);
// cy.get(
// commonSelectors.appCardOptions(commonText.changeIconOption)
// ).verifyVisibleElement("have.text", commonText.changeIconOption);
// cy.get(
// commonSelectors.appCardOptions(commonText.addToFolderOption)
// ).verifyVisibleElement("have.text", commonText.addToFolderOption);
// cy.get(
// commonSelectors.appCardOptions(commonText.cloneAppOption)
// ).verifyVisibleElement("have.text", commonText.cloneAppOption);
// cy.get(
// commonSelectors.appCardOptions(commonText.exportAppOption)
// ).verifyVisibleElement("have.text", commonText.exportAppOption);
// cy.get(
// commonSelectors.appCardOptions(commonText.deleteAppOption)
// ).verifyVisibleElement("have.text", commonText.deleteAppOption);
// modifyAndVerifyAppCardIcon(data.appName);
// createFolder(data.folderName);
// viewAppCardOptions(data.appName);
// cy.get(
// commonSelectors.appCardOptions(commonText.addToFolderOption)
// ).click();
// verifyModal(
// dashboardText.addToFolderTitle,
// dashboardText.addToFolderButton,
// dashboardSelector.selectFolder
// );
// cy.get(dashboardSelector.moveAppText).verifyVisibleElement(
// "have.text",
// dashboardText.moveAppText(data.appName)
// );
// cy.get(dashboardSelector.selectFolder).click();
// cy.get(commonSelectors.folderList).contains(data.folderName).click();
// cy.get(dashboardSelector.addToFolderButton).click();
// cy.verifyToastMessage(
// commonSelectors.toastMessage,
// commonText.AddedToFolderToast,
// false
// );
// cy.get(dashboardSelector.folderName(data.folderName)).verifyVisibleElement(
// "have.text",
// dashboardText.folderName(`${data.folderName} (1)`)
// );
// cy.get(dashboardSelector.folderName(data.folderName)).click();
// cy.get(commonSelectors.appCard(data.appName))
// .contains(data.appName)
// .should("be.visible");
// viewAppCardOptions(data.appName);
// cy.get(commonSelectors.appCardOptions(commonText.removeFromFolderOption))
// .verifyVisibleElement("have.text", commonText.removeFromFolderOption)
// .click();
// verifyConfirmationModal(commonText.appRemovedFromFolderMessage);
// cancelModal(commonText.cancelButton);
// viewAppCardOptions(data.appName);
// cy.get(
// commonSelectors.appCardOptions(commonText.removeFromFolderOption)
// ).click();
// cy.get(commonSelectors.buttonSelector(commonText.modalYesButton)).click();
// cy.verifyToastMessage(
// commonSelectors.toastMessage,
// commonText.appRemovedFromFolderTaost,
// false
// );
// cy.get(commonSelectors.modalComponent).should("not.exist");
// cy.get(commonSelectors.empytyFolderImage).should("be.visible");
// cy.get(commonSelectors.emptyFolderText).verifyVisibleElement(
// "have.text",
// commonText.emptyFolderText
// );
// cy.get(commonSelectors.allApplicationsLink).click();
// deleteFolder(data.folderName);
// cy.get(commonSelectors.allApplicationsLink).click();
// cy.wait(1000);
// viewAppCardOptions(data.appName);
// cy.wait(2000);
// cy.get(commonSelectors.appCardOptions(commonText.exportAppOption)).click();
// cy.get(commonSelectors.exportAllButton).click();
// cy.exec("ls ./cypress/downloads/").then((result) => {
// const downloadedAppExportFileName = result.stdout.split("\n")[0];
// expect(downloadedAppExportFileName).to.contain.string("app");
// });
// viewAppCardOptions(data.appName);
// cy.get(commonSelectors.appCardOptions(commonText.cloneAppOption)).click();
// cy.get('[data-cy="clone-app"]').click();
// cy.get(".go3958317564")
// .should("be.visible")
// .and("have.text", dashboardText.appClonedToast);
// cy.wait(3000);
// cy.renameApp(data.cloneAppName);
// cy.apiAddComponentToApp(data.cloneAppName, "button", 25, 25);
// cy.backToApps();
// cy.wait("@appLibrary");
// cy.wait(1000);
// cy.get(commonSelectors.appCard(data.cloneAppName)).should("be.visible");
// cy.wait(1000);
// viewAppCardOptions(data.cloneAppName);
// cy.get(commonSelectors.deleteAppOption).click();
// cy.get(commonSelectors.modalMessage).verifyVisibleElement(
// "have.text",
// commonText.deleteAppModalMessage(data.cloneAppName)
// );
// cy.get(
// commonSelectors.buttonSelector(commonText.cancelButton)
// ).verifyVisibleElement("have.text", commonText.cancelButton);
// cy.get(
// commonSelectors.buttonSelector(commonText.modalYesButton)
// ).verifyVisibleElement("have.text", commonText.modalYesButton);
// cancelModal(commonText.cancelButton);
// viewAppCardOptions(data.cloneAppName);
// cy.get(commonSelectors.deleteAppOption).click();
// cy.get(commonSelectors.buttonSelector(commonText.modalYesButton)).click();
// cy.verifyToastMessage(
// commonSelectors.toastMessage,
// commonText.appDeletedToast,
// false
// );
// verifyAppDelete(data.cloneAppName);
// cy.wait("@appLibrary");
// cy.deleteApp(data.appName);
// verifyAppDelete(data.appName);
// });
it("should verify the elements on empty dashboard", () => {
cy.intercept("GET", "/api/metadata", {
body: {
@ -171,181 +329,6 @@ describe("dashboard", () => {
verifyTooltip(dashboardSelector.modeToggle, "Mode");
});
it.skip("Should verify app card elements and app card operations", () => {
const customLayout = {
desktop: { top: 100, left: 20 },
mobile: { width: 8, height: 50 },
};
cy.apiCreateApp(data.appName);
cy.openApp();
cy.apiAddComponentToApp(data.appName, "text1", customLayout);
cy.backToApps();
cy.wait(500);
cy.get(commonSelectors.appCard(data.appName))
.parent()
.within(() => {
cy.get(commonSelectors.appCard(data.appName)).should("be.visible");
cy.get(commonSelectors.appTitle(data.appName)).verifyVisibleElement(
"have.text",
data.appName
);
cy.get(commonSelectors.appCreationDetails).should("be.visible");
//Add the edited details
});
viewAppCardOptions(data.appName);
cy.get(
commonSelectors.appCardOptions(commonText.changeIconOption)
).verifyVisibleElement("have.text", commonText.changeIconOption);
cy.get(
commonSelectors.appCardOptions(commonText.addToFolderOption)
).verifyVisibleElement("have.text", commonText.addToFolderOption);
cy.get(
commonSelectors.appCardOptions(commonText.cloneAppOption)
).verifyVisibleElement("have.text", commonText.cloneAppOption);
cy.get(
commonSelectors.appCardOptions(commonText.exportAppOption)
).verifyVisibleElement("have.text", commonText.exportAppOption);
cy.get(
commonSelectors.appCardOptions(commonText.deleteAppOption)
).verifyVisibleElement("have.text", commonText.deleteAppOption);
modifyAndVerifyAppCardIcon(data.appName);
createFolder(data.folderName);
viewAppCardOptions(data.appName);
cy.get(
commonSelectors.appCardOptions(commonText.addToFolderOption)
).click();
verifyModal(
dashboardText.addToFolderTitle,
dashboardText.addToFolderButton,
dashboardSelector.selectFolder
);
cy.get(dashboardSelector.moveAppText).verifyVisibleElement(
"have.text",
dashboardText.moveAppText(data.appName)
);
cy.get(dashboardSelector.selectFolder).click();
cy.get(commonSelectors.folderList).contains(data.folderName).click();
cy.get(dashboardSelector.addToFolderButton).click();
cy.verifyToastMessage(
commonSelectors.toastMessage,
commonText.AddedToFolderToast
);
cy.get(dashboardSelector.folderName(data.folderName)).verifyVisibleElement(
"have.text",
dashboardText.folderName(`${data.folderName} (1)`)
);
cy.get(dashboardSelector.folderName(data.folderName)).click();
cy.get(commonSelectors.appCard(data.appName))
.contains(data.appName)
.should("be.visible");
cy.wait(2000);
viewAppCardOptions(data.appName);
cy.get(commonSelectors.appCardOptions(commonText.removeFromFolderOption))
.verifyVisibleElement("have.text", commonText.removeFromFolderOption)
.click();
verifyConfirmationModal(commonText.appRemovedFromFolderMessage);
cancelModal(commonText.cancelButton);
cy.wait(3000);
viewAppCardOptions(data.appName);
cy.get(
commonSelectors.appCardOptions(commonText.removeFromFolderOption)
).click();
cy.get(commonSelectors.buttonSelector(commonText.modalYesButton)).click();
cy.verifyToastMessage(
commonSelectors.toastMessage,
commonText.appRemovedFromFolderTaost
);
cy.get(commonSelectors.modalComponent).should("not.exist");
cy.get(commonSelectors.empytyFolderImage).should("be.visible");
cy.get(commonSelectors.emptyFolderText).verifyVisibleElement(
"have.text",
commonText.emptyFolderText
);
cy.get(commonSelectors.allApplicationsLink).click();
deleteFolder(data.folderName);
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();
cy.get(".go3958317564")
.should("be.visible")
.and("have.text", dashboardText.appClonedToast);
cy.wait(3000);
cy.renameApp(data.cloneAppName);
cy.apiAddComponentToApp(data.cloneAppName, "button", 25, 25);
cy.backToApps();
cy.wait("@appLibrary");
cy.wait(1000);
cy.reloadAppForTheElement(data.cloneAppName);
cy.get(commonSelectors.appCard(data.cloneAppName)).should("be.visible");
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();
cy.exec("ls ./cypress/downloads/").then((result) => {
const downloadedAppExportFileName = result.stdout.split("\n")[0];
expect(downloadedAppExportFileName).to.contain.string("app");
});
cy.wait(3000);
cy.reloadAppForTheElement(data.cloneAppName);
viewAppCardOptions(data.cloneAppName);
cy.get(commonSelectors.deleteAppOption).click();
cy.get(commonSelectors.modalMessage).verifyVisibleElement(
"have.text",
commonText.deleteAppModalMessage(data.cloneAppName)
);
cy.get(
commonSelectors.buttonSelector(commonText.cancelButton)
).verifyVisibleElement("have.text", commonText.cancelButton);
cy.get(
commonSelectors.buttonSelector(commonText.modalYesButton)
).verifyVisibleElement("have.text", commonText.modalYesButton);
cancelModal(commonText.cancelButton);
cy.wait(3000);
cy.reloadAppForTheElement(data.cloneAppName);
viewAppCardOptions(data.cloneAppName);
cy.get(commonSelectors.deleteAppOption).click();
cy.get(commonSelectors.buttonSelector(commonText.modalYesButton)).click();
cy.verifyToastMessage(
commonSelectors.toastMessage,
commonText.appDeletedToast
);
verifyAppDelete(data.cloneAppName);
cy.wait("@appLibrary");
cy.deleteApp(data.appName);
cy.verifyToastMessage(
commonSelectors.toastMessage,
commonText.appDeletedToast
);
verifyAppDelete(data.appName);
});
it("Should verify the app CRUD operation", () => {
const customLayout = {
desktop: { top: 100, left: 20 },
@ -369,10 +352,7 @@ describe("dashboard", () => {
cy.wait("@appLibrary");
cy.deleteApp(data.appName);
cy.verifyToastMessage(
commonSelectors.toastMessage,
commonText.appDeletedToast
);
verifyAppDelete(data.appName);
});
@ -493,10 +473,7 @@ describe("dashboard", () => {
cy.get(commonSelectors.allApplicationsLink).click();
cy.deleteApp(data.appName);
cy.verifyToastMessage(
commonSelectors.toastMessage,
commonText.appDeletedToast
);
verifyAppDelete(data.appName);
logout();
});

View file

@ -204,10 +204,7 @@ describe("Manage Groups", () => {
cy.wait(2500);
cy.deleteApp(data.appName);
cy.verifyToastMessage(
commonSelectors.toastMessage,
commonText.appDeletedToast
);
// Folder operations
createFolder(data.folderName);

View file

@ -101,11 +101,14 @@ export const navigateToAppEditor = (appName) => {
export const viewAppCardOptions = (appName) => {
cy.wait(1000);
cy.reloadAppForTheElement(appName);
cy.get(commonSelectors.appCard(appName))
.realHover()
.find(commonSelectors.appCardOptionsButton)
.realHover()
cy.contains("div", appName)
.parent()
.within(() => {
cy.get(commonSelectors.appCardOptionsButton).invoke("click");
cy.get(commonSelectors.appCardOptionsButton).click();
});
};
@ -185,8 +188,9 @@ export const searchUser = (email) => {
};
export const selectAppCardOption = (appName, appCardOption) => {
cy.wait(1000);
viewAppCardOptions(appName);
cy.get(appCardOption).should("be.visible").click({ force: true });
cy.get(appCardOption).should("be.visible").click();
};
export const navigateToDatabase = () => {

View file

@ -239,7 +239,8 @@ export const createRestAPIQuery = (
key = "",
value = "",
url = "",
run = true
run = true,
kind = "restapi"
) => {
cy.getCookie("tj_auth_token").then((cookie) => {
const headers = {
@ -247,7 +248,6 @@ export const createRestAPIQuery = (
Cookie: `tj_auth_token=${cookie.value}`,
};
cy.log(Cypress.env("appId"));
cy.request({
method: "GET",
url: `${Cypress.env("server_host")}/api/apps/${Cypress.env("appId")}`,
@ -255,13 +255,13 @@ export const createRestAPIQuery = (
}).then((response) => {
const editingVersionId = response.body.editing_version.id;
const data_source_id = Cypress.env(`${dsName}-id`);
const data_source_id = Cypress.env(kind);
const requestBody = {
app_id: Cypress.env("appId"),
app_version_id: editingVersionId,
name: queryName,
kind: "restapi",
kind: kind,
options: {
method: "get",
url: url,

View file

@ -115,8 +115,8 @@ export const verifyDuplicateVersion = (newVersion = [], version) => {
cy.get(appVersionSelectors.createNewVersionButton).click();
cy.verifyToastMessage(
commonSelectors.toastMessage,
// appVersionText.versionNameAlreadyExists
"Already exists!"
appVersionText.versionNameAlreadyExists
// "Already exists!"
);
};

View file

@ -80,13 +80,21 @@ RUN wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-k
RUN echo "deb http://apt.postgresql.org/pub/repos/apt/ bullseye-pgdg main" | tee /etc/apt/sources.list.d/pgdg.list
RUN apt update && apt -y install postgresql-13 postgresql-client-13 supervisor
# Explicitly create PG main directory with correct ownership
RUN mkdir -p /var/lib/postgresql/13/main && \
chown -R postgres:postgres /var/lib/postgresql
RUN mkdir -p /var/log/supervisor /var/run/postgresql && \
chown -R postgres:postgres /var/run/postgresql /var/log/supervisor
# Explicitly create PG main directory with correct ownerships
RUN mkdir -p /var/lib/postgresql/13/main && \
# Remove existing data and create directory with proper ownership
RUN rm -rf /var/lib/postgresql/13/main && \
mkdir -p /var/lib/postgresql/13/main && \
chown -R postgres:postgres /var/lib/postgresql
# Initialize PostgreSQL
RUN su - postgres -c "/usr/lib/postgresql/13/bin/initdb -D /var/lib/postgresql/13/main"
# Configure Supervisor to manage PostgREST, ToolJet, and Redis
RUN echo "[supervisord] \n" \
"nodaemon=true \n" \

View file

@ -1 +1 @@
3.12.0
3.12.1

@ -1 +1 @@
Subproject commit 381a529935fcaf82b3d5d55d41bf16c66e6a5b98
Subproject commit 280578f99c45224428f78ee16285b62f4c3631fd

View file

@ -13,6 +13,7 @@ export const organizationService = {
getWorkspacesLimit,
checkWorkspaceUniqueness,
updateOrganization,
setDefaultWorkspace,
};
function getUsersByValue(searchInput) {
@ -100,3 +101,8 @@ function checkWorkspaceUniqueness(name, slug) {
const query = queryString.stringify({ name, slug });
return fetch(`${config.apiUrl}/organizations/is-unique?${query}`, requestOptions).then(handleResponse);
}
function setDefaultWorkspace(workspaceId) {
const requestOptions = { method: 'PATCH', headers: authHeader(), credentials: 'include' };
return fetch(`${config.apiUrl}/organizations/${workspaceId}/default`, requestOptions).then(handleResponse);
}

View file

@ -10029,92 +10029,6 @@ tbody {
}
}
}
.manage-ws-table-body {
width: 100%;
.workspace-table-row {
border-bottom: 1px solid var(--slate5);
height: 64px;
width: 100%;
.ws-name {
padding-left: 8px;
.current-workspace-tag {
font-weight: 500;
color: var(--indigo9);
font-size: 12px;
display: flex;
height: 21px;
width: 130px;
align-items: center;
margin-left: 20px;
padding: 4px 8px 5px 8px;
border: 1px solid var(--indigo7);
background-color: var(--indigo3);
border-radius: 100px;
}
}
.open-button-cont {
width: 44px;
padding: 0px 8px 0px 8px;
.workspace-open-btn {
width: 28px;
height: 28px;
background-color: var(--slate1);
border: 1px solid var(--slate7);
box-shadow: none;
&:hover {
background-color: var(--slate4);
}
}
}
.archive-btn-cont {
width: 103px;
padding-right: 8px;
.workspace-archive-btn {
width: 95px;
height: 28px;
background-color: var(--slate1);
box-shadow: none;
border: 1px solid var(--tomato7);
color: var(--tomato9);
&:hover {
background-color: var(--tomato3);
}
&:disabled {
border: 1px solid var(--slate7);
}
}
.workspace-active-btn {
width: 95px;
height: 28px;
background-color: var(--slate1);
box-shadow: none;
border: 1px solid var(--slate7);
color: var(--slate12);
&:hover {
background-color: var(--slate7);
}
}
}
}
}
}
.manage-workspace-table-wrap.dark-mode {

View file

@ -1 +1 @@
3.12.0
3.12.1

View file

@ -0,0 +1,66 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
import { TOOLJET_EDITIONS } from '@modules/app/constants';
import { getCustomEnvVars, getTooljetEdition } from '@helpers/utils.helper';
import { Organization } from '@entities/organization.entity';
import { WORKSPACE_STATUS } from '@modules/users/constants/lifecycle';
export class SetDefaultWorkspace1740401100000 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
if (getTooljetEdition() !== TOOLJET_EDITIONS.EE) {
console.log('Skipping migration as it is not EE edition');
return;
}
// Check if default workspace URL is configured
const defaultWorkspaceUrl = getCustomEnvVars('TOOLJET_DEFAULT_WORKSPACE_URL');
if (defaultWorkspaceUrl) {
try {
const url = new URL(defaultWorkspaceUrl);
const pathParts = url.pathname.split('/');
const workspaceSlug = pathParts[pathParts.length - 1];
if (workspaceSlug) {
const organization = await queryRunner.manager.findOne(Organization, {
where: { slug: workspaceSlug, status: WORKSPACE_STATUS.ACTIVE },
select: ['id'],
});
if (organization){
await queryRunner.query(`
UPDATE organizations
SET is_default = true
WHERE slug = $1
`, [workspaceSlug]);
return;
}
console.log(`No active organization found with slug: ${workspaceSlug}`);
}
} catch (err) {
console.log('Invalid TOOLJET_DEFAULT_WORKSPACE_URL format');
}
}
// Set the first created organization as default
await queryRunner.query(`
UPDATE organizations
SET is_default = true
WHERE id = (
SELECT id
FROM organizations
WHERE status = '${WORKSPACE_STATUS.ACTIVE}'
ORDER BY created_at ASC
LIMIT 1
);
`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
if (getTooljetEdition() !== TOOLJET_EDITIONS.EE) {
return;
}
// Unset all default workspaces
await queryRunner.query(`
UPDATE organizations
SET is_default = false;
`);
}
}

@ -1 +1 @@
Subproject commit cec44e5562b07caad5797ffc901bb097d51eed94
Subproject commit 69bdefb1f3f1d35bd6e7231e50799ff10a77a60f

View file

@ -0,0 +1,30 @@
import { MigrationInterface, QueryRunner, TableColumn } from 'typeorm';
export class AddIsDefaultToOrganizations1740401000000 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
// Add is_default column
await queryRunner.addColumn(
'organizations',
new TableColumn({
name: 'is_default',
type: 'boolean',
default: false,
isNullable: false,
})
);
// Create a partial unique index to ensure only one default workspace
await queryRunner.query(`
CREATE UNIQUE INDEX idx_organizations_single_default
ON organizations (is_default)
WHERE is_default = true;
`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
// Drop the unique index first
await queryRunner.query(`DROP INDEX IF EXISTS idx_organizations_single_default;`);
// Then drop the column
await queryRunner.dropColumn('organizations', 'is_default');
}
}

View file

@ -35,6 +35,9 @@ export class Organization extends BaseEntity {
@Column({ name: 'domain' })
domain: string;
@Column({ name: 'is_default', default: false })
isDefault: boolean;
@Column({ name: 'enable_sign_up' })
enableSignUp: boolean;

View file

@ -5,6 +5,8 @@ import { isEmpty } from 'lodash';
import { USER_TYPE } from '@modules/users/constants/lifecycle';
import { ConflictException } from '@nestjs/common';
import { DataBaseConstraints } from './db_constraints.constants';
import { getEnvVars } from 'scripts/database-config-utils';
const semver = require('semver');
@ -449,5 +451,11 @@ export const getSubpath = () => {
};
export function getTooljetEdition(): string {
return process.env.TOOLJET_EDITION?.toLowerCase() || 'ce';
const envVars = getEnvVars();
return envVars['TOOLJET_EDITION']?.toLowerCase() || 'ce';
}
export function getCustomEnvVars(name: string) {
const envVars = getEnvVars();
return envVars[name] || '';
}

View file

@ -174,7 +174,7 @@ export class OauthService implements IOAuthService {
// Not logging in to specific organization, creating new
const { name, slug } = generateNextNameAndSlug('My workspace');
defaultOrganization = await this.setupOrganizationsUtilService.create(name, slug, null, manager);
defaultOrganization = await this.setupOrganizationsUtilService.create({ name, slug }, null, manager);
userDetails = await this.userRepository.createOrUpdate(
{
@ -221,7 +221,7 @@ export class OauthService implements IOAuthService {
if (!isInviteRedirect) {
// no SSO login enabled organization available for user - creating new one
const { name, slug } = generateNextNameAndSlug('My workspace');
organizationDetails = await this.setupOrganizationsUtilService.create(name, slug, userDetails, manager);
organizationDetails = await this.setupOrganizationsUtilService.create({ name, slug }, userDetails, manager);
await this.userRepository.updateOne(
userDetails.id,
{ defaultOrganizationId: organizationDetails.id },

View file

@ -85,7 +85,7 @@ export class AuthService implements IAuthService {
} else if (allowPersonalWorkspace && !isInviteRedirect) {
// no form login enabled organization available for user - creating new one
const { name, slug } = generateNextNameAndSlug('My workspace');
organization = await this.setupOrganizationsUtilService.create(name, slug, user, manager);
organization = await this.setupOrganizationsUtilService.create({ name, slug }, user, manager);
} else {
if (!isInviteRedirect) throw new UnauthorizedException('User is not assigned to any workspaces');
}

View file

@ -149,7 +149,7 @@ export class AuthUtilService implements IAuthUtilService {
if (!user && allowPersonalWorkspace) {
const { name, slug } = generateNextNameAndSlug('My workspace');
defaultOrganization = await this.setupOrganizationsUtilService.create(name, slug, null, manager);
defaultOrganization = await this.setupOrganizationsUtilService.create({ name, slug }, null, manager);
}
const { source, status } = getUserStatusAndSource(lifecycleEvents.USER_SSO_ACTIVATE, sso);

View file

@ -26,7 +26,7 @@ export class LoginConfigsService implements ILoginConfigsService {
throw new NotFoundException();
}
if (!organizationId) {
const result = this.loginConfigsUtilService.constructSSOConfigs();
const result = await this.loginConfigsUtilService.constructSSOConfigs();
return result;
}

View file

@ -49,7 +49,6 @@ export class OnboardingController implements IOnboardingController {
@InitFeature(FEATURE_KEY.SIGNUP)
@UseGuards(
SignupDisableGuard,
AllowPersonalWorkspaceGuard,
UserCountGuard,
EditorUserCountGuard,
FirstUserSignupDisableGuard,

View file

@ -26,6 +26,7 @@ export interface IOnboardingUtilService {
signingUpOrganization: Organization,
userParams: { firstName: string; lastName: string; password: string },
redirectTo?: string,
defaultWorkspace?: Organization,
manager?: EntityManager
): Promise<void>;
processOrganizationSignup(
@ -40,4 +41,10 @@ export interface IOnboardingUtilService {
organizationInviteUrl: string;
}>;
splitName(name: string): { firstName: string; lastName: string };
updateExistingUserDefaultWorkspace(
userParams: { password: string; firstName: string; lastName: string },
existingUser: User,
defaultWorkspace: Organization,
manager?: EntityManager
)
}

View file

@ -119,6 +119,9 @@ export class OnboardingService implements IOnboardingService {
const { firstName, lastName } = names;
const userParams = { email, password, firstName, lastName };
// Find the default workspace
const defaultWorkspace = await this.organizationRepository. getDefaultWorkspaceOfInstance();
if (existingUser) {
// Handling instance and workspace level signup for existing user
return await this.onboardingUtilService.whatIfTheSignUpIsAtTheWorkspaceLevel(
@ -126,9 +129,18 @@ export class OnboardingService implements IOnboardingService {
signingUpOrganization,
userParams,
redirectTo,
defaultWorkspace,
manager
);
} else {
if(defaultWorkspace && !signingUpOrganization) {
return await this.onboardingUtilService.createUserInDefaultWorkspace(
userParams,
defaultWorkspace,
redirectTo,
manager
);
}
return await this.onboardingUtilService.createUserOrPersonalWorkspace(
userParams,
existingUser,
@ -149,8 +161,7 @@ export class OnboardingService implements IOnboardingService {
const result = await dbTransactionWrap(async (manager: EntityManager) => {
// Create first organization
const organization = await this.organizationRepository.createOne(
workspace || 'My workspace',
'my-workspace',
{ name: workspace || 'My workspace', slug: 'my-workspace' },
manager
);
@ -226,7 +237,8 @@ export class OnboardingService implements IOnboardingService {
(await this.instanceSettingsUtilService.getSettings(INSTANCE_USER_SETTINGS.ALLOW_PERSONAL_WORKSPACE)) ===
'true';
if (!(allowPersonalWorkspace || organizationToken)) {
const defaultWorkspace = await this.organizationRepository.getDefaultWorkspaceOfInstance();
if (!(defaultWorkspace || allowPersonalWorkspace || organizationToken)) {
throw new BadRequestException('Invalid invitation link');
}
if (organizationToken) {
@ -251,7 +263,8 @@ export class OnboardingService implements IOnboardingService {
throw new BadRequestException('Please enter password');
}
if (allowPersonalWorkspace) {
const activateDefaultWorkspace = (defaultWorkspace && defaultWorkspace.id === user.defaultOrganizationId) || allowPersonalWorkspace;
if (activateDefaultWorkspace) {
// Getting default workspace
const defaultOrganizationUser: OrganizationUser = user.organizationUsers.find(
(ou) => ou.organizationId === user.defaultOrganizationId
@ -264,6 +277,14 @@ export class OnboardingService implements IOnboardingService {
// Activate default workspace
await this.organizationUsersUtilService.activateOrganization(defaultOrganizationUser, manager);
if(defaultWorkspace && defaultWorkspace.id === user.defaultOrganizationId){
const personalWorkspaces = await this.organizationUsersUtilService.personalWorkspaces(user.id);
for(const personalWorkspace of personalWorkspaces){
// if any personal workspace left. activate those
await this.organizationUsersUtilService.activateOrganization(personalWorkspace, manager);
}
}
if (workspaceName) {
const { slug } = generateNextNameAndSlug('My workspace');
await this.organizationRepository.updateOne(
@ -449,10 +470,10 @@ export class OnboardingService implements IOnboardingService {
onboarding_details: {
status: user.onboardingStatus,
password: isPasswordMandatory(user.source), // Should accept password if user is setting up first time
questions:
(this.configService.get<string>('ENABLE_ONBOARDING_QUESTIONS_FOR_ALL_SIGN_UPS') === 'true' &&
!organizationUser) || // Should ask onboarding questions if first user of the instance. If ENABLE_ONBOARDING_QUESTIONS_FOR_ALL_SIGN_UPS=true, then will ask questions to all signup users
(await this.userRepository.count({ where: { status: USER_STATUS.ACTIVE } })) === 0,
// questions:
// (this.configService.get<string>('ENABLE_ONBOARDING_QUESTIONS_FOR_ALL_SIGN_UPS') === 'true' &&
// !organizationUser) || // Should ask onboarding questions if first user of the instance. If ENABLE_ONBOARDING_QUESTIONS_FOR_ALL_SIGN_UPS=true, then will ask questions to all signup users
// (await this.userRepository.count({ where: { status: USER_STATUS.ACTIVE } })) === 0,
},
};
}
@ -686,8 +707,7 @@ export class OnboardingService implements IOnboardingService {
// Create first organization
const workspaceSlug = generateWorkspaceSlug(workspaceName || 'My workspace');
const organization = await this.setupOrganizationsUtilService.create(
workspaceName || 'My workspace',
workspaceSlug,
{ name: workspaceName || 'My workspace', slug: workspaceSlug },
null,
manager
);

View file

@ -151,6 +151,7 @@ export class OnboardingUtilService implements IOnboardingUtilService {
signingUpOrganization: Organization,
userParams: { firstName: string; lastName: string; password: string },
redirectTo?: string,
defaultWorkspace?: Organization,
manager?: EntityManager
) => {
return dbTransactionWrap(async (manager: EntityManager) => {
@ -251,19 +252,28 @@ export class OnboardingUtilService implements IOnboardingUtilService {
case hasWorkspaceInviteButUserWantsInstanceSignup: {
const firstTimeSignup = ![SOURCE.SIGNUP, SOURCE.WORKSPACE_SIGNUP].includes(existingUser.source as SOURCE);
if (firstTimeSignup) {
if(defaultWorkspace) {
return this.updateExistingUserDefaultWorkspace({
password,
firstName,
lastName
},existingUser, defaultWorkspace, manager);
}
/* Invite user doing instance signup. So reset name fields and set password */
let defaultOrganizationId = existingUser.defaultOrganizationId;
const isPersonalWorkspaceAllowed =
(await this.instanceSettingsUtilService.getSettings(INSTANCE_USER_SETTINGS.ALLOW_PERSONAL_WORKSPACE)) ===
'true';
if (!existingUser.defaultOrganizationId && isPersonalWorkspaceAllowed) {
if (!existingUser.defaultOrganizationId && isPersonalWorkspaceAllowed) {
const personalWorkspaces = await this.organizationUsersUtilService.personalWorkspaces(existingUser.id);
if (personalWorkspaces.length) {
defaultOrganizationId = personalWorkspaces[0].organizationId;
} else {
/* Create a personal workspace for the user */
const { name, slug } = generateNextNameAndSlug('My workspace');
const defaultOrganization = await this.organizationRepository.createOne(name, slug, manager);
const defaultOrganization = await this.organizationRepository.createOne({ name, slug }, manager);
defaultOrganizationId = defaultOrganization.id;
await this.organizationUserRepository.createOne(existingUser, defaultOrganization, true, manager);
}
@ -272,7 +282,6 @@ export class OnboardingUtilService implements IOnboardingUtilService {
userId: existingUser.id,
});
}
await this.userRepository.updateOne(
existingUser.id,
{
@ -398,7 +407,7 @@ export class OnboardingUtilService implements IOnboardingUtilService {
let personalWorkspace: Organization;
if (isPersonalWorkspaceEnabled) {
const { name, slug } = generateNextNameAndSlug('My workspace');
personalWorkspace = await this.setupOrganizationsUtilService.create(name, slug, null, manager);
personalWorkspace = await this.setupOrganizationsUtilService.create({ name, slug }, null, manager);
}
const organizationRole = personalWorkspace ? USER_ROLE.ADMIN : USER_ROLE.END_USER;
@ -604,4 +613,130 @@ export class OnboardingUtilService implements IOnboardingUtilService {
manager
);
}
createUserInDefaultWorkspace = async (
userParams: { email: string; password: string; firstName: string; lastName: string },
defaultWorkspace: Organization,
redirectTo?: string,
manager?: EntityManager
) => {
return await dbTransactionWrap(async (manager: EntityManager) => {
const { email, password, firstName, lastName } = userParams;
if (!defaultWorkspace) {
throw new Error('No default workspace found in the instance');
}
// Create user with end-user role in default workspace
const lifeCycleParms = getUserStatusAndSource(lifecycleEvents.USER_SIGN_UP);
const user = await this.create(
{
email,
password,
...(firstName && { firstName }),
...(lastName && { lastName }),
...lifeCycleParms,
},
defaultWorkspace.id,
USER_ROLE.END_USER,
null,
true,
null,
manager,
false
);
// Create organization user entry
await this.organizationUserRepository.createOne(
user,
defaultWorkspace,
true,
manager,
WORKSPACE_USER_SOURCE.SIGNUP
);
// Validate license
await this.licenseUserService.validateUser(manager);
// Send welcome email
this.eventEmitter.emit('emailEvent', {
type: EMAIL_EVENTS.SEND_WELCOME_EMAIL,
payload: {
to: user.email,
name: user.firstName,
invitationtoken: user.invitationToken,
},
});
return {};
}, manager);
};
updateExistingUserDefaultWorkspace = async (
userParams: { password: string; firstName: string; lastName: string },
existingUser: User,
defaultWorkspace: Organization,
manager?: EntityManager
) => {
return await dbTransactionWrap(async (manager: EntityManager) => {
const { password, firstName, lastName } = userParams;
// Create organization user entry if not exists
const existingOrgUser = await this.organizationUserRepository.findOne({
where: {
userId: existingUser.id,
organizationId: defaultWorkspace.id,
}
});
if(existingOrgUser){
throw new NotAcceptableException(
'The user is already registered. Please check your inbox for the activation link'
);
}
// Update user's default organization ID
await this.userRepository.updateOne(
existingUser.id,
{
password,
firstName,
lastName,
source: SOURCE.SIGNUP,
defaultOrganizationId: defaultWorkspace.id,
},
manager
);
await this.organizationUserRepository.createOne(
existingUser,
defaultWorkspace,
true,
manager,
WORKSPACE_USER_SOURCE.SIGNUP
);
// Add end-user role in default workspace if not already present
await this.rolesUtilService.addUserRole(
defaultWorkspace.id,
{ role: USER_ROLE.END_USER, userId: existingUser.id },
manager
);
// Validate license
await this.licenseUserService.validateUser(manager);
// send welcome email
this.eventEmitter.emit('emailEvent', {
type: EMAIL_EVENTS.SEND_WELCOME_EMAIL,
payload: {
to: existingUser.email,
name: existingUser.firstName,
invitationtoken: existingUser.invitationToken,
},
});
return {};
}, manager);
};
}

View file

@ -7,6 +7,7 @@ import {
lifecycleEvents,
USER_STATUS,
USER_TYPE,
WORKSPACE_USER_SOURCE,
WORKSPACE_USER_STATUS,
} from '@modules/users/constants/lifecycle';
import { BadRequestException, ConflictException, Injectable } from '@nestjs/common';
@ -212,7 +213,7 @@ export class OrganizationUsersUtilService implements IOrganizationUsersUtilServi
async createDefaultOrganization(manager: EntityManager) {
const { name, slug } = generateNextNameAndSlug('My workspace');
return await this.setupOrganizationsUtilService.create(name, slug, null, manager);
return await this.setupOrganizationsUtilService.create({ name, slug }, null, manager);
}
addUserAsAdmin(userId: string, organizationId: string, manager: EntityManager) {
@ -343,7 +344,7 @@ export class OrganizationUsersUtilService implements IOrganizationUsersUtilServi
async personalWorkspaces(userId: string): Promise<OrganizationUser[]> {
const personalWorkspaces: Partial<OrganizationUser[]> = await this.organizationUsersRepository.find({
select: ['organizationId', 'invitationToken'],
select: ['organizationId', 'invitationToken', 'id'],
where: { userId },
});
const personalWorkspaceArray: OrganizationUser[] = [];
@ -578,4 +579,41 @@ export class OrganizationUsersUtilService implements IOrganizationUsersUtilServi
user.organizationUserSource = organizationUser.source;
return user;
}
addUserToWorkspace = async (
user: User,
workspace: Organization,
manager?: EntityManager
) => {
return await dbTransactionWrap(async (manager: EntityManager) => {
// Create organization user entry if not exists
let existingOrgUser = await this.organizationUsersRepository.findOne({
where: {
userId: user.id,
organizationId: workspace.id,
}
});
if(existingOrgUser){
return existingOrgUser;
}
const organizationUser = await this.organizationUsersRepository.createOne(
user,
workspace,
true,
manager,
WORKSPACE_USER_SOURCE.SIGNUP
);
// Add end-user role in default workspace if not already present
await this.rolesUtilService.addUserRole(
workspace.id,
{ role: USER_ROLE.END_USER, userId: user.id },
manager
);
return organizationUser;
}, manager);
};
}

View file

@ -44,7 +44,7 @@ export class FeatureAbilityFactory extends AbilityFactory<FEATURE_KEY, Subjects>
can([FEATURE_KEY.UPDATE, FEATURE_KEY.GET, FEATURE_KEY.CHECK_UNIQUE], Organization);
}
if (superAdmin) {
can([FEATURE_KEY.WORKSPACE_STATUS_UPDATE], Organization);
can([FEATURE_KEY.WORKSPACE_STATUS_UPDATE, FEATURE_KEY.SET_DEFAULT], Organization);
}
}
}

View file

@ -14,5 +14,6 @@ export const FEATURES: FeaturesConfig = {
[FEATURE_KEY.CHECK_UNIQUE_ONBOARDING]: {
isPublic: true,
},
[FEATURE_KEY.SET_DEFAULT]: {},
},
};

View file

@ -26,4 +26,5 @@ export enum FEATURE_KEY {
CHECK_UNIQUE = 'check_unique',
CREATE = 'create',
CHECK_UNIQUE_ONBOARDING = 'check_unique_onboarding',
SET_DEFAULT = 'set_default',
}

View file

@ -1,4 +1,4 @@
import { Body, Controller, Get, Patch, UseGuards, Query, Param } from '@nestjs/common';
import { Body, Controller, Get, Patch, UseGuards, Query, Param, NotImplementedException } from '@nestjs/common';
import { OrganizationsService } from '@modules/organizations/service';
import { decamelizeKeys } from 'humps';
import { User } from '@modules/app/decorators/user.decorator';
@ -17,7 +17,7 @@ import { OrganizationAuthGuard } from '@modules/session/guards/organization-auth
@Controller('organizations')
@InitModule(MODULES.ORGANIZATIONS)
export class OrganizationsController implements IOrganizationsController {
constructor(private organizationsService: OrganizationsService) {}
constructor(protected organizationsService: OrganizationsService) {}
@InitFeature(FEATURE_KEY.GET)
// TODO: Change to jwt auth guard - check why we need OrganizationAuthGuard here
@ -41,6 +41,15 @@ export class OrganizationsController implements IOrganizationsController {
await this.organizationsService.updateOrganizationNameAndSlug(user.organizationId, organizationUpdateDto);
return;
}
@InitFeature(FEATURE_KEY.SET_DEFAULT)
@UseGuards(JwtAuthGuard, FeatureAbilityGuard)
@Patch(':id/set-default')
async setDefaultWorkspace(@Param('id') id: string) {
await this.organizationsService.setDefaultWorkspace(id);
return;
}
// Note : This endpoint is used for archive/unarchive workspaces.
@InitFeature(FEATURE_KEY.WORKSPACE_STATUS_UPDATE)
@UseGuards(JwtAuthGuard)

View file

@ -11,4 +11,6 @@ export interface IOrganizationsController {
checkWorkspaceUnique(name: string, slug: string): Promise<void>;
checkUniqueWorkspaceName(name: string): Promise<void>;
setDefaultWorkspace(id: string): Promise<void>;
}

View file

@ -1,5 +1,6 @@
import { Organization } from 'src/entities/organization.entity';
import { OrganizationUpdateDto, OrganizationStatusUpdateDto } from '@modules/organizations/dto';
import { EntityManager } from 'typeorm';
export interface IOrganizationsService {
fetchOrganizations(
@ -15,4 +16,8 @@ export interface IOrganizationsService {
updateOrganizationStatus(organizationId: string, updatableData: OrganizationStatusUpdateDto): Promise<Organization>;
checkWorkspaceUniqueness(name: string, slug: string): Promise<void>;
checkWorkspaceNameUniqueness(name: string): Promise<void>;
setDefaultWorkspace(organizationId: string, manager?: EntityManager): Promise<void>;
}

View file

@ -7,6 +7,7 @@ import { catchDbException, isSuperAdmin } from '@helpers/utils.helper';
import { ConfigScope, SSOType } from '@entities/sso_config.entity';
import { WORKSPACE_STATUS, WORKSPACE_USER_STATUS } from '@modules/users/constants/lifecycle';
import { CONSTRAINTS } from './constants';
import { OrganizationInputs } from '@modules/setup-organization/types/organization-inputs';
@Injectable()
export class OrganizationRepository extends Repository<Organization> {
@ -106,7 +107,8 @@ export class OrganizationRepository extends Repository<Organization> {
}, manager);
}
createOne(name: string, slug: string, manager?: EntityManager): Promise<any> {
createOne(organizationInputs: OrganizationInputs, manager?: EntityManager): Promise<any> {
const { name, slug, isDefault } = organizationInputs;
return dbTransactionWrap((manager: EntityManager) => {
return catchDbException(() => {
return manager.save(
@ -120,6 +122,7 @@ export class OrganizationRepository extends Repository<Organization> {
],
name,
slug,
isDefault,
createdAt: new Date(),
updatedAt: new Date(),
})
@ -201,4 +204,27 @@ export class OrganizationRepository extends Repository<Organization> {
});
});
}
async getDefaultWorkspaceOfInstance(): Promise<Organization>{
return dbTransactionWrap(async (manager: EntityManager) => {
try {
return await manager.findOneOrFail(Organization, {
where: { isDefault: true },
});
} catch (error) {
console.error('No default workspace in this instance');
return null;
}
});
}
async changeDefaultWorkspace(organizationId: string, manager?: EntityManager): Promise<void> {
return await dbTransactionWrap(async (manager: EntityManager) => {
// First, unset any existing default workspace
await manager.update(Organization, { isDefault: true }, { isDefault: false });
// Then set the new default workspace
await manager.update(Organization, { id: organizationId }, { isDefault: true });
}, manager || this.manager);
}
}

View file

@ -1,4 +1,4 @@
import { ConflictException, Injectable, NotAcceptableException } from '@nestjs/common';
import { ConflictException, Injectable, NotAcceptableException, NotImplementedException } from '@nestjs/common';
import { Organization } from 'src/entities/organization.entity';
import { isSuperAdmin } from 'src/helpers/utils.helper';
import { dbTransactionWrap } from 'src/helpers/database.helper';
@ -51,6 +51,11 @@ export class OrganizationsService implements IOrganizationsService {
updatableData: OrganizationStatusUpdateDto
): Promise<Organization> {
return await dbTransactionWrap(async (manager: EntityManager) => {
const organization = await this.organizationRepository.findOne({ where: { id: organizationId } });
if (organization.isDefault) {
throw new NotAcceptableException('Default workspace cannot be archived');
}
await this.organizationRepository.updateOne(organizationId, updatableData, manager);
if (updatableData.status === WORKSPACE_STATUS.ACTIVE) {
await this.licenseOrganizationService.validateOrganization(manager); //Check for only unarchiving
@ -85,4 +90,8 @@ export class OrganizationsService implements IOrganizationsService {
if (result) throw new ConflictException('Workspace name must be unique');
return;
}
async setDefaultWorkspace(organizationId: string, manager?: EntityManager): Promise<void> {
throw new NotImplementedException('This feature is only available in Enterprise Edition');
}
}

View file

@ -9,6 +9,7 @@ interface Features {
[FEATURE_KEY.CREATE]: FeatureConfig;
[FEATURE_KEY.CHECK_UNIQUE_ONBOARDING]: FeatureConfig;
[FEATURE_KEY.WORKSPACE_STATUS_UPDATE]: FeatureConfig;
[FEATURE_KEY.SET_DEFAULT]: FeatureConfig;
}
export interface FeaturesConfig {

View file

@ -368,8 +368,8 @@ export class SessionUtilService {
async #onboardingFlags(user: User) {
let isFirstUserOnboardingCompleted = true;
let isOnboardingCompleted = true;
const isOnboardingQuestionsEnabled =
this.configService.get<string>('ENABLE_ONBOARDING_QUESTIONS_FOR_ALL_SIGN_UPS') === 'true';
// const isOnboardingQuestionsEnabled =
// this.configService.get<string>('ENABLE_ONBOARDING_QUESTIONS_FOR_ALL_SIGN_UPS') === 'true';
const instanceUsersCount = await this.userRepository.count({
where: { status: USER_STATUS.ACTIVE },
@ -383,14 +383,14 @@ export class SessionUtilService {
}
/* Signed up user check */
if (
instanceUsersCount > 1 &&
isOnboardingQuestionsEnabled &&
user.onboardingStatus !== OnboardingStatus.ONBOARDING_COMPLETED
) {
/* Signed up user went through onboarding flow, didn't complete */
isOnboardingCompleted = false;
}
// if (
// instanceUsersCount > 1 &&
// isOnboardingQuestionsEnabled &&
// user.onboardingStatus !== OnboardingStatus.ONBOARDING_COMPLETED
// ) {
// /* Signed up user went through onboarding flow, didn't complete */
// isOnboardingCompleted = false;
// }
return { isFirstUserOnboardingCompleted, isOnboardingCompleted };
}

View file

@ -29,8 +29,7 @@ export class SetupOrganizationsController implements ISetupOrganizationsControll
@Res({ passthrough: true }) response: Response
) {
const result = await this.setupOrganizationsService.create(
organizationCreateDto.name,
organizationCreateDto.slug,
{ name: organizationCreateDto.name, slug: organizationCreateDto.slug },
user
);

View file

@ -1,7 +1,8 @@
import { User } from 'src/entities/user.entity';
import { Organization } from 'src/entities/organization.entity';
import { EntityManager } from 'typeorm';
import { OrganizationInputs } from '../types/organization-inputs';
export interface ISetupOrganizationsService {
create(name: string, slug: string, user?: User, manager?: EntityManager): Promise<Organization>;
create(organizationInputs: OrganizationInputs, user?: User, manager?: EntityManager): Promise<Organization>;
}

View file

@ -1,7 +1,8 @@
import { User } from 'src/entities/user.entity';
import { EntityManager } from 'typeorm';
import { Organization } from '@entities/organization.entity';
import { OrganizationInputs } from '../types/organization-inputs';
export interface ISetupOrganizationsUtilService {
create(name: string, slug: string, user?: User, manager?: EntityManager): Promise<Organization>;
create(organizationInputs: OrganizationInputs, user?: User, manager?: EntityManager): Promise<Organization>;
}

View file

@ -4,12 +4,13 @@ import { User } from 'src/entities/user.entity';
import { EntityManager } from 'typeorm';
import { SetupOrganizationsUtilService } from './util.service';
import { ISetupOrganizationsService } from './interfaces/IService';
import { OrganizationInputs } from './types/organization-inputs';
@Injectable()
export class SetupOrganizationsService implements ISetupOrganizationsService {
constructor(protected readonly setupOrganizationsUtilService: SetupOrganizationsUtilService) {}
async create(name: string, slug: string, user?: User, manager?: EntityManager): Promise<Organization> {
return this.setupOrganizationsUtilService.create(name, slug, user, manager);
async create(organizationInputs: OrganizationInputs, user?: User, manager?: EntityManager): Promise<Organization> {
return this.setupOrganizationsUtilService.create(organizationInputs, user, manager);
}
}

View file

@ -0,0 +1,5 @@
export interface OrganizationInputs {
name: string;
slug: string;
isDefault?: boolean;
}

View file

@ -15,6 +15,7 @@ import { OrganizationUsersRepository } from '@modules/organization-users/reposit
import { SampleDataSourceService } from '@modules/data-sources/services/sample-ds.service';
import { ISetupOrganizationsUtilService } from './interfaces/IUtilService';
import { TooljetDbTableOperationsService } from '@modules/tooljet-db/services/tooljet-db-table-operations.service';
import { OrganizationInputs } from './types/organization-inputs';
@Injectable()
export class SetupOrganizationsUtilService implements ISetupOrganizationsUtilService {
@ -31,9 +32,9 @@ export class SetupOrganizationsUtilService implements ISetupOrganizationsUtilSer
protected readonly organizationUserRepository: OrganizationUsersRepository
) {}
async create(name: string, slug: string, user?: User, manager?: EntityManager): Promise<Organization> {
async create(organizationInputs: OrganizationInputs, user?: User, manager?: EntityManager): Promise<Organization> {
return await dbTransactionWrap(async (manager: EntityManager) => {
const organization = await this.organizationRepository.createOne(name, slug, manager);
const organization = await this.organizationRepository.createOne(organizationInputs, manager);
await this.appEnvironmentUtilService.createDefaultEnvironments(organization.id, manager);
await this.groupPermissionUtilService.createDefaultGroups(organization.id, manager);