diff --git a/.github/workflows/render-preview-deploy.yml b/.github/workflows/render-preview-deploy.yml index fda3981a3e..c5cb025cba 100644 --- a/.github/workflows/render-preview-deploy.yml +++ b/.github/workflows/render-preview-deploy.yml @@ -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": [], diff --git a/.version b/.version index 92536a9e48..171a6a93d6 100644 --- a/.version +++ b/.version @@ -1 +1 @@ -3.12.0 +3.12.1 diff --git a/cypress-tests/cypress-platform.config.js b/cypress-tests/cypress-platform.config.js index a53733a70d..b565a0c1d1 100644 --- a/cypress-tests/cypress-platform.config.js +++ b/cypress-tests/cypress-platform.config.js @@ -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, diff --git a/cypress-tests/cypress/commands/commands.js b/cypress-tests/cypress/commands/commands.js index 0acb76c866..d242eb1895 100644 --- a/cypress-tests/cypress/commands/commands.js +++ b/cypress-tests/cypress/commands/commands.js @@ -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); diff --git a/cypress-tests/cypress/constants/texts/version.js b/cypress-tests/cypress/constants/texts/version.js index 4f640db63a..ed92ea4c29 100644 --- a/cypress-tests/cypress/constants/texts/version.js +++ b/cypress-tests/cypress/constants/texts/version.js @@ -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 )}?`; }, diff --git a/cypress-tests/cypress/e2e/happyPath/platform/ceTestcases/apps/appExport.cy.js b/cypress-tests/cypress/e2e/happyPath/platform/ceTestcases/apps/appExport.cy.js index e97269cf39..b1f3733c73 100644 --- a/cypress-tests/cypress/e2e/happyPath/platform/ceTestcases/apps/appExport.cy.js +++ b/cypress-tests/cypress/e2e/happyPath/platform/ceTestcases/apps/appExport.cy.js @@ -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}`); diff --git a/cypress-tests/cypress/e2e/happyPath/platform/ceTestcases/apps/appImport.cy.js b/cypress-tests/cypress/e2e/happyPath/platform/ceTestcases/apps/appImport.cy.js index a6b068d137..2bd1ccf51e 100644 --- a/cypress-tests/cypress/e2e/happyPath/platform/ceTestcases/apps/appImport.cy.js +++ b/cypress-tests/cypress/e2e/happyPath/platform/ceTestcases/apps/appImport.cy.js @@ -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(); diff --git a/cypress-tests/cypress/e2e/happyPath/platform/ceTestcases/apps/appSlug.cy.js b/cypress-tests/cypress/e2e/happyPath/platform/ceTestcases/apps/appSlug.cy.js index b3e3f975dc..b65d54eac6 100644 --- a/cypress-tests/cypress/e2e/happyPath/platform/ceTestcases/apps/appSlug.cy.js +++ b/cypress-tests/cypress/e2e/happyPath/platform/ceTestcases/apps/appSlug.cy.js @@ -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(); diff --git a/cypress-tests/cypress/e2e/happyPath/platform/ceTestcases/apps/privateAndpublicApps.skip.js b/cypress-tests/cypress/e2e/happyPath/platform/ceTestcases/apps/privateAndpublicApps.cy.js similarity index 96% rename from cypress-tests/cypress/e2e/happyPath/platform/ceTestcases/apps/privateAndpublicApps.skip.js rename to cypress-tests/cypress/e2e/happyPath/platform/ceTestcases/apps/privateAndpublicApps.cy.js index fdd6acfe80..d6483fa400 100644 --- a/cypress-tests/cypress/e2e/happyPath/platform/ceTestcases/apps/privateAndpublicApps.skip.js +++ b/cypress-tests/cypress/e2e/happyPath/platform/ceTestcases/apps/privateAndpublicApps.cy.js @@ -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(); diff --git a/cypress-tests/cypress/e2e/happyPath/platform/ceTestcases/apps/version.skip.js b/cypress-tests/cypress/e2e/happyPath/platform/ceTestcases/apps/version.cy.js similarity index 98% rename from cypress-tests/cypress/e2e/happyPath/platform/ceTestcases/apps/version.skip.js rename to cypress-tests/cypress/e2e/happyPath/platform/ceTestcases/apps/version.cy.js index f97962d910..c0f6064564 100644 --- a/cypress-tests/cypress/e2e/happyPath/platform/ceTestcases/apps/version.skip.js +++ b/cypress-tests/cypress/e2e/happyPath/platform/ceTestcases/apps/version.cy.js @@ -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, diff --git a/cypress-tests/cypress/e2e/happyPath/platform/commonTestcases/workspace/appCreate.skip.js b/cypress-tests/cypress/e2e/happyPath/platform/commonTestcases/workspace/appCreate.cy.js similarity index 100% rename from cypress-tests/cypress/e2e/happyPath/platform/commonTestcases/workspace/appCreate.skip.js rename to cypress-tests/cypress/e2e/happyPath/platform/commonTestcases/workspace/appCreate.cy.js diff --git a/cypress-tests/cypress/e2e/happyPath/platform/commonTestcases/workspace/dashboard.skip.js b/cypress-tests/cypress/e2e/happyPath/platform/commonTestcases/workspace/dashboard.cy.js similarity index 63% rename from cypress-tests/cypress/e2e/happyPath/platform/commonTestcases/workspace/dashboard.skip.js rename to cypress-tests/cypress/e2e/happyPath/platform/commonTestcases/workspace/dashboard.cy.js index 64a05d00c7..fb8e932973 100644 --- a/cypress-tests/cypress/e2e/happyPath/platform/commonTestcases/workspace/dashboard.skip.js +++ b/cypress-tests/cypress/e2e/happyPath/platform/commonTestcases/workspace/dashboard.cy.js @@ -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(); }); diff --git a/cypress-tests/cypress/e2e/happyPath/platform/commonTestcases/workspace/groups/basicPermissions.skip.js b/cypress-tests/cypress/e2e/happyPath/platform/commonTestcases/workspace/groups/basicPermissions.cy.js similarity index 100% rename from cypress-tests/cypress/e2e/happyPath/platform/commonTestcases/workspace/groups/basicPermissions.skip.js rename to cypress-tests/cypress/e2e/happyPath/platform/commonTestcases/workspace/groups/basicPermissions.cy.js diff --git a/cypress-tests/cypress/e2e/happyPath/platform/commonTestcases/workspace/groups/permissions.skip.js b/cypress-tests/cypress/e2e/happyPath/platform/commonTestcases/workspace/groups/permissions.cy.js similarity index 99% rename from cypress-tests/cypress/e2e/happyPath/platform/commonTestcases/workspace/groups/permissions.skip.js rename to cypress-tests/cypress/e2e/happyPath/platform/commonTestcases/workspace/groups/permissions.cy.js index 22b6c6b6a5..bfa5806939 100644 --- a/cypress-tests/cypress/e2e/happyPath/platform/commonTestcases/workspace/groups/permissions.skip.js +++ b/cypress-tests/cypress/e2e/happyPath/platform/commonTestcases/workspace/groups/permissions.cy.js @@ -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); diff --git a/cypress-tests/cypress/e2e/happyPath/platform/externalApi/apiUsers.cy.js b/cypress-tests/cypress/e2e/happyPath/platform/eeTestcases/externalApi/apiUsers.cy.js similarity index 100% rename from cypress-tests/cypress/e2e/happyPath/platform/externalApi/apiUsers.cy.js rename to cypress-tests/cypress/e2e/happyPath/platform/eeTestcases/externalApi/apiUsers.cy.js diff --git a/cypress-tests/cypress/e2e/happyPath/platform/externalApi/appImportAndExportAPI.cy.js b/cypress-tests/cypress/e2e/happyPath/platform/eeTestcases/externalApi/appImportAndExportAPI.cy.js similarity index 100% rename from cypress-tests/cypress/e2e/happyPath/platform/externalApi/appImportAndExportAPI.cy.js rename to cypress-tests/cypress/e2e/happyPath/platform/eeTestcases/externalApi/appImportAndExportAPI.cy.js diff --git a/cypress-tests/cypress/e2e/happyPath/platform/ceTestcases/userFlow/firstUserOnboarding.cy.js b/cypress-tests/cypress/e2e/happyPath/platform/firstUser/firstUserOnboarding.cy.js similarity index 100% rename from cypress-tests/cypress/e2e/happyPath/platform/ceTestcases/userFlow/firstUserOnboarding.cy.js rename to cypress-tests/cypress/e2e/happyPath/platform/firstUser/firstUserOnboarding.cy.js diff --git a/cypress-tests/cypress/support/utils/common.js b/cypress-tests/cypress/support/utils/common.js index 0f54553472..7a8c55ffcf 100644 --- a/cypress-tests/cypress/support/utils/common.js +++ b/cypress-tests/cypress/support/utils/common.js @@ -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 = () => { diff --git a/cypress-tests/cypress/support/utils/dataSource.js b/cypress-tests/cypress/support/utils/dataSource.js index 6f17004409..4c1446636e 100644 --- a/cypress-tests/cypress/support/utils/dataSource.js +++ b/cypress-tests/cypress/support/utils/dataSource.js @@ -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, diff --git a/cypress-tests/cypress/support/utils/version.js b/cypress-tests/cypress/support/utils/version.js index 77b7d8b0e8..b15d84f45b 100644 --- a/cypress-tests/cypress/support/utils/version.js +++ b/cypress-tests/cypress/support/utils/version.js @@ -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!" ); }; diff --git a/docker/ce-preview.Dockerfile b/docker/ce-preview.Dockerfile index e1a4b75c01..0c481e13a3 100644 --- a/docker/ce-preview.Dockerfile +++ b/docker/ce-preview.Dockerfile @@ -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" \ diff --git a/frontend/.version b/frontend/.version index 92536a9e48..171a6a93d6 100644 --- a/frontend/.version +++ b/frontend/.version @@ -1 +1 @@ -3.12.0 +3.12.1 diff --git a/frontend/ee b/frontend/ee index 381a529935..280578f99c 160000 --- a/frontend/ee +++ b/frontend/ee @@ -1 +1 @@ -Subproject commit 381a529935fcaf82b3d5d55d41bf16c66e6a5b98 +Subproject commit 280578f99c45224428f78ee16285b62f4c3631fd diff --git a/frontend/src/_services/organization.service.js b/frontend/src/_services/organization.service.js index 2075e5d9de..481e8576c4 100644 --- a/frontend/src/_services/organization.service.js +++ b/frontend/src/_services/organization.service.js @@ -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); +} diff --git a/frontend/src/_styles/theme.scss b/frontend/src/_styles/theme.scss index 1162f50687..b77c83ba30 100644 --- a/frontend/src/_styles/theme.scss +++ b/frontend/src/_styles/theme.scss @@ -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 { diff --git a/server/.version b/server/.version index 92536a9e48..171a6a93d6 100644 --- a/server/.version +++ b/server/.version @@ -1 +1 @@ -3.12.0 +3.12.1 diff --git a/server/data-migrations/1740401100000-SetDefaultWorkspace.ts b/server/data-migrations/1740401100000-SetDefaultWorkspace.ts new file mode 100644 index 0000000000..c337155fd8 --- /dev/null +++ b/server/data-migrations/1740401100000-SetDefaultWorkspace.ts @@ -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 { + 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 { + if (getTooljetEdition() !== TOOLJET_EDITIONS.EE) { + return; + } + + // Unset all default workspaces + await queryRunner.query(` + UPDATE organizations + SET is_default = false; + `); + } +} diff --git a/server/ee b/server/ee index cec44e5562..69bdefb1f3 160000 --- a/server/ee +++ b/server/ee @@ -1 +1 @@ -Subproject commit cec44e5562b07caad5797ffc901bb097d51eed94 +Subproject commit 69bdefb1f3f1d35bd6e7231e50799ff10a77a60f diff --git a/server/migrations/1740401000000-AddIsDefaultToOrganizations.ts b/server/migrations/1740401000000-AddIsDefaultToOrganizations.ts new file mode 100644 index 0000000000..6df32a7117 --- /dev/null +++ b/server/migrations/1740401000000-AddIsDefaultToOrganizations.ts @@ -0,0 +1,30 @@ +import { MigrationInterface, QueryRunner, TableColumn } from 'typeorm'; + +export class AddIsDefaultToOrganizations1740401000000 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + // 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 { + // 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'); + } +} diff --git a/server/src/entities/organization.entity.ts b/server/src/entities/organization.entity.ts index c1f8cb211a..865b124dcb 100644 --- a/server/src/entities/organization.entity.ts +++ b/server/src/entities/organization.entity.ts @@ -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; diff --git a/server/src/helpers/utils.helper.ts b/server/src/helpers/utils.helper.ts index 0eb0498812..0054a4a719 100644 --- a/server/src/helpers/utils.helper.ts +++ b/server/src/helpers/utils.helper.ts @@ -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] || ''; } diff --git a/server/src/modules/auth/oauth/service.ts b/server/src/modules/auth/oauth/service.ts index 79b2c41fa2..438673c29e 100644 --- a/server/src/modules/auth/oauth/service.ts +++ b/server/src/modules/auth/oauth/service.ts @@ -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 }, diff --git a/server/src/modules/auth/service.ts b/server/src/modules/auth/service.ts index 901dd99ad2..7c9fb3ad89 100644 --- a/server/src/modules/auth/service.ts +++ b/server/src/modules/auth/service.ts @@ -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'); } diff --git a/server/src/modules/auth/util.service.ts b/server/src/modules/auth/util.service.ts index 41d11c6bf9..e98402776e 100644 --- a/server/src/modules/auth/util.service.ts +++ b/server/src/modules/auth/util.service.ts @@ -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); diff --git a/server/src/modules/login-configs/service.ts b/server/src/modules/login-configs/service.ts index af7ed67143..34143b5fdd 100644 --- a/server/src/modules/login-configs/service.ts +++ b/server/src/modules/login-configs/service.ts @@ -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; } diff --git a/server/src/modules/onboarding/controller.ts b/server/src/modules/onboarding/controller.ts index b7d56938a3..a523e0e029 100644 --- a/server/src/modules/onboarding/controller.ts +++ b/server/src/modules/onboarding/controller.ts @@ -49,7 +49,6 @@ export class OnboardingController implements IOnboardingController { @InitFeature(FEATURE_KEY.SIGNUP) @UseGuards( SignupDisableGuard, - AllowPersonalWorkspaceGuard, UserCountGuard, EditorUserCountGuard, FirstUserSignupDisableGuard, diff --git a/server/src/modules/onboarding/interfaces/IUtilService.ts b/server/src/modules/onboarding/interfaces/IUtilService.ts index b0001e5b46..4a083de348 100644 --- a/server/src/modules/onboarding/interfaces/IUtilService.ts +++ b/server/src/modules/onboarding/interfaces/IUtilService.ts @@ -26,6 +26,7 @@ export interface IOnboardingUtilService { signingUpOrganization: Organization, userParams: { firstName: string; lastName: string; password: string }, redirectTo?: string, + defaultWorkspace?: Organization, manager?: EntityManager ): Promise; 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 + ) } diff --git a/server/src/modules/onboarding/service.ts b/server/src/modules/onboarding/service.ts index 3facd8357b..c05c9502d2 100644 --- a/server/src/modules/onboarding/service.ts +++ b/server/src/modules/onboarding/service.ts @@ -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('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('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 ); diff --git a/server/src/modules/onboarding/util.service.ts b/server/src/modules/onboarding/util.service.ts index 48c638b0b1..5c32ef89e8 100644 --- a/server/src/modules/onboarding/util.service.ts +++ b/server/src/modules/onboarding/util.service.ts @@ -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); + }; } diff --git a/server/src/modules/organization-users/util.service.ts b/server/src/modules/organization-users/util.service.ts index 618291b732..c040e2cff7 100644 --- a/server/src/modules/organization-users/util.service.ts +++ b/server/src/modules/organization-users/util.service.ts @@ -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 { const personalWorkspaces: Partial = 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); + }; } diff --git a/server/src/modules/organizations/ability/index.ts b/server/src/modules/organizations/ability/index.ts index acfd4a6078..49eaa256dd 100644 --- a/server/src/modules/organizations/ability/index.ts +++ b/server/src/modules/organizations/ability/index.ts @@ -44,7 +44,7 @@ export class FeatureAbilityFactory extends AbilityFactory 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); } } } diff --git a/server/src/modules/organizations/constants/feature.ts b/server/src/modules/organizations/constants/feature.ts index af1c847553..7276f855fb 100644 --- a/server/src/modules/organizations/constants/feature.ts +++ b/server/src/modules/organizations/constants/feature.ts @@ -14,5 +14,6 @@ export const FEATURES: FeaturesConfig = { [FEATURE_KEY.CHECK_UNIQUE_ONBOARDING]: { isPublic: true, }, + [FEATURE_KEY.SET_DEFAULT]: {}, }, }; diff --git a/server/src/modules/organizations/constants/index.ts b/server/src/modules/organizations/constants/index.ts index b829014d86..45b2f85044 100644 --- a/server/src/modules/organizations/constants/index.ts +++ b/server/src/modules/organizations/constants/index.ts @@ -26,4 +26,5 @@ export enum FEATURE_KEY { CHECK_UNIQUE = 'check_unique', CREATE = 'create', CHECK_UNIQUE_ONBOARDING = 'check_unique_onboarding', + SET_DEFAULT = 'set_default', } diff --git a/server/src/modules/organizations/controller.ts b/server/src/modules/organizations/controller.ts index 0dcc325048..2de2ed9936 100644 --- a/server/src/modules/organizations/controller.ts +++ b/server/src/modules/organizations/controller.ts @@ -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) diff --git a/server/src/modules/organizations/interfaces/IController.ts b/server/src/modules/organizations/interfaces/IController.ts index ad5feb0638..2654fe53db 100644 --- a/server/src/modules/organizations/interfaces/IController.ts +++ b/server/src/modules/organizations/interfaces/IController.ts @@ -11,4 +11,6 @@ export interface IOrganizationsController { checkWorkspaceUnique(name: string, slug: string): Promise; checkUniqueWorkspaceName(name: string): Promise; + + setDefaultWorkspace(id: string): Promise; } diff --git a/server/src/modules/organizations/interfaces/IService.ts b/server/src/modules/organizations/interfaces/IService.ts index aad08934d0..658418e4f5 100644 --- a/server/src/modules/organizations/interfaces/IService.ts +++ b/server/src/modules/organizations/interfaces/IService.ts @@ -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; checkWorkspaceUniqueness(name: string, slug: string): Promise; + + checkWorkspaceNameUniqueness(name: string): Promise; + + setDefaultWorkspace(organizationId: string, manager?: EntityManager): Promise; } diff --git a/server/src/modules/organizations/repository.ts b/server/src/modules/organizations/repository.ts index 240639dbe3..25ef5e7929 100644 --- a/server/src/modules/organizations/repository.ts +++ b/server/src/modules/organizations/repository.ts @@ -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 { @@ -106,7 +107,8 @@ export class OrganizationRepository extends Repository { }, manager); } - createOne(name: string, slug: string, manager?: EntityManager): Promise { + createOne(organizationInputs: OrganizationInputs, manager?: EntityManager): Promise { + const { name, slug, isDefault } = organizationInputs; return dbTransactionWrap((manager: EntityManager) => { return catchDbException(() => { return manager.save( @@ -120,6 +122,7 @@ export class OrganizationRepository extends Repository { ], name, slug, + isDefault, createdAt: new Date(), updatedAt: new Date(), }) @@ -201,4 +204,27 @@ export class OrganizationRepository extends Repository { }); }); } + + async getDefaultWorkspaceOfInstance(): Promise{ + 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 { + 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); + } } diff --git a/server/src/modules/organizations/service.ts b/server/src/modules/organizations/service.ts index b2a25570c9..8e8f715399 100644 --- a/server/src/modules/organizations/service.ts +++ b/server/src/modules/organizations/service.ts @@ -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 { 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 { + throw new NotImplementedException('This feature is only available in Enterprise Edition'); + } } diff --git a/server/src/modules/organizations/types/index.ts b/server/src/modules/organizations/types/index.ts index 1b3321eae6..ea5fc8c08f 100644 --- a/server/src/modules/organizations/types/index.ts +++ b/server/src/modules/organizations/types/index.ts @@ -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 { diff --git a/server/src/modules/session/util.service.ts b/server/src/modules/session/util.service.ts index 92a12e1d88..282a6179e5 100644 --- a/server/src/modules/session/util.service.ts +++ b/server/src/modules/session/util.service.ts @@ -368,8 +368,8 @@ export class SessionUtilService { async #onboardingFlags(user: User) { let isFirstUserOnboardingCompleted = true; let isOnboardingCompleted = true; - const isOnboardingQuestionsEnabled = - this.configService.get('ENABLE_ONBOARDING_QUESTIONS_FOR_ALL_SIGN_UPS') === 'true'; + // const isOnboardingQuestionsEnabled = + // this.configService.get('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 }; } diff --git a/server/src/modules/setup-organization/controller.ts b/server/src/modules/setup-organization/controller.ts index 7b377738fd..0b16e05b65 100644 --- a/server/src/modules/setup-organization/controller.ts +++ b/server/src/modules/setup-organization/controller.ts @@ -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 ); diff --git a/server/src/modules/setup-organization/interfaces/IService.ts b/server/src/modules/setup-organization/interfaces/IService.ts index 557b438223..4e463784c3 100644 --- a/server/src/modules/setup-organization/interfaces/IService.ts +++ b/server/src/modules/setup-organization/interfaces/IService.ts @@ -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; + create(organizationInputs: OrganizationInputs, user?: User, manager?: EntityManager): Promise; } diff --git a/server/src/modules/setup-organization/interfaces/IUtilService.ts b/server/src/modules/setup-organization/interfaces/IUtilService.ts index 0aaa5a9350..874ac52c35 100644 --- a/server/src/modules/setup-organization/interfaces/IUtilService.ts +++ b/server/src/modules/setup-organization/interfaces/IUtilService.ts @@ -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; + create(organizationInputs: OrganizationInputs, user?: User, manager?: EntityManager): Promise; } diff --git a/server/src/modules/setup-organization/service.ts b/server/src/modules/setup-organization/service.ts index 78a066e0bb..925e8a75eb 100644 --- a/server/src/modules/setup-organization/service.ts +++ b/server/src/modules/setup-organization/service.ts @@ -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 { - return this.setupOrganizationsUtilService.create(name, slug, user, manager); + async create(organizationInputs: OrganizationInputs, user?: User, manager?: EntityManager): Promise { + return this.setupOrganizationsUtilService.create(organizationInputs, user, manager); } } diff --git a/server/src/modules/setup-organization/types/organization-inputs.ts b/server/src/modules/setup-organization/types/organization-inputs.ts new file mode 100644 index 0000000000..af427cf785 --- /dev/null +++ b/server/src/modules/setup-organization/types/organization-inputs.ts @@ -0,0 +1,5 @@ +export interface OrganizationInputs { + name: string; + slug: string; + isDefault?: boolean; +} diff --git a/server/src/modules/setup-organization/util.service.ts b/server/src/modules/setup-organization/util.service.ts index e053e60b9e..8eac88ec33 100644 --- a/server/src/modules/setup-organization/util.service.ts +++ b/server/src/modules/setup-organization/util.service.ts @@ -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 { + async create(organizationInputs: OrganizationInputs, user?: User, manager?: EntityManager): Promise { 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);