diff --git a/.github/workflows/cypress-appbuilder.yml b/.github/workflows/cypress-appbuilder.yml index b7d1177183..2cbff90ea9 100644 --- a/.github/workflows/cypress-appbuilder.yml +++ b/.github/workflows/cypress-appbuilder.yml @@ -144,7 +144,7 @@ jobs: run: docker buildx use mybuilder - name: Build docker image - run: docker buildx build --platform=linux/amd64 -f docker/production.Dockerfile . -t tooljet/tj-osv:cypress + run: docker buildx build --platform=linux/amd64 -f docker/production.Dockerfile . -t tooljet/tj-osv:cypressplaform - name: Set up environment variables run: | diff --git a/.github/workflows/cypress-marketplace.yml b/.github/workflows/cypress-marketplace.yml index 79c3ad776b..b2c138ff6c 100644 --- a/.github/workflows/cypress-marketplace.yml +++ b/.github/workflows/cypress-marketplace.yml @@ -38,7 +38,7 @@ jobs: run: docker buildx use mybuilder - name: Build docker image - run: docker buildx build --platform=linux/amd64 -f docker/production.Dockerfile . -t tooljet/tj-osv:cypress + run: docker buildx build --platform=linux/amd64 -f docker/production.Dockerfile . -t tooljet/tj-osv:cypressplaform - name: Set up environment variables run: | @@ -131,7 +131,7 @@ jobs: run: docker buildx use mybuilder - name: Build docker image - run: docker buildx build --platform=linux/amd64 -f docker/production.Dockerfile . -t tooljet/tj-osv:cypress + run: docker buildx build --platform=linux/amd64 -f docker/production.Dockerfile . -t tooljet/tj-osv:cypressplaform - name: Set up environment variables run: | diff --git a/.github/workflows/cypress-platform.yml b/.github/workflows/cypress-platform.yml index df001669df..6eda6f4962 100644 --- a/.github/workflows/cypress-platform.yml +++ b/.github/workflows/cypress-platform.yml @@ -148,7 +148,7 @@ jobs: run: docker buildx use mybuilder - name: Build docker image - run: docker buildx build --platform=linux/amd64 -f docker/production.Dockerfile . -t tooljet/tj-osv:cypress + run: docker buildx build --platform=linux/amd64 -f docker/production.Dockerfile . -t tooljet/tj-osv:cypressplaform - name: Set up environment variables run: | diff --git a/.version b/.version index 8392a795a9..f48f82fa2c 100644 --- a/.version +++ b/.version @@ -1 +1 @@ -2.21.2 +2.22.0 diff --git a/cypress-tests/cypress-workspace.config.js b/cypress-tests/cypress-workspace.config.js index 4d5ce1ec0e..f52473973f 100644 --- a/cypress-tests/cypress-workspace.config.js +++ b/cypress-tests/cypress-workspace.config.js @@ -83,7 +83,7 @@ module.exports = defineConfig({ "cypress/e2e/editor/app-version/version.cy.js" ], numTestsKeptInMemory: 1, - redirectionLimit: 7, + redirectionLimit: 15, experimentalRunAllSpecs: true, experimentalMemoryManagement: true, video: false, diff --git a/cypress-tests/cypress.config.js b/cypress-tests/cypress.config.js index 2321dc3cd4..fbe68ac201 100644 --- a/cypress-tests/cypress.config.js +++ b/cypress-tests/cypress.config.js @@ -79,8 +79,8 @@ module.exports = defineConfig({ baseUrl: "http://localhost:8082", specPattern: "cypress/e2e/**/*.cy.js", downloadsFolder: "cypress/downloads", - numTestsKeptInMemory: 10, - redirectionLimit: 5, + numTestsKeptInMemory: 0, + redirectionLimit: 7, experimentalRunAllSpecs: true, trashAssetsBeforeRuns: true, experimentalMemoryManagement: true, diff --git a/cypress-tests/cypress/commands/apiCommands.js b/cypress-tests/cypress/commands/apiCommands.js index d92f378b1e..8d88670dc8 100644 --- a/cypress-tests/cypress/commands/apiCommands.js +++ b/cypress-tests/cypress/commands/apiCommands.js @@ -1,6 +1,10 @@ Cypress.Commands.add( "apiLogin", - (userEmail = "dev@tooljet.io", userPassword = "password", workspaceId = '') => { + ( + userEmail = "dev@tooljet.io", + userPassword = "password", + workspaceId = "" + ) => { cy.request({ url: `http://localhost:3000/api/authenticate/${workspaceId}`, method: "POST", @@ -138,4 +142,24 @@ Cypress.Commands.add( // ] // ); - +Cypress.Commands.add("apiCreateWorkspace", (workspaceName, workspaceSlug) => { + cy.getCookie("tj_auth_token").then((cookie) => { + cy.request( + { + method: "POST", + url: "http://localhost:3000/api/organizations", + headers: { + "Tj-Workspace-Id": Cypress.env("workspaceId"), + Cookie: `tj_auth_token=${cookie.value}`, + }, + body: { + name: workspaceName, + slug: workspaceSlug, + }, + }, + { log: false } + ).then((response) => { + expect(response.status).to.equal(201); + }); + }); +}); diff --git a/cypress-tests/cypress/constants/selectors/common.js b/cypress-tests/cypress/constants/selectors/common.js index e20cfb998d..a61fd9ab5b 100644 --- a/cypress-tests/cypress/constants/selectors/common.js +++ b/cypress-tests/cypress/constants/selectors/common.js @@ -327,10 +327,10 @@ export const commonWidgetSelector = { modalHeader: '[data-cy="modal-header"]', makePublicAppToggleLabel: '[data-cy="make-public-app-label"]', shareableAppLink: '[data-cy="shareable-app-link-label"]', - copyAppLinkButton: '[data-cy="copy-app-link-button"]', // iframeLinkLabel: '[data-cy="iframe-link-label"]', // ifameLinkCopyButton: '[data-cy="iframe-link-copy-button"]', }, + copyAppLinkButton: '.input-group > :nth-child(3)', makePublicAppToggle: '[data-cy="make-public-app-toggle"]', appLink: '[data-cy="app-link"]', appNameSlugInput: '[data-cy="app-name-slug-input"]', diff --git a/cypress-tests/cypress/constants/texts/common.js b/cypress-tests/cypress/constants/texts/common.js index 22699a00f3..6b6c5db6cd 100644 --- a/cypress-tests/cypress/constants/texts/common.js +++ b/cypress-tests/cypress/constants/texts/common.js @@ -164,9 +164,8 @@ export const commonText = { shareModalElements: { modalHeader: "Share", - makePublicAppToggleLabel: "Make application public?", - shareableAppLink: "Get shareable link for this application", - copyAppLinkButton: "copy", + makePublicAppToggleLabel: "Make application public", + shareableAppLink: "Shareable app link", // iframeLinkLabel: "Get embeddable link for this application", // ifameLinkCopyButton: "copy", }, diff --git a/cypress-tests/cypress/constants/texts/exportImport.js b/cypress-tests/cypress/constants/texts/exportImport.js index 530ed63552..b193a1a529 100644 --- a/cypress-tests/cypress/constants/texts/exportImport.js +++ b/cypress-tests/cypress/constants/texts/exportImport.js @@ -20,5 +20,5 @@ export const exportAppModalText = { export const importText = { importOption: "Import", couldNotImportAppToastMessage: `Could not import: SyntaxError: Unexpected token`, - appImportedToastMessage: "Imported successfully.", + appImportedToastMessage: "App imported successfully.", }; diff --git a/cypress-tests/cypress/e2e/editor/globalSetingsHappyPath.cy.js b/cypress-tests/cypress/e2e/editor/globalSetingsHappyPath.cy.js index 20ba9f4881..033365e6b1 100644 --- a/cypress-tests/cypress/e2e/editor/globalSetingsHappyPath.cy.js +++ b/cypress-tests/cypress/e2e/editor/globalSetingsHappyPath.cy.js @@ -19,17 +19,16 @@ import { } from "Texts/common"; describe("Editor- Global Settings", () => { + const data = {}; beforeEach(() => { + data.appName = `${fake.companyName}-App`; cy.apiLogin(); - cy.apiCreateApp(); + cy.apiCreateApp(data.appName); cy.openApp(); }); it("should verify global settings", () => { - const data = {}; data.backgroundColor = fake.randomRgba; - data.appName = `${fake.companyName}-App`; - cy.renameApp(data.appName); cy.get("[data-cy='left-sidebar-settings-button']").click(); cy.get('[data-cy="label-global settings"]').verifyVisibleElement( diff --git a/cypress-tests/cypress/e2e/editor/inspectorHappypath.cy.js b/cypress-tests/cypress/e2e/editor/inspectorHappypath.cy.js index f821183964..a8dce694c5 100644 --- a/cypress-tests/cypress/e2e/editor/inspectorHappypath.cy.js +++ b/cypress-tests/cypress/e2e/editor/inspectorHappypath.cy.js @@ -1,3 +1,4 @@ +import { fake } from "Fixtures/fake"; import { verifyMultipleComponentValuesFromInspector, verifyComponentValueFromInspector, @@ -15,7 +16,7 @@ import { multipageSelector } from "Selectors/multipage"; describe("Editor- Inspector", () => { beforeEach(() => { cy.apiLogin(); - cy.apiCreateApp(); + cy.apiCreateApp(`${fake.companyName}-App`); cy.openApp(); }); diff --git a/cypress-tests/cypress/e2e/editor/multipage/multipageHappypath.cy.js b/cypress-tests/cypress/e2e/editor/multipage/multipageHappypath.cy.js index 1e2c3f33a5..ccf9060476 100644 --- a/cypress-tests/cypress/e2e/editor/multipage/multipageHappypath.cy.js +++ b/cypress-tests/cypress/e2e/editor/multipage/multipageHappypath.cy.js @@ -41,13 +41,12 @@ import { describe("Multipage", () => { beforeEach(() => { cy.apiLogin(); - cy.apiCreateApp(); + cy.apiCreateApp(`${fake.companyName}-App`); cy.openApp(); }); it("should verify the elements on multipage", () => { const data = {}; - data.appName = `${fake.companyName}-App`; data.widgetName = fake.widgetName; data.tooltipText = fake.randomSentence; data.minimumLength = randomNumber(1, 4); diff --git a/cypress-tests/cypress/e2e/editor/queries/runjsHappyPath.cy.js b/cypress-tests/cypress/e2e/editor/queries/runjsHappyPath.cy.js index 22b0a65aa5..dcbda8bd09 100644 --- a/cypress-tests/cypress/e2e/editor/queries/runjsHappyPath.cy.js +++ b/cypress-tests/cypress/e2e/editor/queries/runjsHappyPath.cy.js @@ -64,7 +64,7 @@ import { deleteDownloadsFolder } from "Support/utils/common"; describe("RunJS", () => { beforeEach(() => { cy.apiLogin(); - cy.apiCreateApp(); + cy.apiCreateApp(`${fake.companyName}-App`); cy.openApp(); cy.viewport(1800, 1800); cy.dragAndDropWidget("Button"); diff --git a/cypress-tests/cypress/e2e/editor/queries/runpyHappyPath.cy.js b/cypress-tests/cypress/e2e/editor/queries/runpyHappyPath.cy.js index 6f2bef4624..20173913b3 100644 --- a/cypress-tests/cypress/e2e/editor/queries/runpyHappyPath.cy.js +++ b/cypress-tests/cypress/e2e/editor/queries/runpyHappyPath.cy.js @@ -63,7 +63,7 @@ import { verifyNodeData, openNode, verifyValue } from "Support/utils/inspector"; describe("runpy", () => { beforeEach(() => { cy.apiLogin(); - cy.apiCreateApp(); + cy.apiCreateApp(`${fake.companyName}-App`); cy.openApp(); cy.viewport(1800, 1800); cy.dragAndDropWidget("Button"); @@ -214,7 +214,10 @@ actions.unsetPageVariable('pageVar')` cy.wait(200); cy.waitForAutoSave(); query("run"); - cy.get('[data-cy="sign-in-header"]').should("be.visible"); + + cy.get('[data-cy="sign-in-header"]', { timeout: 20000 }).should( + "be.visible" + ); }); it("should verify global and page data", () => { diff --git a/cypress-tests/cypress/e2e/editor/widget/tableRegression.cy.js b/cypress-tests/cypress/e2e/editor/widget/tableRegression.cy.js index 10dd23461b..1fea385edf 100644 --- a/cypress-tests/cypress/e2e/editor/widget/tableRegression.cy.js +++ b/cypress-tests/cypress/e2e/editor/widget/tableRegression.cy.js @@ -56,7 +56,7 @@ import { resizeQueryPanel } from "Support/utils/dataSource"; describe("Table", () => { beforeEach(() => { cy.apiLogin(); - cy.apiCreateApp(); + cy.apiCreateApp(`${fake.companyName}-App`); cy.openApp(); deleteDownloadsFolder(); cy.viewport(1400, 2200); diff --git a/cypress-tests/cypress/e2e/exportImport/import.cy.js b/cypress-tests/cypress/e2e/exportImport/import.cy.js index 4659180cf7..d5763aeeb8 100644 --- a/cypress-tests/cypress/e2e/exportImport/import.cy.js +++ b/cypress-tests/cypress/e2e/exportImport/import.cy.js @@ -61,14 +61,16 @@ describe("App Import Functionality", () => { cy.get(importSelectors.importOptionInput).selectFile(appFile, { force: true, }); - cy.verifyToastMessage( - commonSelectors.toastMessage, - importText.appImportedToastMessage - ); + cy.get('[data-cy="import-app-title"]').should("be.visible"); + cy.get('[data-cy="Import app"]').click(); + cy.get(".go3958317564") + .should("be.visible") + .and("have.text", importText.appImportedToastMessage); + cy.get(".driver-close-btn").click(); cy.get(commonSelectors.appNameInput).verifyVisibleElement( "contain.value", - appData.name + appData.name.toLowerCase() ); cy.modifyCanvasSize(900, 600); cy.dragAndDropWidget(buttonText.defaultWidgetText); @@ -107,10 +109,12 @@ describe("App Import Functionality", () => { cy.get(importSelectors.importOptionInput).selectFile(exportedFilePath, { force: true, }); - cy.verifyToastMessage( - commonSelectors.toastMessage, - importText.appImportedToastMessage - ); + + cy.get('[data-cy="import-app-title"]').should("be.visible"); + cy.get('[data-cy="Import app"]').click(); + cy.get(".go3958317564") + .should("be.visible") + .and("have.text", importText.appImportedToastMessage); cy.get( `[data-cy="draggable-widget-${buttonText.defaultWidgetName}"]` ).should("be.visible"); @@ -119,7 +123,7 @@ describe("App Import Functionality", () => { cy.get(commonSelectors.appNameInput).verifyVisibleElement( "contain.value", - exportedAppData.app[0].definition.appV2.name + exportedAppData.app[0].definition.appV2.name.toLowerCase() ); cy.get( appVersionSelectors.currentVersionField((currentVersion = "v1")) @@ -178,10 +182,11 @@ describe("App Import Functionality", () => { force: true, } ); - cy.verifyToastMessage( - commonSelectors.toastMessage, - importText.appImportedToastMessage - ); + cy.get('[data-cy="import-app-title"]').should("be.visible"); + cy.get('[data-cy="Import app"]').click(); + cy.get(".go3958317564") + .should("be.visible") + .and("have.text", importText.appImportedToastMessage); cy.get(appVersionSelectors.appVersionMenuField).click(); cy.get(appVersionSelectors.appVersionContentList).should( "have.text", @@ -195,7 +200,7 @@ describe("App Import Functionality", () => { cy.get(commonSelectors.appNameInput).verifyVisibleElement( "contain.value", - exportedAppData.app[0].definition.appV2.name + exportedAppData.app[0].definition.appV2.name.toLowerCase() ); cy.get( appVersionSelectors.currentVersionField( diff --git a/cypress-tests/cypress/e2e/workspace/manageSSO.cy.js b/cypress-tests/cypress/e2e/workspace/manageSSO.cy.js index cf94e5a2a8..bf78dcf765 100644 --- a/cypress-tests/cypress/e2e/workspace/manageSSO.cy.js +++ b/cypress-tests/cypress/e2e/workspace/manageSSO.cy.js @@ -160,9 +160,11 @@ describe("Manage SSO for multi workspace", () => { if (envVar === "Community") { it("Should verify the workspace login page", () => { - data.workspaceName = fake.companyName; - - common.createWorkspace(data.workspaceName); + data.workspaceName = fake.companyName.toLowerCase(); + cy.apiLogin() + cy.apiCreateWorkspace(data.workspaceName, data.workspaceName) + cy.visit(data.workspaceName) + cy.wait(500) common.navigateToManageSSO(); SSO.visitWorkspaceLoginPage(); SSO.workspaceLoginPageElements(data.workspaceName); @@ -243,10 +245,11 @@ describe("Manage SSO for multi workspace", () => { cy.notVisible(commonSelectors.passwordInputField); cy.notVisible(commonSelectors.loginButton); - data.workspaceName = fake.companyName; - cy.appUILogin(); - common.createWorkspace(data.workspaceName); - cy.wait(300); + data.workspaceName = fake.companyName.toLowerCase(); + cy.apiLogin() + cy.apiCreateWorkspace(data.workspaceName, data.workspaceName) + cy.visit(data.workspaceName) + cy.wait(500) SSO.disableDefaultSSO(); cy.get(ssoSelector.passwordEnableToggle).uncheck(); cy.get(commonSelectors.buttonSelector("Yes")).click(); diff --git a/cypress-tests/cypress/e2e/workspace/shareApp.cy.js b/cypress-tests/cypress/e2e/workspace/shareApp.cy.js index b9391d303d..89ca36c237 100644 --- a/cypress-tests/cypress/e2e/workspace/shareApp.cy.js +++ b/cypress-tests/cypress/e2e/workspace/shareApp.cy.js @@ -17,12 +17,16 @@ describe("App share functionality", () => { beforeEach(() => { cy.appUILogin(); }); + before(() => { + cy.apiLogin(); + cy.apiCreateApp(data.appName); + cy.visit('/') + logout(); + }) if (envVar === "Community") { it("Verify private and public app share funtionality", () => { - cy.apiLogin(); - cy.apiCreateApp(data.appName); - cy.openApp(); + cy.openApp(data.appName); cy.dragAndDropWidget("Table", 250, 250); cy.get(commonWidgetSelector.shareAppButton).click(); @@ -35,7 +39,7 @@ describe("App share functionality", () => { commonText.shareModalElements[elements] ); } - + cy.get(commonWidgetSelector.copyAppLinkButton).should("be.visible"); cy.get(commonWidgetSelector.makePublicAppToggle).should("be.visible"); cy.get(commonWidgetSelector.appLink).should("be.visible"); cy.get(commonWidgetSelector.appNameSlugInput).should("be.visible"); diff --git a/cypress-tests/cypress/e2e/workspace/userPermissions.cy.js b/cypress-tests/cypress/e2e/workspace/userPermissions.cy.js index 6911368965..826d783b13 100644 --- a/cypress-tests/cypress/e2e/workspace/userPermissions.cy.js +++ b/cypress-tests/cypress/e2e/workspace/userPermissions.cy.js @@ -19,13 +19,12 @@ data.folderName = `${fake.companyName.toLowerCase()}-folder`; describe("User permissions", () => { before(() => { cy.intercept("GET", "/api/apps?page=1&folder=&searchKey=").as("homePage"); - cy.appUILogin(); + cy.apiLogin(); + cy.apiCreateApp(data.appName); + cy.visit('/') permissions.reset(); cy.get(commonSelectors.homePageLogo).click(); cy.wait("@homePage"); - cy.createApp(data.appName); - cy.dragAndDropWidget("Table", 250, 250); - cy.get(commonSelectors.editorPageLogo).click(); permissions.addNewUserMW(data.firstName, data.email); common.logout(); }); diff --git a/cypress-tests/cypress/support/utils/common.js b/cypress-tests/cypress/support/utils/common.js index a39fc9fb3c..57d3e1b128 100644 --- a/cypress-tests/cypress/support/utils/common.js +++ b/cypress-tests/cypress/support/utils/common.js @@ -207,6 +207,8 @@ export const createWorkspace = (workspaceName) => { cy.get(commonSelectors.workspaceName).click(); cy.get(commonSelectors.addWorkspaceButton).click(); cy.clearAndType(commonSelectors.workspaceNameInput, workspaceName); + cy.clearAndType('[data-cy="workspace-slug-input-field"]', workspaceName); + cy.wait(1000) cy.intercept("GET", "/api/apps?page=1&folder=&searchKey=").as("homePage"); cy.get(commonSelectors.createWorkspaceButton).click(); cy.wait("@homePage"); diff --git a/frontend/.version b/frontend/.version index 8392a795a9..f48f82fa2c 100644 --- a/frontend/.version +++ b/frontend/.version @@ -1 +1 @@ -2.21.2 +2.22.0 diff --git a/frontend/assets/translations/en.json b/frontend/assets/translations/en.json index ff40b511e6..a8c46d9055 100644 --- a/frontend/assets/translations/en.json +++ b/frontend/assets/translations/en.json @@ -117,10 +117,10 @@ "preview": "Preview", "share": "Share", "shareModal": { - "makeApplicationPublic": "Make application public?", - "shareableLink": "Get shareable link for this application", + "makeApplicationPublic": "Make application public", + "shareableLink": "Shareable app link", "copy": "copy", - "embeddableLink": "Get embeddable link for this application", + "embeddableLink": "Embedded app link", "manageUsers": "Users" }, "appVersionManager": { @@ -234,7 +234,7 @@ "addNewWorkSpace": "Add new workspace", "loadOrganizations": "Load Organizations", "createWorkspace": "Create workspace", - "workspaceName": "workspace name", + "workspaceName": "Workspace name", "editWorkspace": "Edit workspace", "menus": { "addWorkspace": "Add workspace", diff --git a/frontend/ee/components/LoginPage/GitSSOLoginButton.jsx b/frontend/ee/components/LoginPage/GitSSOLoginButton.jsx index 3bac30ab25..c6d3d75f61 100644 --- a/frontend/ee/components/LoginPage/GitSSOLoginButton.jsx +++ b/frontend/ee/components/LoginPage/GitSSOLoginButton.jsx @@ -1,9 +1,10 @@ import React from 'react'; import { buildURLWithQuery } from '@/_helpers/utils'; -export default function GitSSOLoginButton({ configs, text }) { +export default function GitSSOLoginButton({ configs, text, setRedirectUrlToCookie }) { const gitLogin = (e) => { e.preventDefault(); + setRedirectUrlToCookie && setRedirectUrlToCookie(); window.location.href = buildURLWithQuery(`${configs.host_name || 'https://github.com'}/login/oauth/authorize`, { client_id: configs?.client_id, scope: 'user:email', diff --git a/frontend/ee/components/LoginPage/GoogleSSOLoginButton.jsx b/frontend/ee/components/LoginPage/GoogleSSOLoginButton.jsx index bd344ac31e..ff0cbe3fb1 100644 --- a/frontend/ee/components/LoginPage/GoogleSSOLoginButton.jsx +++ b/frontend/ee/components/LoginPage/GoogleSSOLoginButton.jsx @@ -12,6 +12,7 @@ export default function GoogleSSOLoginButton(props) { }; const googleLogin = (e) => { e.preventDefault(); + props.setRedirectUrlToCookie && props.setRedirectUrlToCookie(); const { client_id } = props.configs; const authUrl = buildURLWithQuery('https://accounts.google.com/o/oauth2/auth', { redirect_uri: `${window.public_config?.TOOLJET_HOST}${window.public_config?.SUB_PATH ?? '/'}sso/google${ diff --git a/frontend/src/App/App.jsx b/frontend/src/App/App.jsx index e72284077d..5958d6ea70 100644 --- a/frontend/src/App/App.jsx +++ b/frontend/src/App/App.jsx @@ -1,16 +1,8 @@ import React, { Suspense } from 'react'; // eslint-disable-next-line no-unused-vars -import config from 'config'; import { BrowserRouter, Navigate, Route, Routes } from 'react-router-dom'; - -import { - getWorkspaceIdFromURL, - appendWorkspaceId, - stripTrailingSlash, - getSubpath, - pathnameWithoutSubpath, -} from '@/_helpers/utils'; -import { authenticationService, tooljetService, organizationService } from '@/_services'; +import { authorizeWorkspace } from '@/_helpers/authorizeWorkspace'; +import { authenticationService, tooljetService } from '@/_services'; import { withRouter } from '@/_hoc/withRouter'; import { PrivateRoute, AdminRoute } from '@/_components'; import { HomePage } from '@/HomePage'; @@ -36,6 +28,7 @@ import { AppLoader } from '@/AppLoader'; import SetupScreenSelfHost from '../SuccessInfoScreen/SetupScreenSelfHost'; export const BreadCrumbContext = React.createContext({}); import 'react-tooltip/dist/react-tooltip.css'; +import { getWorkspaceIdOrSlugFromURL } from '@/_helpers/routes'; const AppWrapper = (props) => { return ( @@ -69,59 +62,8 @@ class AppComponent extends React.Component { }); }; - isThisExistedRoute = () => { - const existedPaths = [ - 'forgot-password', - 'reset-password', - 'invitations', - 'organization-invitations', - 'setup', - 'confirm', - 'confirm-invite', - ]; - - const subpath = getSubpath(); - const subpathArray = subpath ? subpath.split('/').filter((path) => path != '') : []; - const pathnames = window.location.pathname.split('/')?.filter((path) => path != ''); - const checkPath = () => existedPaths.find((path) => pathnames[subpath ? subpathArray.length : 0] === path); - return pathnames?.length > 0 ? (checkPath() ? true : false) : false; - }; - componentDidMount() { - if (!this.isThisExistedRoute()) { - const workspaceId = getWorkspaceIdFromURL(); - if (workspaceId) { - this.authorizeUserAndHandleErrors(workspaceId); - } else { - const isApplicationsPath = window.location.pathname.includes('/applications/'); - const appId = isApplicationsPath ? pathnameWithoutSubpath(window.location.pathname).split('/')[2] : null; - authenticationService - .validateSession(appId) - .then(({ current_organization_id }) => { - //check if the page is not switch-workspace, if then redirect to the page - if (window.location.pathname !== `${getSubpath() ?? ''}/switch-workspace`) { - this.authorizeUserAndHandleErrors(current_organization_id); - } else { - this.updateCurrentSession({ - current_organization_id, - }); - } - }) - .catch(() => { - if (!this.isThisWorkspaceLoginPage(true) && !isApplicationsPath) { - this.updateCurrentSession({ - authentication_status: false, - }); - } else if (isApplicationsPath) { - this.updateCurrentSession({ - authentication_failed: true, - load_app: true, - }); - } - }); - } - } - + authorizeWorkspace(); this.fetchMetadata(); setInterval(this.fetchMetadata, 1000 * 60 * 60 * 1); } @@ -136,8 +78,8 @@ class AppComponent extends React.Component { componentDidUpdate(prevProps) { // Check if the current location is the dashboard (homepage) if ( - this.props.location.pathname === `/${getWorkspaceIdFromURL()}` && - prevProps.location.pathname !== `/${getWorkspaceIdFromURL()}` && + this.props.location.pathname === `/${getWorkspaceIdOrSlugFromURL()}` && + prevProps.location.pathname !== `/${getWorkspaceIdOrSlugFromURL()}` && this.checkPreviousRoute(prevProps.location.pathname) && prevProps.location.pathname !== `/:workspaceId` ) { @@ -146,93 +88,6 @@ class AppComponent extends React.Component { } } - isThisWorkspaceLoginPage = (justLoginPage = false) => { - const subpath = window?.public_config?.SUB_PATH ? stripTrailingSlash(window?.public_config?.SUB_PATH) : null; - const pathname = location.pathname.replace(subpath, ''); - const pathnames = pathname.split('/').filter((path) => path !== ''); - return (justLoginPage && pathnames[0] === 'login') || (pathnames.length === 2 && pathnames[0] === 'login'); - }; - - authorizeUserAndHandleErrors = (workspaceId) => { - const subpath = getSubpath(); - this.updateCurrentSession({ - current_organization_id: workspaceId, - }); - authenticationService - .authorize() - .then((data) => { - organizationService.getOrganizations().then((response) => { - const current_organization_name = response.organizations.find((org) => org.id === workspaceId)?.name; - // this will add the other details like permission and user previlliage details to the subject - this.updateCurrentSession({ - ...data, - current_organization_name, - organizations: response.organizations, - load_app: true, - }); - - // if user is trying to load the workspace login page, then redirect to the dashboard - if (this.isThisWorkspaceLoginPage()) - return (window.location = appendWorkspaceId(workspaceId, '/:workspaceId')); - }); - }) - .catch((error) => { - // if the auth token didn't contain workspace-id, try switch workspace fn - if (error && error?.data?.statusCode === 401) { - //get current session workspace id - authenticationService - .validateSession() - .then(({ current_organization_id }) => { - // change invalid or not authorized org id to previous one - this.updateCurrentSession({ - current_organization_id, - }); - - organizationService - .switchOrganization(workspaceId) - .then((data) => { - this.updateCurrentSession(data); - if (this.isThisWorkspaceLoginPage()) - return (window.location = appendWorkspaceId(workspaceId, '/:workspaceId')); - this.authorizeUserAndHandleErrors(workspaceId); - }) - .catch(() => { - organizationService.getOrganizations().then((response) => { - const current_organization_name = response.organizations.find( - (org) => org.id === current_organization_id - )?.name; - - this.updateCurrentSession({ - current_organization_name, - load_app: true, - }); - - if (!this.isThisWorkspaceLoginPage()) - return (window.location = `${subpath ?? ''}/login/${workspaceId}`); - }); - }); - }) - .catch(() => this.logout()); - } else if ((error && error?.data?.statusCode == 422) || error?.data?.statusCode == 404) { - window.location = subpath ? `${subpath}${'/switch-workspace'}` : '/switch-workspace'; - } else { - if (!this.isThisWorkspaceLoginPage() && !this.isThisWorkspaceLoginPage(true)) - this.updateCurrentSession({ - authentication_status: false, - }); - } - }); - }; - - updateCurrentSession = (newSession) => { - const currentSession = authenticationService.currentSessionValue; - authenticationService.updateCurrentSession({ ...currentSession, ...newSession }); - }; - - logout = () => { - authenticationService.logout(); - }; - switchDarkMode = (newMode) => { this.setState({ darkMode: newMode }); localStorage.setItem('darkMode', newMode); @@ -314,22 +169,13 @@ class AppComponent extends React.Component { /> } /> - - - - } - /> { - const params = useParams(); - const appId = params.id; - - // eslint-disable-next-line react-hooks/exhaustive-deps - useEffect(() => loadAppDetails(), []); - - const loadAppDetails = () => { - appService.getApp(appId, 'edit').catch((error) => { - handleError(error); - }); - }; - - const switchOrganization = (orgId) => { - const path = `/apps/${appId}`; - const sub_path = window?.public_config?.SUB_PATH ? stripTrailingSlash(window?.public_config?.SUB_PATH) : ''; - organizationService.switchOrganization(orgId).then( - () => { - window.location.href = `${sub_path}/${orgId}${path}`; - }, - () => { - return (window.location.href = `${sub_path}/login/${orgId}?redirectTo=${path}`); - } - ); - }; - - const handleError = (error) => { - try { - if (error?.data) { - const statusCode = error.data?.statusCode; - if (statusCode === 403) { - const errorObj = safelyParseJSON(error.data?.message); - if ( - errorObj?.organizationId && - authenticationService.currentSessionValue.current_organization_id !== errorObj?.organizationId - ) { - switchOrganization(errorObj?.organizationId); - return; - } - redirectToDashboard(); - } else if (statusCode === 401) { - window.location = `${getSubpath() ?? ''}/login${ - !_.isEmpty(getWorkspaceId()) ? `/${getWorkspaceId()}` : '' - }?redirectTo=${this.props.location.pathname}`; - return; - } else if (statusCode === 404 || statusCode === 422) { - toast.error(error?.error ?? 'App not found'); - } - redirectToDashboard(); - } - } catch (err) { - redirectToDashboard(); - } - }; - return config.ENABLE_MULTIPLAYER_EDITING ? : ; }; diff --git a/frontend/src/Editor/Comment/CommentActions.jsx b/frontend/src/Editor/Comment/CommentActions.jsx index 836012b806..f63ee536a0 100644 --- a/frontend/src/Editor/Comment/CommentActions.jsx +++ b/frontend/src/Editor/Comment/CommentActions.jsx @@ -5,10 +5,11 @@ import { useSpring, animated } from 'react-spring'; import usePopover from '@/_hooks/use-popover'; import OptionsIcon from './icons/options.svg'; // import OptionsSelectedIcon from './icons/options-selected.svg'; -import useRouter from '@/_hooks/use-router'; import { commentsService } from '@/_services'; import { useTranslation } from 'react-i18next'; +import { useAppDataStore } from '@/_stores/appDataStore'; +import { shallow } from 'zustand/shallow'; const CommentActions = ({ socket, @@ -21,8 +22,13 @@ const CommentActions = ({ }) => { const [open, trigger, content, setOpen] = usePopover(false); const popoverFadeStyle = useSpring({ opacity: open ? 1 : 0 }); - const router = useRouter(); const { t } = useTranslation(); + const { appId } = useAppDataStore( + (state) => ({ + appId: state?.appId, + }), + shallow + ); const handleDelete = async () => { await commentsService.deleteComment(commentId); @@ -31,7 +37,7 @@ const CommentActions = ({ socket.send( JSON.stringify({ event: 'events', - data: { message: 'notifications', appId: router.query.id }, + data: { message: 'notifications', appId }, }) ); }; diff --git a/frontend/src/Editor/Comment/CommentHeader.jsx b/frontend/src/Editor/Comment/CommentHeader.jsx index 0232be8521..828c46d899 100644 --- a/frontend/src/Editor/Comment/CommentHeader.jsx +++ b/frontend/src/Editor/Comment/CommentHeader.jsx @@ -7,14 +7,12 @@ import { commentsService } from '@/_services'; import { pluralize } from '@/_helpers/utils'; import Spinner from '@/_ui/Spinner'; -import useRouter from '@/_hooks/use-router'; import UnResolvedIcon from './icons/unresolved.svg'; import ResolvedIcon from './icons/resolved.svg'; -const CommentHeader = ({ socket, count = 0, threadId, isResolved, isThreadOwner, fetchThreads, close }) => { +const CommentHeader = ({ socket, count = 0, threadId, isResolved, isThreadOwner, fetchThreads, close, appId }) => { const [spinning, setSpinning] = React.useState(false); - const router = useRouter(); const handleResolved = async () => { setSpinning(true); @@ -24,7 +22,7 @@ const CommentHeader = ({ socket, count = 0, threadId, isResolved, isThreadOwner, socket.send( JSON.stringify({ event: 'events', - data: { message: 'notifications', appId: router.query.id }, + data: { message: 'notifications', appId }, }) ); if (!isResolved) { @@ -41,7 +39,7 @@ const CommentHeader = ({ socket, count = 0, threadId, isResolved, isThreadOwner, socket.send( JSON.stringify({ event: 'events', - data: { message: 'notifications', appId: router.query.id }, + data: { message: 'notifications', appId }, }) ); }; diff --git a/frontend/src/Editor/Comment/index.jsx b/frontend/src/Editor/Comment/index.jsx index 7c126a3d54..f35912bc48 100644 --- a/frontend/src/Editor/Comment/index.jsx +++ b/frontend/src/Editor/Comment/index.jsx @@ -11,8 +11,20 @@ import { commentsService, organizationService, authenticationService } from '@/_ import useRouter from '@/_hooks/use-router'; import DOMPurify from 'dompurify'; import { capitalize } from 'lodash'; +import { getPathname } from '@/_helpers/routes'; -const Comment = ({ socket, x, y, threadId, user = {}, isResolved, fetchThreads, appVersionsId, canvasWidth }) => { +const Comment = ({ + socket, + x, + y, + threadId, + user = {}, + isResolved, + fetchThreads, + appVersionsId, + canvasWidth, + appId, +}) => { const [loading, setLoading] = React.useState(true); const [editComment, setEditComment] = React.useState(''); const [editCommentId, setEditCommentId] = React.useState(''); @@ -60,7 +72,7 @@ const Comment = ({ socket, x, y, threadId, user = {}, isResolved, fetchThreads, } else { // resetting the query param // react router updates the url with the set basename resulting invalid url unless replaced - router.history(window.location.pathname.replace(window.public_config?.SUB_PATH, '/')); + router.history(getPathname()); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [open]); @@ -82,13 +94,13 @@ const Comment = ({ socket, x, y, threadId, user = {}, isResolved, fetchThreads, socket.send( JSON.stringify({ event: 'events', - data: { message: threadId, appId: router.query.id }, + data: { message: threadId, appId }, }) ); socket.send( JSON.stringify({ event: 'events', - data: { message: 'notifications', appId: router.query.id }, + data: { message: 'notifications', appId }, }) ); fetchData(); @@ -100,7 +112,7 @@ const Comment = ({ socket, x, y, threadId, user = {}, isResolved, fetchThreads, socket.send( JSON.stringify({ event: 'events', - data: { message: 'notifications', appId: router.query.id }, + data: { message: 'notifications', appId }, }) ); }; @@ -168,6 +180,7 @@ const Comment = ({ socket, x, y, threadId, user = {}, isResolved, fetchThreads, fetchThreads={fetchThreads} isThreadOwner={currentUser?.id === user.id} isResolved={isResolved} + appId={appId} /> { const router = useRouter(); @@ -35,7 +36,7 @@ const Content = ({ notifications, loading, darkMode }) => { onClick={() => { router.push({ // react router updates the url with the set basename resulting invalid url unless replaced - pathname: window.location.pathname.replace(window.public_config?.SUB_PATH, '/'), + pathname: getPathname(), search: `?threadId=${comment.thread.id}&commentId=${comment.id}`, }); }} diff --git a/frontend/src/Editor/CommentNotifications/index.jsx b/frontend/src/Editor/CommentNotifications/index.jsx index bc7364ce6a..f2eda8323e 100644 --- a/frontend/src/Editor/CommentNotifications/index.jsx +++ b/frontend/src/Editor/CommentNotifications/index.jsx @@ -3,9 +3,9 @@ import cx from 'classnames'; import React from 'react'; import { commentsService } from '@/_services'; import TabContent from './Content'; -import useRouter from '@/_hooks/use-router'; import { useAppVersionStore } from '@/_stores/appVersionStore'; import { useEditorStore } from '@/_stores/editorStore'; +import { useAppDataStore } from '@/_stores/appDataStore'; import { shallow } from 'zustand/shallow'; const CommentNotifications = ({ socket, pageId }) => { @@ -22,24 +22,31 @@ const CommentNotifications = ({ socket, pageId }) => { }), shallow ); + const { appId } = useAppDataStore( + (state) => ({ + appId: state?.appId, + }), + shallow + ); const [notifications, setNotifications] = React.useState([]); const [loading, setLoading] = React.useState(false); const [key, setKey] = React.useState('active'); - const router = useRouter(); - async function fetchData(selectedKey) { - const isResolved = selectedKey === 'resolved'; - setLoading(true); - const { data } = await commentsService.getNotifications(router.query.id, isResolved, appVersionsId, pageId); - setLoading(false); - setNotifications(data); + if (appId) { + console.log('inside-CommentNotifications', appId); + const isResolved = selectedKey === 'resolved'; + setLoading(true); + const { data } = await commentsService.getNotifications(appId, isResolved, appVersionsId, pageId); + setLoading(false); + setNotifications(data); + } } React.useEffect(() => { fetchData(); // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + }, [appId]); React.useEffect(() => { socket?.addEventListener('message', function (event) { diff --git a/frontend/src/Editor/Comments.jsx b/frontend/src/Editor/Comments.jsx index 55c6c02013..172d0b3a39 100644 --- a/frontend/src/Editor/Comments.jsx +++ b/frontend/src/Editor/Comments.jsx @@ -5,15 +5,20 @@ import { isEmpty } from 'lodash'; import Comment from './Comment'; import { commentsService } from '@/_services'; import { useAppVersionStore } from '@/_stores/appVersionStore'; -import useRouter from '@/_hooks/use-router'; +import { useAppDataStore } from '@/_stores/appDataStore'; const Comments = ({ newThread = {}, socket, canvasWidth, currentPageId }) => { const [threads, setThreads] = React.useState([]); - const router = useRouter(); const { appVersionsId } = useAppVersionStore((state) => ({ appVersionsId: state?.editingVersion?.id }), shallow); + const { appId } = useAppDataStore( + (state) => ({ + appId: state?.appId, + }), + shallow + ); async function fetchData() { - const { data } = await commentsService.getThreads(router.query.id, appVersionsId); + const { data } = await commentsService.getThreads(appId, appVersionsId); setThreads(data); } @@ -49,6 +54,7 @@ const Comments = ({ newThread = {}, socket, canvasWidth, currentPageId }) => { socket={socket} threadId={id} canvasWidth={canvasWidth} + appId={appId} {...thread} /> ); diff --git a/frontend/src/Editor/Container.jsx b/frontend/src/Editor/Container.jsx index d196cd5213..f7f49a994c 100644 --- a/frontend/src/Editor/Container.jsx +++ b/frontend/src/Editor/Container.jsx @@ -7,7 +7,6 @@ import { DraggableBox } from './DraggableBox'; import update from 'immutability-helper'; import { componentTypes } from './WidgetManager/components'; import { resolveReferences } from '@/_helpers/utils'; -import useRouter from '@/_hooks/use-router'; import Comments from './Comments'; import { commentsService } from '@/_services'; import config from 'config'; @@ -18,6 +17,7 @@ import { addComponents, addNewWidgetToTheEditor } from '@/_helpers/appUtils'; import { useCurrentState } from '@/_stores/currentStateStore'; import { useAppVersionStore } from '@/_stores/appVersionStore'; import { useEditorStore } from '@/_stores/editorStore'; +import { useAppDataStore } from '@/_stores/appDataStore'; import { shallow } from 'zustand/shallow'; const NO_OF_GRIDS = 43; @@ -70,6 +70,12 @@ export const Container = ({ }), shallow ); + const { appId } = useAppDataStore( + (state) => ({ + appId: state?.appId, + }), + shallow + ); const [boxes, setBoxes] = useState(components); const [isDragging, setIsDragging] = useState(false); @@ -78,7 +84,6 @@ export const Container = ({ const [newThread, addNewThread] = useState({}); const [isContainerFocused, setContainerFocus] = useState(false); const [canvasHeight, setCanvasHeight] = useState(null); - const router = useRouter(); const canvasRef = useRef(null); const focusedParentIdRef = useRef(undefined); useHotkeys('meta+z, control+z', () => handleUndo()); @@ -420,7 +425,7 @@ export const Container = ({ ]); const { data } = await commentsService.createThread({ - appId: router.query.id, + appId, x: x, y: e.nativeEvent.offsetY, appVersionsId, @@ -436,7 +441,7 @@ export const Container = ({ socket.send( JSON.stringify({ event: 'events', - data: { message: 'threads', appId: router.query.id }, + data: { message: 'threads', appId }, }) ); @@ -465,7 +470,7 @@ export const Container = ({ }, ]); const { data } = await commentsService.createThread({ - appId: router.query.id, + appId, x, y: y - 130, appVersionsId, @@ -481,7 +486,7 @@ export const Container = ({ socket.send( JSON.stringify({ event: 'events', - data: { message: 'threads', appId: router.query.id }, + data: { message: 'threads', appId }, }) ); diff --git a/frontend/src/Editor/Editor.jsx b/frontend/src/Editor/Editor.jsx index ce8cb2333a..49e6f4a6f1 100644 --- a/frontend/src/Editor/Editor.jsx +++ b/frontend/src/Editor/Editor.jsx @@ -1,6 +1,6 @@ import React from 'react'; import { - appService, + appsService, authenticationService, appVersionService, orgEnvironmentVariableService, @@ -8,8 +8,7 @@ import { } from '@/_services'; import { DndProvider } from 'react-dnd'; import { HTML5Backend } from 'react-dnd-html5-backend'; -import { defaults, cloneDeep, isEqual, isEmpty, debounce, omit } from 'lodash'; -import { shallow } from 'zustand/shallow'; +import _, { defaults, cloneDeep, isEqual, isEmpty, debounce, omit } from 'lodash'; import { Container } from './Container'; import { EditorKeyHooks } from './EditorKeyHooks'; import { CustomDragLayer } from './CustomDragLayer'; @@ -60,6 +59,7 @@ import { useAppDataStore } from '@/_stores/appDataStore'; import { useCurrentStateStore, useCurrentState } from '@/_stores/currentStateStore'; import { resetAllStores } from '@/_stores/utils'; import { setCookie } from '@/_helpers/cookie'; +import { shallow } from 'zustand/shallow'; setAutoFreeze(false); enablePatches(); @@ -67,21 +67,16 @@ enablePatches(); class EditorComponent extends React.Component { constructor(props) { super(props); - resetAllStores(); - const appId = this.props.params.id; + resetAllStores(); + const appId = props.id; useAppDataStore.getState().actions.setAppId(appId); useEditorStore.getState().actions.setIsEditorActive(true); const { socket } = createWebsocketConnection(appId); - - this.renameQueryNameId = React.createRef(); - this.socket = socket; - + this.renameQueryNameId = React.createRef(); const defaultPageId = uuid(); - this.subscription = null; - this.defaultDefinition = { showViewerNavigation: true, homePageId: defaultPageId, @@ -199,7 +194,6 @@ class EditorComponent extends React.Component { threshold: 0, }, }); - const globals = { ...this.props.currentState.globals, theme: { name: this.props.darkMode ? 'dark' : 'light' }, @@ -341,7 +335,7 @@ class EditorComponent extends React.Component { const newState = !this.state.app.is_maintenance_on; // eslint-disable-next-line no-unused-vars - appService.setMaintenance(this.state.app.id, newState).then((data) => { + appsService.setMaintenance(this.state.app.id, newState).then((data) => { this.setState({ app: { ...this.state.app, @@ -358,7 +352,7 @@ class EditorComponent extends React.Component { }; fetchApps = (page) => { - appService.getAll(page).then((data) => + appsService.getAll(page).then((data) => this.setState({ apps: data.apps, }) @@ -366,7 +360,7 @@ class EditorComponent extends React.Component { }; fetchApp = (startingPageHandle) => { - const appId = this.props.params.id; + const appId = this.props.id; const callBack = async (data) => { let dataDefinition = defaults(data.definition, this.defaultDefinition); @@ -424,7 +418,7 @@ class EditorComponent extends React.Component { isLoading: true, }, () => { - appService.getApp(appId).then(callBack); + appsService.getApp(appId).then(callBack); } ); }; @@ -1399,7 +1393,11 @@ class EditorComponent extends React.Component { const queryParamsString = queryParams.map(([key, value]) => `${key}=${value}`).join('&'); - this.props.navigate(`/${getWorkspaceId()}/apps/${this.state.appId}/${handle}?${queryParamsString}`); + this.props.navigate(`/${getWorkspaceId()}/apps/${this.state.slug}/${handle}?${queryParamsString}`, { + state: { + isSwitchingPage: true, + }, + }); const { globals: existingGlobals } = this.props.currentState; @@ -1511,8 +1509,11 @@ class EditorComponent extends React.Component { const selectedComponents = this?.props?.selectedComponents; const currentState = this.props?.currentState; const editingVersion = this.props?.editingVersion; + const previewQuery = queryString.stringify({ version: editingVersion?.name }); const appVersionPreviewLink = editingVersion - ? `/applications/${app.id}/versions/${editingVersion.id}/${currentState.page.handle}` + ? `/applications/${slug || appId}/${currentState.page.handle}${ + !_.isEmpty(previewQuery) ? `?${previewQuery}` : '' + }` : ''; return (
@@ -1605,6 +1606,8 @@ class EditorComponent extends React.Component { updateOnSortingPages={this.updateOnSortingPages} apps={apps} setEditorMarginLeft={this.handleEditorMarginLeftChange} + slug={slug} + handleSlugChange={this.handleSlugChange} /> {!this.props.showComments && ( diff --git a/frontend/src/Editor/Header/EditAppName.jsx b/frontend/src/Editor/Header/EditAppName.jsx index 304e964eb2..d1514d60e7 100644 --- a/frontend/src/Editor/Header/EditAppName.jsx +++ b/frontend/src/Editor/Header/EditAppName.jsx @@ -1,6 +1,6 @@ import React, { useRef, useEffect, useState } from 'react'; import { ToolTip } from '@/_components'; -import { appService } from '@/_services'; +import { appsService } from '@/_services'; import { handleHttpErrorMessages, validateName } from '@/_helpers/utils'; import InfoOrErrorBox from './InfoOrErrorBox'; import { toast } from 'react-hot-toast'; @@ -47,7 +47,7 @@ function EditAppName({ appId, appName = '', onNameChanged }) { } try { - await appService.saveApp(appId, { name: trimmedName }); + await appsService.saveApp(appId, { name: trimmedName }); onNameChanged(trimmedName); setIsValid(true); setIsEditing(false); diff --git a/frontend/src/Editor/Header/GlobalSettings.jsx b/frontend/src/Editor/Header/GlobalSettings.jsx index 81b16c426b..3ff57e6302 100644 --- a/frontend/src/Editor/Header/GlobalSettings.jsx +++ b/frontend/src/Editor/Header/GlobalSettings.jsx @@ -5,9 +5,11 @@ import { Confirm } from '../Viewer/Confirm'; import { HeaderSection } from '@/_ui/LeftSidebar'; import FxButton from '../CodeBuilder/Elements/FxButton'; import { CodeHinter } from '../CodeBuilder/CodeHinter'; -import { resolveReferences } from '@/_helpers/utils'; +import { resolveReferences, validateName, getWorkspaceId } from '@/_helpers/utils'; import { useTranslation } from 'react-i18next'; import _ from 'lodash'; +import { appsService } from '@/_services'; +import { replaceEditorURL, getHostURL } from '@/_helpers/routes'; import ExportAppModal from '../../HomePage/ExportAppModal'; import { useAppVersionStore } from '@/_stores/appVersionStore'; import { shallow } from 'zustand/shallow'; @@ -22,6 +24,8 @@ export const GlobalSettings = ({ app, backgroundFxQuery, realState, + handleSlugChange, + slug: oldSlug, }) => { const { t } = useTranslation(); const { hideHeader, canvasMaxWidth, canvasMaxWidthType, canvasBackgroundColor } = globalSettings; @@ -29,6 +33,10 @@ export const GlobalSettings = ({ const [forceCodeBox, setForceCodeBox] = useState(true); const [showConfirmation, setConfirmationShow] = useState(false); const [isExportingApp, setIsExportingApp] = React.useState(false); + /* Unique app slug states */ + const [slug, setSlug] = useState({ value: null, error: '' }); + const [slugProgress, setSlugProgress] = useState(false); + const [isSlugUpdated, setSlugUpdatedState] = useState(false); const { isVersionReleased } = useAppVersionStore( (state) => ({ isVersionReleased: state.isVersionReleased, @@ -44,6 +52,59 @@ export const GlobalSettings = ({ left: '0px', }; + useEffect(() => { + /* + Only will fail for existed apps before the app/workspace url revamp which has + special chars or spaces in their app slugs + */ + const existedSlugErrors = validateName(oldSlug, 'App slug', true, false, false, false); + setSlug({ value: oldSlug, error: existedSlugErrors.errorMsg }); + }, [oldSlug]); + + const handleInputChange = (value, field) => { + setSlug({ + value: slug?.value, + error: null, + }); + + const error = validateName(value, `App ${field}`, true, false, !(field === 'slug'), !(field === 'slug')); + + if (!_.isEmpty(value) && value !== oldSlug && _.isEmpty(error.errorMsg)) { + setSlugProgress(true); + appsService + .setSlug(app?.id, value) + .then(() => { + setSlug({ + value, + error: '', + }); + setSlugProgress(false); + handleSlugChange(value); + setSlugUpdatedState(true); + replaceEditorURL(value, realState?.page?.handle); + }) + .catch(({ error }) => { + setSlug({ + value, + error, + }); + setSlugProgress(false); + setSlugUpdatedState(false); + }); + } else { + setSlugProgress(false); + setSlugUpdatedState(false); + setSlug({ + value, + error: error?.errorMsg, + }); + } + }; + + const delayedSlugChange = _.debounce((value, field) => { + handleInputChange(value, field); + }, 500); + const outerStyles = { width: '142px', height: '32px', @@ -81,12 +142,72 @@ export const GlobalSettings = ({ darkMode={darkMode} /> )} -
+
-
+
+
+
+
+ + { + e.persist(); + delayedSlugChange(e.target.value, 'slug'); + }} + data-cy="app-slug-input-field" + defaultValue={oldSlug} + /> + {isSlugUpdated && ( +
+ + + +
+ )} + {slug?.error ? ( + + ) : isSlugUpdated ? ( + + ) : ( + + )} +
+
+
+
+ +
+ {!slugProgress ? ( + `${getHostURL()}/${getWorkspaceId()}/apps/${slug?.value || oldSlug || ''}` + ) : ( +
+
+ Loading... +
+ {`Updating link`} +
+ )} +
+ +
+
+
+
+
diff --git a/frontend/src/Editor/Header/index.js b/frontend/src/Editor/Header/index.js index 52476b009e..005d4a8ff3 100644 --- a/frontend/src/Editor/Header/index.js +++ b/frontend/src/Editor/Header/index.js @@ -12,8 +12,10 @@ import config from 'config'; // eslint-disable-next-line import/no-unresolved import { useUpdatePresence } from '@y-presence/react'; import { useAppVersionStore } from '@/_stores/appVersionStore'; +import { useCurrentState } from '@/_stores/currentStateStore'; import { shallow } from 'zustand/shallow'; import SolidIcon from '@/_ui/Icon/SolidIcons'; +import { redirectToDashboard } from '@/_helpers/routes'; export default function EditorHeader({ M, @@ -43,6 +45,7 @@ export default function EditorHeader({ }), shallow ); + const currentState = useCurrentState(); const updatePresence = useUpdatePresence(); useEffect(() => { @@ -61,7 +64,7 @@ export default function EditorHeader({ }, [currentUser]); const handleLogoClick = () => { // Force a reload for clearing interval triggers - window.location.href = '/'; + redirectToDashboard(); }; return ( @@ -148,9 +151,10 @@ export default function EditorHeader({ )}
diff --git a/frontend/src/Editor/LeftSidebar/SidebarComment.jsx b/frontend/src/Editor/LeftSidebar/SidebarComment.jsx index 8fe411c1ca..9ce9fb6a01 100644 --- a/frontend/src/Editor/LeftSidebar/SidebarComment.jsx +++ b/frontend/src/Editor/LeftSidebar/SidebarComment.jsx @@ -2,9 +2,9 @@ import React, { forwardRef } from 'react'; import cx from 'classnames'; import { LeftSidebarItem } from './SidebarItem'; import { commentsService } from '@/_services'; -import useRouter from '@/_hooks/use-router'; import { useAppVersionStore } from '@/_stores/appVersionStore'; import { useEditorStore } from '@/_stores/editorStore'; +import { useAppDataStore } from '@/_stores/appDataStore'; import { shallow } from 'zustand/shallow'; export const LeftSidebarComment = forwardRef(({ selectedSidebarItem, currentPageId }, ref) => { @@ -20,18 +20,23 @@ export const LeftSidebarComment = forwardRef(({ selectedSidebarItem, currentPage }), shallow ); + const { appId } = useAppDataStore( + (state) => ({ + appId: state?.appId, + }), + shallow + ); const [isActive, toggleActive] = React.useState(false); const [notifications, setNotifications] = React.useState([]); - const router = useRouter(); React.useEffect(() => { - if (appVersionsId) { - commentsService.getNotifications(router.query.id, false, appVersionsId, currentPageId).then(({ data }) => { + if (appVersionsId && appId) { + commentsService.getNotifications(appId, false, appVersionsId, currentPageId).then(({ data }) => { setNotifications(data); }); } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [appVersionsId, currentPageId]); + }, [appVersionsId, currentPageId, appId]); return ( 0} diff --git a/frontend/src/Editor/LeftSidebar/index.jsx b/frontend/src/Editor/LeftSidebar/index.jsx index f946b6df2f..55a8e11107 100644 --- a/frontend/src/Editor/LeftSidebar/index.jsx +++ b/frontend/src/Editor/LeftSidebar/index.jsx @@ -54,6 +54,8 @@ export const LeftSidebar = forwardRef((props, ref) => { toggleAppMaintenance, app, disableEnablePage, + slug, + handleSlugChange, } = props; const { is_maintenance_on } = app; @@ -223,6 +225,8 @@ export const LeftSidebar = forwardRef((props, ref) => { app={app} backgroundFxQuery={backgroundFxQuery} realState={realState} + slug={slug} + handleSlugChange={handleSlugChange} /> ), }; diff --git a/frontend/src/Editor/ManageAppUsers.jsx b/frontend/src/Editor/ManageAppUsers.jsx index 04860f1622..0a5c924793 100644 --- a/frontend/src/Editor/ManageAppUsers.jsx +++ b/frontend/src/Editor/ManageAppUsers.jsx @@ -1,16 +1,15 @@ import React from 'react'; -import { appService, authenticationService } from '@/_services'; +import { appService, appsService, authenticationService } from '@/_services'; import Modal from 'react-bootstrap/Modal'; import { toast } from 'react-hot-toast'; import { CopyToClipboard } from 'react-copy-to-clipboard'; import Skeleton from 'react-loading-skeleton'; -import { debounce } from 'lodash'; -import Textarea from '@/_ui/Textarea'; +import _, { debounce } from 'lodash'; import { withTranslation } from 'react-i18next'; import { Link } from 'react-router-dom'; -import { getPrivateRoute } from '@/_helpers/routes'; +import { getPrivateRoute, replaceEditorURL, getHostURL } from '@/_helpers/routes'; +import { validateName } from '@/_helpers/utils'; import SolidIcon from '@/_ui/Icon/SolidIcons'; -import { getSubpath } from '@/_helpers/utils'; class ManageAppUsersComponent extends React.Component { constructor(props) { @@ -20,14 +19,32 @@ class ManageAppUsersComponent extends React.Component { this.state = { showModal: false, app: { ...props.app }, - slugError: null, isLoading: true, isSlugVerificationInProgress: false, addingUser: false, newUser: {}, + newSlug: { + value: null, + error: '', + }, + isSlugUpdated: false, }; } + /* + Only will fail for existed apps before the app/workspace url revamp which has + special chars or spaces in their app slugs + */ + validateThePreExistingSlugs = () => { + const existedSlugErrors = validateName(this.props.slug, 'App slug', true, false, false, false); + this.setState({ + newSlug: { + value: this.props.slug, + error: existedSlugErrors.errorMsg, + }, + }); + }; + componentDidMount() { const appId = this.props.app.id; this.fetchAppUsers(); @@ -35,7 +52,7 @@ class ManageAppUsersComponent extends React.Component { } fetchAppUsers = () => { - appService + appsService .getAppUsers(this.props.app.id) .then((data) => this.setState({ @@ -52,6 +69,12 @@ class ManageAppUsersComponent extends React.Component { hideModal = () => { this.setState({ showModal: false, + newSlug: { + value: this.props.slug, + error: '', + }, + isSlugVerificationInProgress: false, + isSlugUpdated: false, }); }; @@ -82,7 +105,7 @@ class ManageAppUsersComponent extends React.Component { }); // eslint-disable-next-line no-unused-vars - appService + appsService .setVisibility(this.state.app.id, newState) .then(() => { this.setState({ @@ -107,42 +130,78 @@ class ManageAppUsersComponent extends React.Component { }); }; - handleSetSlug = (event) => { - const newSlug = event.target.value || this.props.app.id; - this.setState({ isSlugVerificationInProgress: true }); - - appService - .setSlug(this.state.app.id, newSlug) - .then(() => { - this.setState({ - slugError: null, - isSlugVerificationInProgress: false, - }); - this.props.handleSlugChange(newSlug); - }) - .catch(({ error }) => { - this.setState({ - slugError: error, - isSlugVerificationInProgress: false, - }); - }); - }; - delayedSlugChange = debounce((e) => { - this.handleSetSlug(e); + this.handleInputChange(e.target.value, 'slug'); }, 500); + handleInputChange = (value, field) => { + this.setState({ + newSlug: { + value: this.state.newSlug?.value, + error: '', + isSlugUpdated: false, + }, + }); + + const error = validateName(value, `App ${field}`, true, false, !(field === 'slug'), !(field === 'slug')); + + if (!_.isEmpty(value) && value !== this.props.slug && _.isEmpty(error.errorMsg)) { + this.setState({ + isSlugVerificationInProgress: true, + }); + appsService + .setSlug(this.state.app.id, value) + .then(() => { + this.setState({ + newSlug: { + value: value, + error: '', + }, + isSlugVerificationInProgress: false, + isSlugUpdated: true, + }); + this.props.handleSlugChange(value); + replaceEditorURL(value, this.props.pageHandle); + }) + .catch(({ error }) => { + this.setState({ + newSlug: { + value, + error, + }, + isSlugVerificationInProgress: false, + isSlugUpdated: false, + }); + }); + } else { + this.setState({ + newSlug: { + value, + error: error?.errorMsg, + }, + isSlugVerificationInProgress: false, + isSlugUpdated: false, + }); + } + }; + render() { - const { isLoading, app, slugError, isSlugVerificationInProgress } = this.state; + const { isLoading, app, isSlugVerificationInProgress, newSlug, isSlugUpdated } = this.state; const appId = app.id; - const appLink = `${window.public_config?.TOOLJET_HOST}${getSubpath() ? getSubpath() : ''}/applications/`; + const appLink = `${getHostURL()}/applications/`; const shareableLink = appLink + (this.props.slug || appId); - const slugButtonClass = isSlugVerificationInProgress ? '' : slugError !== null ? 'is-invalid' : 'is-valid'; + const slugButtonClass = !_.isEmpty(newSlug.error) ? 'is-invalid' : 'is-valid'; const embeddableLink = ``; return ( -
- this.setState({ showModal: true })}> +
+ { + this.validateThePreExistingSlugs(); + this.setState({ showModal: true }); + }} + >
) : ( -
+