mirror of
https://github.com/ToolJet/ToolJet
synced 2026-05-23 17:08:34 +00:00
Merge branch 'main' into fix/external-epi
This commit is contained in:
commit
81f039b481
56 changed files with 715 additions and 400 deletions
37
.github/workflows/render-preview-deploy.yml
vendored
37
.github/workflows/render-preview-deploy.yml
vendored
|
|
@ -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": [],
|
||||
|
|
|
|||
2
.version
2
.version
|
|
@ -1 +1 @@
|
|||
3.12.0
|
||||
3.12.1
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)}?`;
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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}`);
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
@ -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,
|
||||
|
|
@ -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();
|
||||
});
|
||||
|
|
@ -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);
|
||||
|
|
@ -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 = () => {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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!"
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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" \
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
3.12.0
|
||||
3.12.1
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
Subproject commit 381a529935fcaf82b3d5d55d41bf16c66e6a5b98
|
||||
Subproject commit 280578f99c45224428f78ee16285b62f4c3631fd
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
3.12.0
|
||||
3.12.1
|
||||
|
|
|
|||
66
server/data-migrations/1740401100000-SetDefaultWorkspace.ts
Normal file
66
server/data-migrations/1740401100000-SetDefaultWorkspace.ts
Normal 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
|
||||
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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] || '';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 },
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -49,7 +49,6 @@ export class OnboardingController implements IOnboardingController {
|
|||
@InitFeature(FEATURE_KEY.SIGNUP)
|
||||
@UseGuards(
|
||||
SignupDisableGuard,
|
||||
AllowPersonalWorkspaceGuard,
|
||||
UserCountGuard,
|
||||
EditorUserCountGuard,
|
||||
FirstUserSignupDisableGuard,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,5 +14,6 @@ export const FEATURES: FeaturesConfig = {
|
|||
[FEATURE_KEY.CHECK_UNIQUE_ONBOARDING]: {
|
||||
isPublic: true,
|
||||
},
|
||||
[FEATURE_KEY.SET_DEFAULT]: {},
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -26,4 +26,5 @@ export enum FEATURE_KEY {
|
|||
CHECK_UNIQUE = 'check_unique',
|
||||
CREATE = 'create',
|
||||
CHECK_UNIQUE_ONBOARDING = 'check_unique_onboarding',
|
||||
SET_DEFAULT = 'set_default',
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -11,4 +11,6 @@ export interface IOrganizationsController {
|
|||
checkWorkspaceUnique(name: string, slug: string): Promise<void>;
|
||||
|
||||
checkUniqueWorkspaceName(name: string): Promise<void>;
|
||||
|
||||
setDefaultWorkspace(id: string): Promise<void>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -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>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
export interface OrganizationInputs {
|
||||
name: string;
|
||||
slug: string;
|
||||
isDefault?: boolean;
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue