mirror of
https://github.com/ToolJet/ToolJet
synced 2026-05-23 17:08:34 +00:00
Merge pull request #7957 from ToolJet/main
hotfix: merge develop to main
This commit is contained in:
commit
83d585a79a
108 changed files with 2901 additions and 1547 deletions
2
.version
2
.version
|
|
@ -1 +1 @@
|
|||
2.21.1
|
||||
2.22.0
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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"]',
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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.",
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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", () => {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
2.21.1
|
||||
2.22.0
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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${
|
||||
|
|
|
|||
|
|
@ -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 {
|
|||
/>
|
||||
<Route
|
||||
exact
|
||||
path="/:workspaceId/apps/:id/:pageHandle?/*"
|
||||
path="/:workspaceId/apps/:slug/:pageHandle?/*"
|
||||
element={
|
||||
<PrivateRoute>
|
||||
<AppLoader switchDarkMode={this.switchDarkMode} darkMode={darkMode} />
|
||||
</PrivateRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path="/applications/:id/versions/:versionId/:pageHandle?"
|
||||
element={
|
||||
<PrivateRoute>
|
||||
<Viewer switchDarkMode={this.switchDarkMode} darkMode={darkMode} />
|
||||
</PrivateRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path="/applications/:slug/:pageHandle?"
|
||||
|
|
|
|||
|
|
@ -1,69 +1,10 @@
|
|||
import React, { useEffect } from 'react';
|
||||
import React from 'react';
|
||||
import { withTranslation } from 'react-i18next';
|
||||
import { appService, organizationService, authenticationService } from '@/_services';
|
||||
import { Editor } from '../Editor/Editor';
|
||||
import { RealtimeEditor } from '@/Editor/RealtimeEditor';
|
||||
import config from 'config';
|
||||
import { safelyParseJSON, stripTrailingSlash, redirectToDashboard, getSubpath, getWorkspaceId } from '@/_helpers/utils';
|
||||
import { toast } from 'react-hot-toast';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import _ from 'lodash';
|
||||
|
||||
const AppLoaderComponent = (props) => {
|
||||
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 ? <RealtimeEditor {...props} /> : <Editor {...props} />;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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 },
|
||||
})
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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 },
|
||||
})
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
<CommentBody
|
||||
socket={socket}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import moment from 'moment';
|
|||
import useRouter from '@/_hooks/use-router';
|
||||
|
||||
import Spinner from '@/_ui/Spinner';
|
||||
import { getPathname } from '@/_helpers/routes';
|
||||
|
||||
const Content = ({ notifications, loading, darkMode }) => {
|
||||
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}`,
|
||||
});
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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 },
|
||||
})
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<div className="editor wrapper">
|
||||
|
|
@ -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 && (
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
)}
|
||||
<div id="" className={cx({ 'dark-theme': darkMode, disabled: isVersionReleased })}>
|
||||
<div id="" className={cx({ 'dark-theme': darkMode })}>
|
||||
<div bsPrefix="global-settings-popover">
|
||||
<HeaderSection darkMode={darkMode}>
|
||||
<HeaderSection.PanelHeader title="Global settings" />
|
||||
</HeaderSection>
|
||||
<div style={{ padding: '12px 16px' }}>
|
||||
<div className="card-body">
|
||||
<div className="app-slug-container">
|
||||
<div className="row">
|
||||
<div className="col tj-app-input input-with-icon">
|
||||
<label className="field-name">Unique app slug</label>
|
||||
<input
|
||||
type="text"
|
||||
className={`form-control ${slug?.error ? 'is-invalid' : 'is-valid'} slug-input`}
|
||||
placeholder={t('editor.appSlug', 'Unique app slug')}
|
||||
maxLength={50}
|
||||
onChange={(e) => {
|
||||
e.persist();
|
||||
delayedSlugChange(e.target.value, 'slug');
|
||||
}}
|
||||
data-cy="app-slug-input-field"
|
||||
defaultValue={oldSlug}
|
||||
/>
|
||||
{isSlugUpdated && (
|
||||
<div className="icon-container">
|
||||
<svg width="15" height="10" viewBox="0 0 15 10" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M14.256 0.244078C14.5814 0.569515 14.5814 1.09715 14.256 1.42259L5.92263 9.75592C5.59719 10.0814 5.06956 10.0814 4.74412 9.75592L0.577452 5.58926C0.252015 5.26382 0.252015 4.73618 0.577452 4.41074C0.902889 4.08531 1.43053 4.08531 1.75596 4.41074L5.33337 7.98816L13.0775 0.244078C13.4029 -0.0813592 13.9305 -0.0813592 14.256 0.244078Z"
|
||||
fill="#46A758"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
)}
|
||||
{slug?.error ? (
|
||||
<label className="label tj-input-error">{slug?.error || ''}</label>
|
||||
) : isSlugUpdated ? (
|
||||
<label className="label label-success">{`Slug accepted!`}</label>
|
||||
) : (
|
||||
<label className="label label-info">{`URL-friendly 'slug' consists of lowercase letters, numbers, and hyphens`}</label>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="row">
|
||||
<div className="col modal-main tj-app-input">
|
||||
<label className="field-name">App link</label>
|
||||
<div className={`tj-text-input break-all ${darkMode ? 'dark' : ''}`}>
|
||||
{!slugProgress ? (
|
||||
`${getHostURL()}/${getWorkspaceId()}/apps/${slug?.value || oldSlug || ''}`
|
||||
) : (
|
||||
<div className="d-flex gap-2">
|
||||
<div class="spinner-border text-secondary workspace-spinner" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
{`Updating link`}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<label className="label label-success label-updated">
|
||||
{isSlugUpdated ? `Link updated successfully!` : ''}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ padding: '12px 16px' }} className={cx({ disabled: isVersionReleased })}>
|
||||
<div className="tj-text-xsm color-slate12 ">
|
||||
<div className="d-flex mb-3">
|
||||
<span data-cy={`label-hide-header-for-launched-apps`}>
|
||||
|
|
|
|||
|
|
@ -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({
|
|||
<ManageAppUsers
|
||||
app={app}
|
||||
slug={slug}
|
||||
M={M}
|
||||
handleSlugChange={handleSlugChange}
|
||||
darkMode={darkMode}
|
||||
handleSlugChange={handleSlugChange}
|
||||
pageHandle={currentState?.page?.handle}
|
||||
M={M}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<LeftSidebarItem
|
||||
commentBadge={notifications?.length > 0}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
),
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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 = `<iframe width="560" height="315" src="${appLink}${this.props.slug}" title="Tooljet app - ${this.props.slug}" frameborder="0" allowfullscreen></iframe>`;
|
||||
|
||||
return (
|
||||
<div title="Share" className="editor-header-icon tj-secondary-btn" data-cy="share-button-link">
|
||||
<span className="d-flex" onClick={() => this.setState({ showModal: true })}>
|
||||
<div title="Share" className="manage-app-users editor-header-icon tj-secondary-btn" data-cy="share-button-link">
|
||||
<span
|
||||
className="d-flex"
|
||||
onClick={() => {
|
||||
this.validateThePreExistingSlugs();
|
||||
this.setState({ showModal: true });
|
||||
}}
|
||||
>
|
||||
<SolidIcon name="share" width="14" className="cursor-pointer" fill="#3E63DD" />
|
||||
</span>
|
||||
<Modal
|
||||
|
|
@ -168,96 +227,159 @@ class ManageAppUsersComponent extends React.Component {
|
|||
<Skeleton count={5} />
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
<div class="shareable-link-container">
|
||||
<div className="make-public mb-3">
|
||||
<div className="form-check form-switch">
|
||||
<div className="form-check form-switch d-flex align-items-center">
|
||||
<input
|
||||
className="form-check-input color-slate12"
|
||||
className="form-check-input"
|
||||
type="checkbox"
|
||||
onClick={this.toggleAppVisibility}
|
||||
checked={this.state.app.is_public}
|
||||
disabled={this.state.ischangingVisibility}
|
||||
data-cy="make-public-app-toggle"
|
||||
/>
|
||||
<span className="form-check-label" data-cy="make-public-app-label">
|
||||
{this.props.t('editor.shareModal.makeApplicationPublic', 'Make application public?')}
|
||||
<span className="form-check-label field-name" data-cy="make-public-app-label">
|
||||
{this.props.t('editor.shareModal.makeApplicationPublic', 'Make application public')}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="shareable-link mb-3">
|
||||
<label className="form-label" data-cy="shareable-app-link-label">
|
||||
<small>
|
||||
{this.props.t('editor.shareModal.shareableLink', 'Get shareable link for this application')}
|
||||
</small>
|
||||
<div className="shareable-link tj-app-input mb-2">
|
||||
<label data-cy="shareable-app-link-label" className="field-name">
|
||||
{this.props.t('editor.shareModal.shareableLink', 'Shareable app link')}
|
||||
</label>
|
||||
<div className="input-group">
|
||||
<span className="input-group-text" data-cy="app-link">
|
||||
<span className="input-group-text applink-text flex-grow-1 slug-ellipsis" data-cy="app-link">
|
||||
{appLink}
|
||||
</span>
|
||||
<div className="input-with-icon app-name-slug-input">
|
||||
<div className="input-with-icon">
|
||||
<input
|
||||
type="text"
|
||||
className={`form-control color-slate12 ${slugButtonClass}`}
|
||||
placeholder={appId}
|
||||
className={`form-control form-control-sm ${slugButtonClass}`}
|
||||
placeholder={this.props.slug}
|
||||
maxLength={50}
|
||||
onChange={(e) => {
|
||||
e.persist();
|
||||
this.delayedSlugChange(e);
|
||||
}}
|
||||
style={{ maxWidth: '150px' }}
|
||||
defaultValue={this.props.slug}
|
||||
data-cy="app-name-slug-input"
|
||||
/>
|
||||
{isSlugVerificationInProgress && (
|
||||
<div className="icon-container">
|
||||
<div className="spinner-border text-azure spinner-border-sm" role="status"></div>
|
||||
<div class="spinner-border text-secondary " role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="icon-container">
|
||||
{newSlug?.error ? (
|
||||
<svg
|
||||
width="21"
|
||||
height="20"
|
||||
viewBox="0 0 21 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M3.94252 3.61195C4.31445 3.24003 4.91746 3.24003 5.28939 3.61195L10.3302 8.6528L15.3711 3.61195C15.743 3.24003 16.346 3.24003 16.718 3.61195C17.0899 3.98388 17.0899 4.5869 16.718 4.95882L11.6771 9.99967L16.718 15.0405C17.0899 15.4125 17.0899 16.0155 16.718 16.3874C16.346 16.7593 15.743 16.7593 15.3711 16.3874L10.3302 11.3465L5.28939 16.3874C4.91746 16.7593 4.31445 16.7593 3.94252 16.3874C3.57059 16.0155 3.57059 15.4125 3.94252 15.0405L8.98337 9.99967L3.94252 4.95882C3.57059 4.5869 3.57059 3.98388 3.94252 3.61195Z"
|
||||
fill="#E54D2E"
|
||||
/>
|
||||
</svg>
|
||||
) : (
|
||||
isSlugUpdated &&
|
||||
!isSlugVerificationInProgress && (
|
||||
<svg
|
||||
width="21"
|
||||
height="20"
|
||||
viewBox="0 0 21 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M17.5859 5.24408C17.9114 5.56951 17.9114 6.09715 17.5859 6.42259L9.25259 14.7559C8.92715 15.0814 8.39951 15.0814 8.07407 14.7559L3.90741 10.5893C3.58197 10.2638 3.58197 9.73618 3.90741 9.41074C4.23284 9.08531 4.76048 9.08531 5.08592 9.41074L8.66333 12.9882L16.4074 5.24408C16.7328 4.91864 17.2605 4.91864 17.5859 5.24408Z"
|
||||
fill="#46A758"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<CopyToClipboard text={shareableLink} onCopy={() => toast.success('Link copied to clipboard')}>
|
||||
<button className="btn-sm tj-tertiary-btn" data-cy="copy-app-link-button">
|
||||
{this.props.t('editor.shareModal.copy', 'copy')}
|
||||
</button>
|
||||
</CopyToClipboard>
|
||||
<div className="invalid-feedback">{slugError}</div>
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
{(this.state.app.is_public || window?.public_config?.ENABLE_PRIVATE_APP_EMBED === 'true') && (
|
||||
<div className="shareable-link mb-3">
|
||||
<label className="form-label" data-cy="iframe-link-label">
|
||||
<small>
|
||||
{this.props.t('editor.shareModal.embeddableLink', 'Get embeddable link for this application')}
|
||||
</small>
|
||||
</label>
|
||||
<div className="input-group">
|
||||
<Textarea
|
||||
disabled
|
||||
className={`input-with-icon ${this.props.darkMode && 'text-light'}`}
|
||||
rows={5}
|
||||
value={embeddableLink}
|
||||
data-cy="iframe-link"
|
||||
/>
|
||||
<CopyToClipboard
|
||||
text={embeddableLink}
|
||||
onCopy={() => toast.success('Embeddable link copied to clipboard')}
|
||||
>
|
||||
<button className="tj-tertiary-btn btn-sm" data-cy="iframe-link-copy-button">
|
||||
{this.props.t('editor.shareModal.copy', 'copy')}
|
||||
</button>
|
||||
<span className="input-group-text">
|
||||
<CopyToClipboard text={shareableLink} onCopy={() => toast.success('Link copied to clipboard')}>
|
||||
<svg
|
||||
className="cursor-pointer"
|
||||
width="17"
|
||||
height="18"
|
||||
viewBox="0 0 17 18"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M9.11154 5.18031H5.88668V4.83302C5.88668 3.29859 7.13059 2.05469 8.66502 2.05469H12.8325C14.3669 2.05469 15.6109 3.29859 15.6109 4.83302V9.00052C15.6109 10.535 14.3669 11.7789 12.8325 11.7789H12.4852V8.554C12.4852 6.69076 10.9748 5.18031 9.11154 5.18031Z"
|
||||
fill="#889096"
|
||||
/>
|
||||
<path
|
||||
d="M8.66502 15.9464H4.49752C2.96309 15.9464 1.71918 14.7025 1.71918 13.168V9.00052C1.71918 7.46609 2.96309 6.22219 4.49752 6.22219H8.66502C10.1994 6.22219 11.4434 7.46609 11.4434 9.00052V13.168C11.4434 14.7025 10.1994 15.9464 8.66502 15.9464Z"
|
||||
fill="#889096"
|
||||
/>
|
||||
</svg>
|
||||
</CopyToClipboard>
|
||||
</div>
|
||||
</span>
|
||||
</div>
|
||||
{newSlug?.error ? (
|
||||
<label className="label tj-input-error">{newSlug?.error || ''}</label>
|
||||
) : isSlugUpdated ? (
|
||||
<label className="label label-success">{`Slug accepted!`}</label>
|
||||
) : (
|
||||
<label className="label label-info">{`URL-friendly 'slug' consists of lowercase letters, numbers, and hyphens`}</label>
|
||||
)}
|
||||
</div>
|
||||
{(this.state.app.is_public || window?.public_config?.ENABLE_PRIVATE_APP_EMBED === 'true') && (
|
||||
<div className="tj-app-input">
|
||||
<label className="field-name">Embedded app link</label>
|
||||
<span className={`tj-text-input justify-content-between ${this.props.darkMode ? 'dark' : ''}`}>
|
||||
<span>{embeddableLink}</span>
|
||||
<span className="copy-container">
|
||||
<CopyToClipboard text={embeddableLink} onCopy={() => toast.success('Link copied to clipboard')}>
|
||||
<svg
|
||||
className="cursor-pointer"
|
||||
width="17"
|
||||
height="18"
|
||||
viewBox="0 0 17 18"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M9.11154 5.18031H5.88668V4.83302C5.88668 3.29859 7.13059 2.05469 8.66502 2.05469H12.8325C14.3669 2.05469 15.6109 3.29859 15.6109 4.83302V9.00052C15.6109 10.535 14.3669 11.7789 12.8325 11.7789H12.4852V8.554C12.4852 6.69076 10.9748 5.18031 9.11154 5.18031Z"
|
||||
fill="#889096"
|
||||
/>
|
||||
<path
|
||||
d="M8.66502 15.9464H4.49752C2.96309 15.9464 1.71918 14.7025 1.71918 13.168V9.00052C1.71918 7.46609 2.96309 6.22219 4.49752 6.22219H8.66502C10.1994 6.22219 11.4434 7.46609 11.4434 9.00052V13.168C11.4434 14.7025 10.1994 15.9464 8.66502 15.9464Z"
|
||||
fill="#889096"
|
||||
/>
|
||||
</svg>
|
||||
</CopyToClipboard>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</Modal.Body>
|
||||
|
||||
<Modal.Footer>
|
||||
<Modal.Footer className="manage-app-users-footer">
|
||||
{this.isUserAdmin && (
|
||||
<Link
|
||||
to={getPrivateRoute('workspace_settings')}
|
||||
target="_blank"
|
||||
className="tj-primary-btn tj-base-btn"
|
||||
className={`btn border-0 default-secondary-button float-right1`}
|
||||
data-cy="manage-users-button"
|
||||
>
|
||||
Manage users
|
||||
|
|
|
|||
|
|
@ -2,7 +2,8 @@ import React, { useState, useEffect, useContext } from 'react';
|
|||
import { CodeHinter } from '@/Editor/CodeBuilder/CodeHinter';
|
||||
import { TooljetDatabaseContext } from '@/TooljetDatabase/index';
|
||||
import Select from '@/_ui/Select';
|
||||
import { isEmpty, uniqueId } from 'lodash';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { isEmpty } from 'lodash';
|
||||
import { useMounted } from '@/_hooks/use-mount';
|
||||
import { ButtonSolid } from '@/_ui/AppButton/AppButton';
|
||||
|
||||
|
|
@ -40,7 +41,7 @@ export const CreateRow = React.memo(({ optionchanged, options, darkMode }) => {
|
|||
}
|
||||
const existingColumnOption = Object.values ? Object.values(columnOptions) : [];
|
||||
const emptyColumnOption = { column: '', value: '' };
|
||||
handleColumnOptionChange({ ...existingColumnOption, ...{ [uniqueId()]: emptyColumnOption } });
|
||||
handleColumnOptionChange({ ...existingColumnOption, ...{ [uuidv4()]: emptyColumnOption } });
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import React, { useContext } from 'react';
|
||||
import { TooljetDatabaseContext } from '@/TooljetDatabase/index';
|
||||
import { isEmpty, uniqueId } from 'lodash';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { isEmpty } from 'lodash';
|
||||
import { CodeHinter } from '@/Editor/CodeBuilder/CodeHinter';
|
||||
import Select from '@/_ui/Select';
|
||||
import { operators } from '@/TooljetDatabase/constants';
|
||||
|
|
@ -18,7 +19,7 @@ export const DeleteRows = React.memo(({ darkMode }) => {
|
|||
function addNewFilterConditionPair() {
|
||||
const existingFilters = deleteRowsOptions?.where_filters ? Object.values(deleteRowsOptions?.where_filters) : [];
|
||||
const emptyFilter = { column: '', operator: '', value: '' };
|
||||
const newFilter = { ...emptyFilter, ...{ id: uniqueId() } };
|
||||
const newFilter = { ...emptyFilter, ...{ id: uuidv4() } };
|
||||
handleWhereFiltersChange({ ...existingFilters, ...{ [newFilter.id]: newFilter } });
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
import React, { useContext } from 'react';
|
||||
import { CodeHinter } from '@/Editor/CodeBuilder/CodeHinter';
|
||||
import { TooljetDatabaseContext } from '@/TooljetDatabase/index';
|
||||
import { isEmpty, uniqueId } from 'lodash';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { isEmpty } from 'lodash';
|
||||
import Select from '@/_ui/Select';
|
||||
import { operators } from '@/TooljetDatabase/constants';
|
||||
import { isOperatorOptions } from './util';
|
||||
|
|
@ -21,14 +22,14 @@ export const ListRows = React.memo(({ darkMode }) => {
|
|||
function addNewFilterConditionPair() {
|
||||
const existingFilters = listRowsOptions?.where_filters ? Object.values(listRowsOptions?.where_filters) : [];
|
||||
const emptyFilter = { column: '', operator: '', value: '' };
|
||||
const newFilter = { ...emptyFilter, ...{ id: uniqueId() } };
|
||||
const newFilter = { ...emptyFilter, ...{ id: uuidv4() } };
|
||||
handleWhereFiltersChange({ ...existingFilters, ...{ [newFilter.id]: newFilter } });
|
||||
}
|
||||
|
||||
function addNewSortConditionPair() {
|
||||
const existingFilters = listRowsOptions?.order_filters ? Object.values(listRowsOptions?.order_filters) : [];
|
||||
const emptyFilter = { column: '', order: '' };
|
||||
const newFilter = { ...emptyFilter, ...{ id: uniqueId() } };
|
||||
const newFilter = { ...emptyFilter, ...{ id: uuidv4() } };
|
||||
handleOrderFiltersChange({ ...existingFilters, ...{ [newFilter.id]: newFilter } });
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -160,7 +160,7 @@ const ToolJetDbOperations = ({ optionchanged, options, darkMode, isHorizontalLay
|
|||
}, [options['join_table']?.['joins'], tables]);
|
||||
|
||||
useEffect(() => {
|
||||
selectedTableId && fetchTableInformation(selectedTableId);
|
||||
selectedTableId && fetchTableInformation(selectedTableId, false, tables);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [selectedTableId]);
|
||||
|
||||
|
|
@ -266,6 +266,10 @@ const ToolJetDbOperations = ({ optionchanged, options, darkMode, isHorizontalLay
|
|||
});
|
||||
};
|
||||
|
||||
const findTableDetailsWithTableList = (tableId, tableList) => {
|
||||
return tableList.find((table) => table.table_id == tableId);
|
||||
};
|
||||
|
||||
const findTableDetails = (tableId) => {
|
||||
return tables.find((table) => table.table_id == tableId);
|
||||
};
|
||||
|
|
@ -329,15 +333,16 @@ const ToolJetDbOperations = ({ optionchanged, options, darkMode, isHorizontalLay
|
|||
}
|
||||
|
||||
if (Array.isArray(data?.result)) {
|
||||
setTables(
|
||||
const tableList =
|
||||
data.result.map((table) => {
|
||||
return { table_name: table.table_name, table_id: table.id };
|
||||
}) || []
|
||||
);
|
||||
}) || [];
|
||||
|
||||
setTables(tableList);
|
||||
const selectedTableInfo = data.result.find((table) => table.id === options['table_id']);
|
||||
if (selectedTableInfo) {
|
||||
setSelectedTableId(selectedTableInfo.id);
|
||||
fetchTableInformation(selectedTableInfo.id);
|
||||
fetchTableInformation(selectedTableInfo.id, false, tableList);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
@ -345,8 +350,8 @@ const ToolJetDbOperations = ({ optionchanged, options, darkMode, isHorizontalLay
|
|||
/**
|
||||
* TODO: This function to be removed and replaced with loadTableInformation function everywhere
|
||||
*/
|
||||
const fetchTableInformation = async (tableId, isNewTableAdded) => {
|
||||
const tableDetails = findTableDetails(tableId);
|
||||
const fetchTableInformation = async (tableId, isNewTableAdded, tableList) => {
|
||||
const tableDetails = findTableDetailsWithTableList(tableId, tableList);
|
||||
if (tableDetails?.table_name) {
|
||||
const { table_name } = tableDetails;
|
||||
const { error, data } = await tooljetDatabaseService.viewTable(organizationId, table_name);
|
||||
|
|
@ -402,7 +407,7 @@ const ToolJetDbOperations = ({ optionchanged, options, darkMode, isHorizontalLay
|
|||
|
||||
const handleTableNameSelect = (tableId) => {
|
||||
setSelectedTableId(tableId);
|
||||
fetchTableInformation(tableId, true);
|
||||
fetchTableInformation(tableId, true, tables);
|
||||
optionchanged('organization_id', organizationId);
|
||||
optionchanged('table_id', tableId);
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,8 @@ import { CodeHinter } from '@/Editor/CodeBuilder/CodeHinter';
|
|||
import { TooljetDatabaseContext } from '@/TooljetDatabase/index';
|
||||
import Select from '@/_ui/Select';
|
||||
import { operators } from '@/TooljetDatabase/constants';
|
||||
import { isEmpty, uniqueId } from 'lodash';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { isEmpty } from 'lodash';
|
||||
import { isOperatorOptions } from './util';
|
||||
import { ButtonSolid } from '@/_ui/AppButton/AppButton';
|
||||
|
||||
|
|
@ -34,7 +35,7 @@ export const UpdateRows = React.memo(({ darkMode }) => {
|
|||
|
||||
const existingColumnOption = Object.values ? Object.values(updateRowsOptions?.columns) : [];
|
||||
const emptyColumnOption = { column: '', value: '' };
|
||||
handleColumnOptionChange({ ...existingColumnOption, ...{ [uniqueId()]: emptyColumnOption } });
|
||||
handleColumnOptionChange({ ...existingColumnOption, ...{ [uuidv4()]: emptyColumnOption } });
|
||||
}
|
||||
|
||||
function handleWhereFiltersChange(filters) {
|
||||
|
|
@ -44,7 +45,7 @@ export const UpdateRows = React.memo(({ darkMode }) => {
|
|||
function addNewFilterConditionPair() {
|
||||
const existingFilters = updateRowsOptions?.where_filters ? Object.values(updateRowsOptions?.where_filters) : [];
|
||||
const emptyFilter = { column: '', operator: '', value: '' };
|
||||
const newFilter = { ...emptyFilter, ...{ id: uniqueId() } };
|
||||
const newFilter = { ...emptyFilter, ...{ id: uuidv4() } };
|
||||
handleWhereFiltersChange({ ...existingFilters, ...{ [newFilter.id]: newFilter } });
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,8 +4,6 @@ import config from 'config';
|
|||
import { RoomProvider } from '@y-presence/react';
|
||||
import Spinner from '@/_ui/Spinner';
|
||||
import { Editor } from '@/Editor';
|
||||
import useRouter from '@/_hooks/use-router';
|
||||
import { useParams } from 'react-router-dom';
|
||||
const Y = require('yjs');
|
||||
const psl = require('psl');
|
||||
const { WebsocketProvider } = require('y-websocket');
|
||||
|
|
@ -28,18 +26,15 @@ const getWebsocketUrl = () => {
|
|||
};
|
||||
|
||||
export const RealtimeEditor = (props) => {
|
||||
const params = useParams();
|
||||
const appId = params.id;
|
||||
const appId = props.id;
|
||||
const [provider, setProvider] = React.useState();
|
||||
const router = useRouter();
|
||||
|
||||
React.useEffect(() => {
|
||||
/* TODO: when we convert the editor.jsx to fn component. please try to avoid this extra call */
|
||||
const domain = psl.parse(window.location.host).domain;
|
||||
document.cookie = domain ? `domain=.${domain}; path=/` : `path=/`;
|
||||
document.cookie = domain
|
||||
? `app_id=${router.query.id}; domain=.${domain}; path=/`
|
||||
: `app_id=${router.query.id}; path=/`;
|
||||
document.cookie = `app_id=${router.query.id}; domain=.${domain}; path=/`;
|
||||
document.cookie = domain ? `app_id=${appId}; domain=.${domain}; path=/` : `app_id=${appId}; path=/`;
|
||||
document.cookie = `app_id=${appId}; domain=.${domain}; path=/`;
|
||||
setProvider(new WebsocketProvider(getWebsocketUrl(), 'yjs', ydoc));
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [appId]);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import React, { useState } from 'react';
|
||||
import cx from 'classnames';
|
||||
import { appService } from '@/_services';
|
||||
import { appsService } from '@/_services';
|
||||
import { toast } from 'react-hot-toast';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useAppVersionStore } from '@/_stores/appVersionStore';
|
||||
|
|
@ -30,7 +30,7 @@ export const ReleaseVersionButton = function DeployVersionButton({
|
|||
setShowPageDeletionConfirmation(false);
|
||||
setIsReleasing(true);
|
||||
saveEditingVersion();
|
||||
appService
|
||||
appsService
|
||||
.saveApp(appId, {
|
||||
name: appName,
|
||||
current_version_id: editingVersion.id,
|
||||
|
|
|
|||
|
|
@ -1,10 +1,9 @@
|
|||
import React from 'react';
|
||||
import {
|
||||
appService,
|
||||
appsService,
|
||||
authenticationService,
|
||||
orgEnvironmentVariableService,
|
||||
orgEnvironmentConstantService,
|
||||
organizationService,
|
||||
} from '@/_services';
|
||||
import { DndProvider } from 'react-dnd';
|
||||
import { HTML5Backend } from 'react-dnd-html5-backend';
|
||||
|
|
@ -23,27 +22,20 @@ import {
|
|||
import queryString from 'query-string';
|
||||
import ViewerLogoIcon from './Icons/viewer-logo.svg';
|
||||
import { DataSourceTypes } from './DataSourceManager/SourceComponents';
|
||||
import {
|
||||
resolveReferences,
|
||||
safelyParseJSON,
|
||||
stripTrailingSlash,
|
||||
getSubpath,
|
||||
excludeWorkspaceIdFromURL,
|
||||
isQueryRunnable,
|
||||
redirectToDashboard,
|
||||
getWorkspaceId,
|
||||
} from '@/_helpers/utils';
|
||||
import { resolveReferences, isQueryRunnable } from '@/_helpers/utils';
|
||||
import { withTranslation } from 'react-i18next';
|
||||
import _ from 'lodash';
|
||||
import { Navigate } from 'react-router-dom';
|
||||
import Spinner from '@/_ui/Spinner';
|
||||
import { toast } from 'react-hot-toast';
|
||||
import { withRouter } from '@/_hoc/withRouter';
|
||||
import { useEditorStore } from '@/_stores/editorStore';
|
||||
import { setCookie } from '@/_helpers/cookie';
|
||||
import { useDataQueriesStore } from '@/_stores/dataQueriesStore';
|
||||
import { useCurrentStateStore } from '@/_stores/currentStateStore';
|
||||
import { shallow } from 'zustand/shallow';
|
||||
import { useAppDataStore } from '@/_stores/appDataStore';
|
||||
import { getPreviewQueryParams, redirectToDashboard } from '@/_helpers/routes';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
class ViewerComponent extends React.Component {
|
||||
constructor(props) {
|
||||
|
|
@ -52,15 +44,10 @@ class ViewerComponent extends React.Component {
|
|||
const deviceWindowWidth = window.screen.width - 5;
|
||||
|
||||
const slug = this.props.params.slug;
|
||||
const appId = this.props.params.id;
|
||||
const versionId = this.props.params.versionId;
|
||||
|
||||
this.subscription = null;
|
||||
|
||||
this.state = {
|
||||
slug,
|
||||
appId,
|
||||
versionId,
|
||||
deviceWindowWidth,
|
||||
currentUser: null,
|
||||
isLoading: true,
|
||||
|
|
@ -68,9 +55,6 @@ class ViewerComponent extends React.Component {
|
|||
appDefinition: { pages: {} },
|
||||
queryConfirmationList: [],
|
||||
isAppLoaded: false,
|
||||
errorAppId: null,
|
||||
errorVersionId: null,
|
||||
errorDetails: null,
|
||||
pages: {},
|
||||
homepage: null,
|
||||
};
|
||||
|
|
@ -253,8 +237,8 @@ class ViewerComponent extends React.Component {
|
|||
return variables;
|
||||
};
|
||||
|
||||
loadApplicationBySlug = (slug) => {
|
||||
appService
|
||||
loadApplicationBySlug = (slug, authentication_failed = false) => {
|
||||
appsService
|
||||
.getAppBySlug(slug)
|
||||
.then((data) => {
|
||||
this.setStateForApp(data);
|
||||
|
|
@ -263,90 +247,42 @@ class ViewerComponent extends React.Component {
|
|||
})
|
||||
.catch((error) => {
|
||||
this.setState({
|
||||
errorDetails: error,
|
||||
errorAppId: slug,
|
||||
errorVersionId: null,
|
||||
isLoading: false,
|
||||
});
|
||||
if (authentication_failed && error?.statusCode === 404) {
|
||||
/* User is not authenticated. but the app url is wrong */
|
||||
toast.error("Couldn't find the app. \n Please verify the app URL again.");
|
||||
setTimeout(() => {
|
||||
redirectToDashboard();
|
||||
}, 3000);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
loadApplicationByVersion = (appId, versionId) => {
|
||||
appService
|
||||
appsService
|
||||
.getAppByVersion(appId, versionId)
|
||||
.then((data) => {
|
||||
this.setStateForApp(data);
|
||||
this.setStateForContainer(data);
|
||||
})
|
||||
.catch((error) => {
|
||||
.catch(() => {
|
||||
this.setState({
|
||||
errorDetails: error,
|
||||
errorAppId: appId,
|
||||
errorVersionId: versionId,
|
||||
isLoading: false,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
switchOrganization = (orgId, appId, versionId) => {
|
||||
const path = `/applications/${appId}${versionId ? `/versions/${versionId}` : ''}`;
|
||||
const sub_path = window?.public_config?.SUB_PATH ? stripTrailingSlash(window?.public_config?.SUB_PATH) : '';
|
||||
|
||||
organizationService.switchOrganization(orgId).then(
|
||||
() => {
|
||||
window.location.href = `${sub_path}${path}`;
|
||||
},
|
||||
() => {
|
||||
return (window.location.href = `${sub_path}/login/${orgId}?redirectTo=${path}`);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
handleError = (errorDetails, appId, versionId) => {
|
||||
try {
|
||||
if (errorDetails?.data) {
|
||||
const statusCode = errorDetails.data?.statusCode;
|
||||
if (statusCode === 403) {
|
||||
const errorObj = safelyParseJSON(errorDetails.data?.message);
|
||||
const currentSessionValue = authenticationService.currentSessionValue;
|
||||
if (
|
||||
errorObj?.organizationId &&
|
||||
this.state.currentUser &&
|
||||
currentSessionValue.current_organization_id !== errorObj?.organizationId
|
||||
) {
|
||||
this.switchOrganization(errorObj?.organizationId, appId, versionId);
|
||||
return;
|
||||
}
|
||||
/* router dom Navigate is not working now. so hard reloading */
|
||||
redirectToDashboard();
|
||||
return <Navigate replace to={'/'} />;
|
||||
} else if (statusCode === 401) {
|
||||
window.location = `${getSubpath() ?? ''}/login${
|
||||
!_.isEmpty(getWorkspaceId()) ? `/${getWorkspaceId()}` : ''
|
||||
}?redirectTo=${this.props.location.pathname}`;
|
||||
} else if (statusCode === 404) {
|
||||
toast.error(errorDetails?.error ?? 'App not found', {
|
||||
position: 'top-center',
|
||||
});
|
||||
} else {
|
||||
redirectToDashboard();
|
||||
return <Navigate replace to={'/'} />;
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
redirectToDashboard();
|
||||
return <Navigate replace to={'/'} />;
|
||||
}
|
||||
};
|
||||
|
||||
setupViewer() {
|
||||
const slug = this.props.params.slug;
|
||||
const appId = this.props.params.id;
|
||||
const versionId = this.props.params.versionId;
|
||||
|
||||
this.subscription = authenticationService.currentSession.subscribe((currentSession) => {
|
||||
if (currentSession?.load_app) {
|
||||
const slug = this.props.params.slug;
|
||||
const appId = this.props.id;
|
||||
const versionId = this.props.versionId;
|
||||
|
||||
if (currentSession?.load_app && slug) {
|
||||
if (currentSession?.group_permissions) {
|
||||
useAppDataStore.getState().actions.setAppId(appId);
|
||||
|
||||
const currentUser = currentSession.current_user;
|
||||
const userVars = {
|
||||
email: currentUser.email,
|
||||
|
|
@ -362,16 +298,12 @@ class ViewerComponent extends React.Component {
|
|||
});
|
||||
this.setState({
|
||||
currentUser,
|
||||
|
||||
userVars,
|
||||
versionId,
|
||||
});
|
||||
slug ? this.loadApplicationBySlug(slug) : this.loadApplicationByVersion(appId, versionId);
|
||||
} else if (currentSession?.authentication_failed && !slug) {
|
||||
const loginPath = (window.public_config?.SUB_PATH || '/') + 'login';
|
||||
const pathname = getSubpath() ? window.location.pathname.replace(getSubpath(), '') : window.location.pathname;
|
||||
window.location.href = loginPath + `?redirectTo=${excludeWorkspaceIdFromURL(pathname)}`;
|
||||
} else {
|
||||
slug && this.loadApplicationBySlug(slug);
|
||||
versionId ? this.loadApplicationByVersion(appId, versionId) : this.loadApplicationBySlug(slug);
|
||||
} else if (currentSession?.authentication_failed) {
|
||||
this.loadApplicationBySlug(slug, true);
|
||||
}
|
||||
}
|
||||
this.setState({ isLoading: false });
|
||||
|
|
@ -508,6 +440,8 @@ class ViewerComponent extends React.Component {
|
|||
|
||||
switchPage = (id, queryParams = []) => {
|
||||
document.getElementById('real-canvas').scrollIntoView();
|
||||
/* Keep default query params for preview */
|
||||
const defaultParams = getPreviewQueryParams();
|
||||
|
||||
if (this.state.currentPageId === id) return;
|
||||
|
||||
|
|
@ -515,11 +449,16 @@ class ViewerComponent extends React.Component {
|
|||
|
||||
const queryParamsString = queryParams.map(([key, value]) => `${key}=${value}`).join('&');
|
||||
|
||||
if (this.state.slug) this.props.navigate(`/applications/${this.state.slug}/${handle}?${queryParamsString}`);
|
||||
else
|
||||
this.props.navigate(
|
||||
`/applications/${this.state.appId}/versions/${this.state.versionId}/${handle}?${queryParamsString}`
|
||||
);
|
||||
this.props.navigate(
|
||||
`/applications/${this.state.slug}/${handle}?${
|
||||
!_.isEmpty(defaultParams) ? `version=${defaultParams.version}` : ''
|
||||
}${queryParamsString ? `${!_.isEmpty(defaultParams) ? '&' : ''}${queryParamsString}` : ''}`,
|
||||
{
|
||||
state: {
|
||||
isSwitchingPage: true,
|
||||
},
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
handleEvent = (eventName, options) => onEvent(this, eventName, options, 'view');
|
||||
|
|
@ -550,9 +489,6 @@ class ViewerComponent extends React.Component {
|
|||
defaultComponentStateComputed,
|
||||
dataQueries,
|
||||
queryConfirmationList,
|
||||
errorAppId,
|
||||
errorVersionId,
|
||||
errorDetails,
|
||||
canvasWidth,
|
||||
} = this.state;
|
||||
|
||||
|
|
@ -585,38 +521,6 @@ class ViewerComponent extends React.Component {
|
|||
</div>
|
||||
);
|
||||
} else {
|
||||
if (errorDetails) {
|
||||
this.handleError(errorDetails, errorAppId, errorVersionId);
|
||||
}
|
||||
|
||||
const pageArray = Object.values(this.state.appDefinition?.pages || {});
|
||||
//checking if page is disabled
|
||||
if (
|
||||
pageArray.find((page) => page.handle === this.props.params.pageHandle)?.disabled &&
|
||||
this.state.currentPageId !== this.state.appDefinition?.homePageId && //Prevent page crashing when home page is disabled
|
||||
this.state.appDefinition?.pages?.[this.state.appDefinition?.homePageId]
|
||||
) {
|
||||
const homeHandle = this.state.appDefinition?.pages?.[this.state.appDefinition?.homePageId]?.handle;
|
||||
let url = `/applications/${this.state.appId}/versions/${this.state.versionId}/${homeHandle}`;
|
||||
if (this.state.slug) {
|
||||
url = `/applications/${this.state.slug}/${homeHandle}`;
|
||||
}
|
||||
return <Navigate to={url} replace />;
|
||||
}
|
||||
|
||||
//checking if page exists
|
||||
if (
|
||||
!pageArray.find((page) => page.handle === this.props.params.pageHandle) &&
|
||||
this.state.appDefinition?.pages?.[this.state.appDefinition?.homePageId]
|
||||
) {
|
||||
const homeHandle = this.state.appDefinition?.pages?.[this.state.appDefinition?.homePageId]?.handle;
|
||||
let url = `/applications/${this.state.appId}/versions/${this.state.versionId}/${homeHandle}`;
|
||||
if (this.state.slug) {
|
||||
url = `/applications/${this.state.slug}/${homeHandle}`;
|
||||
}
|
||||
return <Navigate to={`${url}${this.props.params.pageHandle ? '' : window.location.search}`} replace />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="viewer wrapper">
|
||||
<Confirm
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import Header from './Header';
|
|||
import FolderList from '@/_ui/FolderList/FolderList';
|
||||
import { useEditorStore } from '@/_stores/editorStore';
|
||||
import { shallow } from 'zustand/shallow';
|
||||
import { redirectToDashboard } from '@/_helpers/routes';
|
||||
|
||||
export const ViewerNavigation = ({ isMobileDevice, pages, currentPageId, switchPage, darkMode }) => {
|
||||
if (isMobileDevice) {
|
||||
|
|
@ -164,7 +165,7 @@ const ViewerHeader = ({ showHeader, appName, changeDarkMode, darkMode, pages, cu
|
|||
<Link
|
||||
data-cy="viewer-page-logo"
|
||||
onClick={() => {
|
||||
window.location.href = '/';
|
||||
redirectToDashboard();
|
||||
}}
|
||||
>
|
||||
<LogoIcon />
|
||||
|
|
|
|||
|
|
@ -11,8 +11,8 @@ import { useTranslation } from 'react-i18next';
|
|||
import SolidIcon from '@/_ui/Icon/SolidIcons';
|
||||
import BulkIcon from '@/_ui/Icon/BulkIcons';
|
||||
|
||||
import { getPrivateRoute } from '@/_helpers/routes';
|
||||
import { getSubpath } from '@/_helpers/utils';
|
||||
import { getPrivateRoute, getSubpath } from '@/_helpers/routes';
|
||||
import { validateName } from '@/_helpers/utils';
|
||||
const { defaultIcon } = configs;
|
||||
|
||||
export default function AppCard({
|
||||
|
|
@ -47,6 +47,11 @@ export default function AppCard({
|
|||
[app, appActionModal, currentFolder]
|
||||
);
|
||||
|
||||
const isValidSlug = (slug) => {
|
||||
const validate = validateName(slug, 'slug', true, false, false, false);
|
||||
return validate.status;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
!isMenuOpen && setFocused(!!isHovered);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
|
|
@ -120,7 +125,7 @@ export default function AppCard({
|
|||
<ToolTip message="Open in app builder">
|
||||
<Link
|
||||
to={getPrivateRoute('editor', {
|
||||
id: app.id,
|
||||
slug: isValidSlug(app.slug) ? app.slug : app.id,
|
||||
})}
|
||||
>
|
||||
<button type="button" className="tj-primary-btn edit-button tj-text-xsm" data-cy="edit-button">
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import { default as BootstrapModal } from 'react-bootstrap/Modal';
|
||||
import moment from 'moment';
|
||||
import { appService } from '@/_services/app.service';
|
||||
import { appsService } from '@/_services';
|
||||
import { toast } from 'react-hot-toast';
|
||||
import { ButtonSolid } from '@/_components/AppButton';
|
||||
|
||||
|
|
@ -15,7 +15,7 @@ export default function ExportAppModal({ title, show, closeModal, customClassNam
|
|||
useEffect(() => {
|
||||
async function fetchAppVersions() {
|
||||
try {
|
||||
const fetchVersions = await appService.getVersions(app.id);
|
||||
const fetchVersions = await appsService.getVersions(app.id);
|
||||
const { versions } = fetchVersions;
|
||||
setVersions(versions);
|
||||
} catch (error) {
|
||||
|
|
@ -27,7 +27,7 @@ export default function ExportAppModal({ title, show, closeModal, customClassNam
|
|||
}
|
||||
async function fetchAppTables() {
|
||||
try {
|
||||
const fetchTables = await appService.getTables(app.id);
|
||||
const fetchTables = await appsService.getTables(app.id);
|
||||
const { tables } = fetchTables;
|
||||
setTables(tables);
|
||||
} catch (error) {
|
||||
|
|
@ -57,7 +57,7 @@ export default function ExportAppModal({ title, show, closeModal, customClassNam
|
|||
organization_id: app.organization_id,
|
||||
};
|
||||
|
||||
appService
|
||||
appsService
|
||||
.exportResource(requestBody)
|
||||
.then((data) => {
|
||||
const appName = app.name.replace(/\s+/g, '-').toLowerCase();
|
||||
|
|
|
|||
|
|
@ -12,7 +12,8 @@ import { BreadCrumbContext } from '@/App/App';
|
|||
import { ButtonSolid } from '@/_ui/AppButton/AppButton';
|
||||
import { SearchBox } from '@/_components/SearchBox';
|
||||
import _ from 'lodash';
|
||||
import { validateName, handleHttpErrorMessages } from '@/_helpers/utils';
|
||||
import { validateName, handleHttpErrorMessages, getWorkspaceId } from '@/_helpers/utils';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
export const Folders = function Folders({
|
||||
folders,
|
||||
|
|
@ -40,6 +41,7 @@ export const Folders = function Folders({
|
|||
const [activeFolder, setActiveFolder] = useState(currentFolder || {});
|
||||
const [filteredData, setFilteredData] = useState(folders);
|
||||
const [errorText, setErrorText] = useState('');
|
||||
const navigate = useNavigate();
|
||||
|
||||
const { t } = useTranslation();
|
||||
const { updateSidebarNAV } = useContext(BreadCrumbContext);
|
||||
|
|
@ -55,9 +57,15 @@ export const Folders = function Folders({
|
|||
}, [folders]);
|
||||
|
||||
useEffect(() => {
|
||||
updateSidebarNAV('All apps');
|
||||
if (_.isEmpty(currentFolder)) {
|
||||
updateSidebarNAV('All apps');
|
||||
setActiveFolder({});
|
||||
} else {
|
||||
updateSidebarNAV(currentFolder.name);
|
||||
setActiveFolder(currentFolder);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
}, [currentFolder]);
|
||||
|
||||
const handleSearch = (e) => {
|
||||
const value = e?.target?.value;
|
||||
|
|
@ -98,6 +106,13 @@ export const Folders = function Folders({
|
|||
}
|
||||
folderChanged(folder);
|
||||
updateSidebarNAV(folder?.name ?? 'All apps');
|
||||
//update the url query parameter with folder name
|
||||
updateFolderQuery(folder?.name);
|
||||
}
|
||||
|
||||
function updateFolderQuery(name) {
|
||||
const path = `${name ? `?folder=${name}` : ''}`;
|
||||
navigate({ pathname: location.pathname, search: path }, { replace: true });
|
||||
}
|
||||
|
||||
function deleteFolder(folder) {
|
||||
|
|
@ -150,6 +165,7 @@ export const Folders = function Folders({
|
|||
setUpdationStatus(false);
|
||||
setShowUpdateForm(false);
|
||||
setNewFolderName('');
|
||||
updateFolderQuery(folderName);
|
||||
updateSidebarNAV(newFolderName);
|
||||
foldersChanged();
|
||||
})
|
||||
|
|
@ -172,7 +188,7 @@ export const Folders = function Folders({
|
|||
|
||||
const handleInputChange = (e) => {
|
||||
setErrorText('');
|
||||
const error = validateName(e.target.value, 'Folder name', false, false);
|
||||
const error = validateName(e.target.value, 'Folder name', true, false, false);
|
||||
if (!error.status) {
|
||||
setErrorText(error.errorMsg);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import React from 'react';
|
||||
import cx from 'classnames';
|
||||
import { appService, folderService, authenticationService, libraryAppService } from '@/_services';
|
||||
import { appsService, folderService, authenticationService, libraryAppService } from '@/_services';
|
||||
import { ConfirmDialog, AppModal } from '@/_components';
|
||||
import Select from '@/_ui/Select';
|
||||
import { Folders } from './Folders';
|
||||
|
|
@ -36,6 +36,7 @@ class HomePageComponent extends React.Component {
|
|||
this.state = {
|
||||
currentUser: {
|
||||
id: currentSession?.current_user.id,
|
||||
organization_id: currentSession?.current_organization_id,
|
||||
},
|
||||
users: null,
|
||||
isLoading: true,
|
||||
|
|
@ -87,7 +88,7 @@ class HomePageComponent extends React.Component {
|
|||
appSearchKey,
|
||||
});
|
||||
|
||||
appService.getAll(page, folder, appSearchKey).then((data) =>
|
||||
appsService.getAll(page, folder, appSearchKey).then((data) =>
|
||||
this.setState({
|
||||
apps: data.apps,
|
||||
meta: { ...this.state.meta, ...data.meta },
|
||||
|
|
@ -104,14 +105,16 @@ class HomePageComponent extends React.Component {
|
|||
});
|
||||
|
||||
folderService.getAll(appSearchKey).then((data) => {
|
||||
const currentFolder = data?.folders?.filter(
|
||||
(folder) => this.state.currentFolder?.id && folder.id === this.state.currentFolder?.id
|
||||
)?.[0];
|
||||
const folder_slug = new URL(window.location.href)?.searchParams?.get('folder');
|
||||
const folder = data?.folders?.find((folder) => folder.name === folder_slug);
|
||||
const currentFolderId = folder ? folder.id : this.state.currentFolder?.id;
|
||||
const currentFolder = data?.folders?.find((folder) => currentFolderId && folder.id === currentFolderId);
|
||||
this.setState({
|
||||
folders: data.folders,
|
||||
foldersLoading: false,
|
||||
currentFolder: currentFolder || {},
|
||||
});
|
||||
currentFolder && this.fetchApps(1, currentFolder.id);
|
||||
});
|
||||
};
|
||||
|
||||
|
|
@ -132,7 +135,7 @@ class HomePageComponent extends React.Component {
|
|||
let _self = this;
|
||||
_self.setState({ creatingApp: true });
|
||||
try {
|
||||
const data = await appService.createApp({ icon: sample(iconList), name: appName });
|
||||
const data = await appsService.createApp({ icon: sample(iconList), name: appName });
|
||||
const workspaceId = getWorkspaceId();
|
||||
_self.props.navigate(`/${workspaceId}/apps/${data.id}`);
|
||||
toast.success('App created successfully!');
|
||||
|
|
@ -152,7 +155,7 @@ class HomePageComponent extends React.Component {
|
|||
let _self = this;
|
||||
_self.setState({ renamingApp: true });
|
||||
try {
|
||||
await appService.saveApp(appId, { name: newAppName });
|
||||
await appsService.saveApp(appId, { name: newAppName });
|
||||
await this.fetchApps();
|
||||
toast.success('App name has been updated!');
|
||||
_self.setState({ renamingApp: false });
|
||||
|
|
@ -174,9 +177,9 @@ class HomePageComponent extends React.Component {
|
|||
cloneApp = async (appName, appId) => {
|
||||
this.setState({ isCloningApp: true });
|
||||
try {
|
||||
const data = await appService.cloneResource({
|
||||
const data = await appsService.cloneResource({
|
||||
app: [{ id: appId, name: appName }],
|
||||
organization_id: getWorkspaceId(),
|
||||
organization_id: this.state.currentUser?.organization_id,
|
||||
});
|
||||
toast.success('App cloned successfully!');
|
||||
this.props.navigate(`/${getWorkspaceId()}/apps/${data?.imports?.app[0]?.id}`);
|
||||
|
|
@ -206,11 +209,18 @@ class HomePageComponent extends React.Component {
|
|||
fileReader.readAsText(file, 'UTF-8');
|
||||
fileReader.onload = (event) => {
|
||||
const result = event.target.result;
|
||||
const fileContent = JSON.parse(result);
|
||||
let fileContent;
|
||||
try {
|
||||
fileContent = JSON.parse(result);
|
||||
} catch (parseError) {
|
||||
toast.error(`Could not import: ${parseError}`);
|
||||
return;
|
||||
}
|
||||
this.setState({ fileContent, fileName, showImportAppModal: true });
|
||||
};
|
||||
fileReader.onerror = (error) => {
|
||||
throw new Error(`Could not import the app: ${error}`);
|
||||
toast.error(`Could not import the app: ${error}`);
|
||||
return;
|
||||
};
|
||||
event.target.value = null;
|
||||
} catch (error) {
|
||||
|
|
@ -221,14 +231,16 @@ class HomePageComponent extends React.Component {
|
|||
importFile = async (importJSON, appName) => {
|
||||
this.setState({ isImportingApp: true });
|
||||
// For backward compatibility with legacy app import
|
||||
const organization_id = getWorkspaceId();
|
||||
const organization_id = this.state.currentUser?.organization_id;
|
||||
const isLegacyImport = isEmpty(importJSON.tooljet_version);
|
||||
if (isLegacyImport) {
|
||||
importJSON = { app: [{ definition: importJSON, appName: appName }], tooljet_version: importJSON.tooljetVersion };
|
||||
} else {
|
||||
importJSON.app[0].appName = appName;
|
||||
}
|
||||
const requestBody = { organization_id, ...importJSON };
|
||||
try {
|
||||
const data = await appService.importResource(requestBody);
|
||||
const data = await appsService.importResource(requestBody);
|
||||
toast.success('App imported successfully.');
|
||||
this.setState({
|
||||
isImportingApp: false,
|
||||
|
|
@ -245,6 +257,7 @@ class HomePageComponent extends React.Component {
|
|||
if (error.statusCode === 409) {
|
||||
return false;
|
||||
}
|
||||
toast.error("Couldn't import the app");
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -350,7 +363,7 @@ class HomePageComponent extends React.Component {
|
|||
|
||||
executeAppDeletion = () => {
|
||||
this.setState({ isDeletingApp: true });
|
||||
appService
|
||||
appsService
|
||||
.deleteApp(this.state.appToBeDeleted.id)
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
.then((data) => {
|
||||
|
|
@ -492,7 +505,7 @@ class HomePageComponent extends React.Component {
|
|||
}
|
||||
this.setState({ appOperations: { ...appOperations, isAdding: true } });
|
||||
|
||||
appService
|
||||
appsService
|
||||
.changeIcon(appOperations.selectedIcon, appOperations.selectedApp.id)
|
||||
.then(() => {
|
||||
toast.success('Icon updated.');
|
||||
|
|
|
|||
|
|
@ -1,13 +1,14 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import { Modal } from 'react-bootstrap';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { getAvatar, appendWorkspaceId } from '../../_helpers/utils';
|
||||
import { getAvatar } from '@/_helpers/utils';
|
||||
import { appendWorkspaceId } from '@/_helpers/routes';
|
||||
import cx from 'classnames';
|
||||
import { organizationService } from '@/_services';
|
||||
|
||||
function SwitchWorkspaceModal({ organizations, switchOrganization, ...props }) {
|
||||
const { t } = useTranslation();
|
||||
const [selectedOrganizationId, setOrganizationId] = useState();
|
||||
const [selectedOrganization, setOrganization] = useState({});
|
||||
|
||||
return (
|
||||
<Modal
|
||||
|
|
@ -44,15 +45,15 @@ function SwitchWorkspaceModal({ organizations, switchOrganization, ...props }) {
|
|||
<div
|
||||
key={organization.id}
|
||||
className={cx('org-item', {
|
||||
'selected-item': organization.id === selectedOrganizationId,
|
||||
'selected-item': organization.id === selectedOrganization?.id,
|
||||
})}
|
||||
onClick={() => setOrganizationId(organization.id)}
|
||||
onClick={() => setOrganization(organization)}
|
||||
>
|
||||
<input
|
||||
type={'radio'}
|
||||
value={organization.id}
|
||||
name="organization_id"
|
||||
checked={organization.id === selectedOrganizationId}
|
||||
checked={organization.id === selectedOrganization?.id}
|
||||
onChange={() => {}}
|
||||
/>
|
||||
<span className={'avatar avatar-sm'}>{getAvatar(organization.name)}</span>
|
||||
|
|
@ -62,7 +63,7 @@ function SwitchWorkspaceModal({ organizations, switchOrganization, ...props }) {
|
|||
</div>
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<button className="btn btn-primary" onClick={() => switchOrganization(selectedOrganizationId)}>
|
||||
<button className="btn btn-primary" onClick={() => switchOrganization(selectedOrganization)}>
|
||||
{t('globals.workspace-modal.continue-btn', 'Continue on this workspace')}
|
||||
</button>
|
||||
</Modal.Footer>
|
||||
|
|
@ -77,9 +78,9 @@ export default function SwitchWorkspacePage({ darkMode }) {
|
|||
organizationService.getOrganizations().then((response) => setOrganizations(response.organizations));
|
||||
};
|
||||
|
||||
const switchOrganization = (orgId) => {
|
||||
if (orgId) {
|
||||
const newPath = appendWorkspaceId(orgId, location.pathname, true);
|
||||
const switchOrganization = ({ id, slug }) => {
|
||||
if (slug || id) {
|
||||
const newPath = appendWorkspaceId(slug || id, location.pathname, true);
|
||||
window.history.replaceState(null, null, newPath);
|
||||
window.location.reload();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,17 +1,10 @@
|
|||
import React from 'react';
|
||||
import { authenticationService } from '@/_services';
|
||||
import { toast } from 'react-hot-toast';
|
||||
import { Link, Navigate } from 'react-router-dom';
|
||||
import queryString from 'query-string';
|
||||
import { Link } from 'react-router-dom';
|
||||
import GoogleSSOLoginButton from '@ee/components/LoginPage/GoogleSSOLoginButton';
|
||||
import GitSSOLoginButton from '@ee/components/LoginPage/GitSSOLoginButton';
|
||||
import {
|
||||
getSubpath,
|
||||
redirectToWorkspace,
|
||||
validateEmail,
|
||||
eraseRedirectUrl,
|
||||
returnWorkspaceIdIfNeed,
|
||||
} from '@/_helpers/utils';
|
||||
import { validateEmail } from '@/_helpers/utils';
|
||||
import { ShowLoading } from '@/_components';
|
||||
import { withTranslation } from 'react-i18next';
|
||||
import OnboardingNavbar from '@/_components/OnboardingNavbar';
|
||||
|
|
@ -20,8 +13,10 @@ import EnterIcon from '../../assets/images/onboardingassets/Icons/Enter';
|
|||
import EyeHide from '../../assets/images/onboardingassets/Icons/EyeHide';
|
||||
import EyeShow from '../../assets/images/onboardingassets/Icons/EyeShow';
|
||||
import Spinner from '@/_ui/Spinner';
|
||||
import { setCookie } from '@/_helpers/cookie';
|
||||
import { withRouter } from '@/_hoc/withRouter';
|
||||
import { pathnameToArray, getSubpath, getRedirectURL, redirectToDashboard, getRedirectTo } from '@/_helpers/routes';
|
||||
import { getCookie, eraseCookie, setCookie } from '@/_helpers/cookie';
|
||||
|
||||
class LoginPageComponent extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
|
@ -31,7 +26,6 @@ class LoginPageComponent extends React.Component {
|
|||
isGettingConfigs: true,
|
||||
configs: undefined,
|
||||
emailError: false,
|
||||
navigateToLogin: false,
|
||||
current_organization_name: null,
|
||||
};
|
||||
this.organizationId = props.params.organizationId;
|
||||
|
|
@ -39,48 +33,43 @@ class LoginPageComponent extends React.Component {
|
|||
darkMode = localStorage.getItem('darkMode') === 'true';
|
||||
|
||||
componentDidMount() {
|
||||
// Page is loaded inside an iframe
|
||||
const appInsideIframe = window !== window.top;
|
||||
|
||||
if (appInsideIframe) {
|
||||
const params = new URL(window.location.href).searchParams;
|
||||
|
||||
const redirectPath = params.get('redirectTo') || '/';
|
||||
window.parent.postMessage(
|
||||
{
|
||||
type: 'redirectTo',
|
||||
payload: {
|
||||
redirectPath: redirectPath,
|
||||
},
|
||||
},
|
||||
'*'
|
||||
);
|
||||
}
|
||||
|
||||
this.setRedirectUrlToCookie(appInsideIframe);
|
||||
|
||||
/* remove login oranization's id and slug from the cookie */
|
||||
authenticationService.deleteLoginOrganizationId();
|
||||
authenticationService.deleteLoginOrganizationSlug();
|
||||
|
||||
this.organizationSlug = this.organizationId;
|
||||
this.currentSessionObservable = authenticationService.currentSession.subscribe((newSession) => {
|
||||
if (newSession?.current_organization_name)
|
||||
this.setState({ current_organization_name: newSession.current_organization_name });
|
||||
if (newSession?.group_permissions || newSession?.id) {
|
||||
if (
|
||||
(!this.organizationId && newSession?.current_organization_id) ||
|
||||
(this.organizationId && newSession?.current_organization_id === this.organizationId)
|
||||
(!this.state.isGettingConfigs &&
|
||||
this.organizationId &&
|
||||
newSession?.current_organization_id === this.organizationId)
|
||||
) {
|
||||
// redirect to home if already logged in
|
||||
redirectToWorkspace();
|
||||
// set redirect path for sso/form login
|
||||
let redirectPath = '';
|
||||
if (this.state.formLogin) {
|
||||
const path = getRedirectURL();
|
||||
redirectPath = path === '/confirm' ? '/' : path;
|
||||
} else {
|
||||
const path = this.eraseRedirectUrl();
|
||||
redirectPath = getRedirectURL(path);
|
||||
}
|
||||
window.location = getSubpath() ? `${getSubpath()}${redirectPath}` : redirectPath;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (this.organizationId) {
|
||||
authenticationService.saveLoginOrganizationId(this.organizationId);
|
||||
}
|
||||
|
||||
authenticationService.getOrganizationConfigs(this.organizationId).then(
|
||||
authenticationService.getOrganizationConfigs(this.organizationSlug).then(
|
||||
(configs) => {
|
||||
this.setState({ isGettingConfigs: false, configs });
|
||||
this.organizationId = configs.id;
|
||||
this.setState({
|
||||
isGettingConfigs: false,
|
||||
configs,
|
||||
});
|
||||
},
|
||||
(response) => {
|
||||
if (response.data.statusCode !== 404 && response.data.statusCode !== 422) {
|
||||
|
|
@ -93,10 +82,10 @@ class LoginPageComponent extends React.Component {
|
|||
// If there is no organization found for single organization setup
|
||||
// show form to sign up
|
||||
// redirected here for self hosted version
|
||||
response.data.statusCode !== 422 && this.props.navigate('/setup');
|
||||
response.data.statusCode !== 422 && !this.organizationSlug && this.props.navigate('/setup');
|
||||
|
||||
// if wrong workspace id then show workspace-switching page
|
||||
if (response.data.statusCode === 422) {
|
||||
if (response.data.statusCode === 422 || (response.data.statusCode === 404 && this.organizationSlug)) {
|
||||
authenticationService
|
||||
.validateSession()
|
||||
.then(({ current_organization_id }) => {
|
||||
|
|
@ -106,7 +95,7 @@ class LoginPageComponent extends React.Component {
|
|||
this.props.history.push('/switch-workspace');
|
||||
})
|
||||
.catch(() => {
|
||||
window.location = '/login';
|
||||
if (pathnameToArray()[0] !== 'login') window.location = '/login';
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -133,6 +122,12 @@ class LoginPageComponent extends React.Component {
|
|||
this.currentSessionObservable && this.currentSessionObservable.unsubscribe();
|
||||
}
|
||||
|
||||
eraseRedirectUrl() {
|
||||
const redirectPath = getCookie('redirectPath');
|
||||
redirectPath && eraseCookie('redirectPath');
|
||||
return redirectPath;
|
||||
}
|
||||
|
||||
handleChange = (event) => {
|
||||
this.setState({ [event.target.name]: event.target.value, emailError: '' });
|
||||
};
|
||||
|
|
@ -141,17 +136,35 @@ class LoginPageComponent extends React.Component {
|
|||
this.setState((prev) => ({ showPassword: !prev.showPassword }));
|
||||
};
|
||||
|
||||
setRedirectUrlToCookie(iframe) {
|
||||
setRedirectUrlToCookie() {
|
||||
// Page is loaded inside an iframe
|
||||
const iframe = window !== window.top;
|
||||
|
||||
if (iframe) {
|
||||
const redirectPath = getRedirectTo();
|
||||
window.parent.postMessage(
|
||||
{
|
||||
type: 'redirectTo',
|
||||
payload: {
|
||||
redirectPath: redirectPath,
|
||||
},
|
||||
},
|
||||
'*'
|
||||
);
|
||||
}
|
||||
|
||||
const params = iframe ? new URL(window.location.href).searchParams : new URL(location.href).searchParams;
|
||||
const redirectPath = params.get('redirectTo');
|
||||
|
||||
authenticationService.saveLoginOrganizationId(this.organizationId);
|
||||
authenticationService.saveLoginOrganizationSlug(this.organizationSlug);
|
||||
redirectPath && setCookie('redirectPath', redirectPath, iframe);
|
||||
}
|
||||
|
||||
authUser = (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
this.setState({ isLoading: true });
|
||||
this.setState({ isLoading: true, formLogin: true });
|
||||
|
||||
const { email, password } = this.state;
|
||||
|
||||
|
|
@ -174,17 +187,10 @@ class LoginPageComponent extends React.Component {
|
|||
.then(this.authSuccessHandler, this.authFailureHandler);
|
||||
};
|
||||
|
||||
//TODO: remove this code if we don't need
|
||||
authSuccessHandler = () => {
|
||||
authenticationService.deleteLoginOrganizationId();
|
||||
const params = queryString.parse(this.props.location.search);
|
||||
const { from } = params.redirectTo ? { from: { pathname: params.redirectTo } } : { from: { pathname: '/' } };
|
||||
if (from.pathname !== '/confirm')
|
||||
// appending workspace-id to avoid 401 error. App.jsx will take the workspace id from URL
|
||||
from.pathname = `${returnWorkspaceIdIfNeed(from.pathname)}${from.pathname !== '/' ? from.pathname : ''}`;
|
||||
const redirectPath = from.pathname === '/confirm' ? '/' : from.pathname;
|
||||
this.setState({ isLoading: false });
|
||||
eraseRedirectUrl();
|
||||
window.location = getSubpath() ? `${getSubpath()}${redirectPath}` : redirectPath;
|
||||
this.eraseRedirectUrl();
|
||||
};
|
||||
|
||||
authFailureHandler = (res) => {
|
||||
|
|
@ -195,244 +201,243 @@ class LoginPageComponent extends React.Component {
|
|||
this.setState({ isLoading: false });
|
||||
};
|
||||
|
||||
redirectToUrl = () => {
|
||||
const redirectPath = eraseRedirectUrl();
|
||||
return redirectPath ? redirectPath : '/';
|
||||
};
|
||||
|
||||
render() {
|
||||
const { isLoading, configs, isGettingConfigs, navigateToLogin } = this.state;
|
||||
const { isLoading, configs, isGettingConfigs } = this.state;
|
||||
return (
|
||||
<>
|
||||
{navigateToLogin ? (
|
||||
<Navigate to={this.redirectToUrl()} />
|
||||
) : (
|
||||
<div className="common-auth-section-whole-wrapper page">
|
||||
<div className="common-auth-section-left-wrapper">
|
||||
<OnboardingNavbar darkMode={this.darkMode} />
|
||||
<div className="common-auth-section-left-wrapper-grid">
|
||||
{this.state.isGettingConfigs && (
|
||||
<div className="common-auth-section-whole-wrapper page">
|
||||
<div className="common-auth-section-left-wrapper">
|
||||
<OnboardingNavbar darkMode={this.darkMode} />
|
||||
<div className="common-auth-section-left-wrapper-grid">
|
||||
{this.state.isGettingConfigs && (
|
||||
<div className="loader-wrapper">
|
||||
<ShowLoading />
|
||||
</div>
|
||||
)}
|
||||
<form action="." method="get" autoComplete="off">
|
||||
{isGettingConfigs ? (
|
||||
<div className="loader-wrapper">
|
||||
<ShowLoading />
|
||||
</div>
|
||||
)}
|
||||
<form action="." method="get" autoComplete="off">
|
||||
{isGettingConfigs ? (
|
||||
<div className="loader-wrapper">
|
||||
<ShowLoading />
|
||||
</div>
|
||||
) : (
|
||||
<div className="common-auth-container-wrapper ">
|
||||
{!configs?.form && !configs?.git && !configs?.google && (
|
||||
<div className="text-center-onboard">
|
||||
<h2 data-cy="no-login-methods-warning">
|
||||
{this.props.t(
|
||||
'loginSignupPage.noLoginMethodsEnabled',
|
||||
'No login methods enabled for this workspace'
|
||||
)}
|
||||
) : /* If the configs don't have any organization id. that means the workspace slug is invalid */
|
||||
this.organizationSlug && !configs?.id ? (
|
||||
<div className="text-center-onboard">
|
||||
<h2 data-cy="no-workspace">
|
||||
{this.props.t(
|
||||
'loginSignupPage.workspaceDoesntExist',
|
||||
'Workspace does not exist. Please check the workspace login url again'
|
||||
)}
|
||||
</h2>
|
||||
</div>
|
||||
) : (
|
||||
<div className="common-auth-container-wrapper ">
|
||||
{!configs?.form && !configs?.git && !configs?.google && (
|
||||
<div className="text-center-onboard">
|
||||
<h2 data-cy="no-login-methods-warning">
|
||||
{this.props.t(
|
||||
'loginSignupPage.noLoginMethodsEnabled',
|
||||
'No login methods enabled for this workspace'
|
||||
)}
|
||||
</h2>
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
{(this.state?.configs?.google?.enabled ||
|
||||
this.state?.configs?.git?.enabled ||
|
||||
configs?.form?.enabled) && (
|
||||
<>
|
||||
<h2 className="common-auth-section-header sign-in-header" data-cy="sign-in-header">
|
||||
{this.props.t('loginSignupPage.signIn', `Sign in`)}
|
||||
</h2>
|
||||
{this.organizationId && (
|
||||
<p
|
||||
className="text-center-onboard workspace-login-description"
|
||||
data-cy="workspace-sign-in-sub-header"
|
||||
>
|
||||
Sign in to your workspace - {configs?.name}
|
||||
</p>
|
||||
)}
|
||||
<div className="tj-text-input-label">
|
||||
{!this.organizationId && (configs?.form?.enable_sign_up || configs?.enable_sign_up) && (
|
||||
<div className="common-auth-sub-header sign-in-sub-header" data-cy="sign-in-sub-header">
|
||||
{this.props.t('newToTooljet', 'New to ToolJet?')}
|
||||
<Link
|
||||
to={'/signup'}
|
||||
tabIndex="-1"
|
||||
style={{ marginLeft: '4px' }}
|
||||
data-cy="create-an-account-link"
|
||||
>
|
||||
{this.props.t('loginSignupPage.createToolJetAccount', `Create an account`)}
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{this.state?.configs?.git?.enabled && (
|
||||
<div className="login-sso-wrapper">
|
||||
<GitSSOLoginButton
|
||||
configs={this.state?.configs?.git?.configs}
|
||||
setRedirectUrlToCookie={() => {
|
||||
this.setRedirectUrlToCookie();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
{(this.state?.configs?.google?.enabled ||
|
||||
this.state?.configs?.git?.enabled ||
|
||||
configs?.form?.enabled) && (
|
||||
<>
|
||||
<h2 className="common-auth-section-header sign-in-header" data-cy="sign-in-header">
|
||||
{this.props.t('loginSignupPage.signIn', `Sign in`)}
|
||||
</h2>
|
||||
{this.organizationId && (
|
||||
<p
|
||||
className="text-center-onboard workspace-login-description"
|
||||
data-cy="workspace-sign-in-sub-header"
|
||||
>
|
||||
Sign in to your workspace - {configs?.name}
|
||||
</p>
|
||||
)}
|
||||
<div className="tj-text-input-label">
|
||||
{!this.organizationId && (configs?.form?.enable_sign_up || configs?.enable_sign_up) && (
|
||||
<div className="common-auth-sub-header sign-in-sub-header" data-cy="sign-in-sub-header">
|
||||
{this.props.t('newToTooljet', 'New to ToolJet?')}
|
||||
<Link
|
||||
to={'/signup'}
|
||||
tabIndex="-1"
|
||||
style={{ marginLeft: '4px' }}
|
||||
data-cy="create-an-account-link"
|
||||
>
|
||||
{this.props.t('loginSignupPage.createToolJetAccount', `Create an account`)}
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
{this.state?.configs?.google?.enabled && (
|
||||
<div className="login-sso-wrapper">
|
||||
<GoogleSSOLoginButton
|
||||
configs={this.state?.configs?.google?.configs}
|
||||
configId={this.state?.configs?.google?.config_id}
|
||||
setRedirectUrlToCookie={() => {
|
||||
this.setRedirectUrlToCookie();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{(this.state?.configs?.google?.enabled || this.state?.configs?.git?.enabled) &&
|
||||
configs?.form?.enabled && (
|
||||
<div className="separator-onboarding ">
|
||||
<div className="mt-2 separator" data-cy="onboarding-separator">
|
||||
<h2>
|
||||
<span>OR</span>
|
||||
</h2>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{this.state?.configs?.git?.enabled && (
|
||||
<div className="login-sso-wrapper">
|
||||
<GitSSOLoginButton configs={this.state?.configs?.git?.configs} />
|
||||
</div>
|
||||
)}
|
||||
{this.state?.configs?.google?.enabled && (
|
||||
<div className="login-sso-wrapper">
|
||||
<GoogleSSOLoginButton
|
||||
configs={this.state?.configs?.google?.configs}
|
||||
configId={this.state?.configs?.google?.config_id}
|
||||
{configs?.form?.enabled && (
|
||||
<>
|
||||
<div className="signin-email-wrap">
|
||||
<label className="tj-text-input-label" data-cy="work-email-label">
|
||||
{this.props.t('loginSignupPage.workEmail', 'Email?')}
|
||||
</label>
|
||||
<input
|
||||
onChange={this.handleChange}
|
||||
name="email"
|
||||
type="email"
|
||||
className="tj-text-input"
|
||||
placeholder={this.props.t('loginSignupPage.enterWorkEmail', 'Enter your email')}
|
||||
style={{ marginBottom: '0px' }}
|
||||
data-cy="work-email-input"
|
||||
autoFocus
|
||||
autoComplete="off"
|
||||
/>
|
||||
{this.state?.emailError && (
|
||||
<span className="tj-text-input-error-state" data-cy="email-error-message">
|
||||
{this.state?.emailError}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{(this.state?.configs?.google?.enabled || this.state?.configs?.git?.enabled) &&
|
||||
configs?.form?.enabled && (
|
||||
<div className="separator-onboarding ">
|
||||
<div className="mt-2 separator" data-cy="onboarding-separator">
|
||||
<h2>
|
||||
<span>OR</span>
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{configs?.form?.enabled && (
|
||||
<>
|
||||
<div className="signin-email-wrap">
|
||||
<label className="tj-text-input-label" data-cy="work-email-label">
|
||||
{this.props.t('loginSignupPage.workEmail', 'Email?')}
|
||||
</label>
|
||||
<div>
|
||||
<label className="tj-text-input-label" data-cy="password-label">
|
||||
{this.props.t('loginSignupPage.password', 'Password')}
|
||||
<span style={{ marginLeft: '4px' }}>
|
||||
<Link
|
||||
to={'/forgot-password'}
|
||||
tabIndex="-1"
|
||||
className="login-forgot-password"
|
||||
style={{ color: this.darkMode && '#3E63DD' }}
|
||||
data-cy="forgot-password-link"
|
||||
>
|
||||
{this.props.t('loginSignupPage.forgot', 'Forgot?')}
|
||||
</Link>
|
||||
</span>
|
||||
</label>
|
||||
<div className="login-password">
|
||||
<input
|
||||
onChange={this.handleChange}
|
||||
name="email"
|
||||
type="email"
|
||||
name="password"
|
||||
type={this.state?.showPassword ? 'text' : 'password'}
|
||||
className="tj-text-input"
|
||||
placeholder={this.props.t('loginSignupPage.enterWorkEmail', 'Enter your email')}
|
||||
style={{ marginBottom: '0px' }}
|
||||
data-cy="work-email-input"
|
||||
autoFocus
|
||||
autoComplete="off"
|
||||
placeholder={this.props.t('loginSignupPage.EnterPassword', 'Enter password')}
|
||||
data-cy="password-input-field"
|
||||
autoComplete="new-password"
|
||||
/>
|
||||
{this.state?.emailError && (
|
||||
<span className="tj-text-input-error-state" data-cy="email-error-message">
|
||||
{this.state?.emailError}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<label className="tj-text-input-label" data-cy="password-label">
|
||||
{this.props.t('loginSignupPage.password', 'Password')}
|
||||
<span style={{ marginLeft: '4px' }}>
|
||||
<Link
|
||||
to={'/forgot-password'}
|
||||
tabIndex="-1"
|
||||
className="login-forgot-password"
|
||||
style={{ color: this.darkMode && '#3E63DD' }}
|
||||
data-cy="forgot-password-link"
|
||||
>
|
||||
{this.props.t('loginSignupPage.forgot', 'Forgot?')}
|
||||
</Link>
|
||||
</span>
|
||||
</label>
|
||||
<div className="login-password">
|
||||
<input
|
||||
onChange={this.handleChange}
|
||||
name="password"
|
||||
type={this.state?.showPassword ? 'text' : 'password'}
|
||||
className="tj-text-input"
|
||||
placeholder={this.props.t('loginSignupPage.EnterPassword', 'Enter password')}
|
||||
data-cy="password-input-field"
|
||||
autoComplete="new-password"
|
||||
/>
|
||||
|
||||
<div
|
||||
className="login-password-hide-img"
|
||||
onClick={this.handleOnCheck}
|
||||
data-cy="show-password-icon"
|
||||
>
|
||||
{this.state?.showPassword ? (
|
||||
<EyeHide
|
||||
fill={
|
||||
this.darkMode
|
||||
? this.state?.password?.length
|
||||
? '#D1D5DB'
|
||||
: '#656565'
|
||||
: this.state?.password?.length
|
||||
? '#384151'
|
||||
: '#D1D5DB'
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<EyeShow
|
||||
fill={
|
||||
this.darkMode
|
||||
? this.state?.password?.length
|
||||
? '#D1D5DB'
|
||||
: '#656565'
|
||||
: this.state?.password?.length
|
||||
? '#384151'
|
||||
: '#D1D5DB'
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
className="login-password-hide-img"
|
||||
onClick={this.handleOnCheck}
|
||||
data-cy="show-password-icon"
|
||||
>
|
||||
{this.state?.showPassword ? (
|
||||
<EyeHide
|
||||
fill={
|
||||
this.darkMode
|
||||
? this.state?.password?.length
|
||||
? '#D1D5DB'
|
||||
: '#656565'
|
||||
: this.state?.password?.length
|
||||
? '#384151'
|
||||
: '#D1D5DB'
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<EyeShow
|
||||
fill={
|
||||
this.darkMode
|
||||
? this.state?.password?.length
|
||||
? '#D1D5DB'
|
||||
: '#656565'
|
||||
: this.state?.password?.length
|
||||
? '#384151'
|
||||
: '#D1D5DB'
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={` d-flex flex-column align-items-center ${!configs?.form?.enabled ? 'mt-0' : ''}`}
|
||||
>
|
||||
{configs?.form?.enabled && (
|
||||
<ButtonSolid
|
||||
className="login-btn"
|
||||
onClick={this.authUser}
|
||||
disabled={isLoading}
|
||||
data-cy="login-button"
|
||||
>
|
||||
{isLoading ? (
|
||||
<div className="spinner-center">
|
||||
<Spinner />
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<span> {this.props.t('loginSignupPage.loginTo', 'Login')}</span>
|
||||
<EnterIcon
|
||||
className="enter-icon-onboard"
|
||||
fill={
|
||||
isLoading || !this.state?.email || !this.state?.password
|
||||
? this.darkMode
|
||||
? '#656565'
|
||||
: ' #D1D5DB'
|
||||
: '#fff'
|
||||
}
|
||||
></EnterIcon>
|
||||
</>
|
||||
)}
|
||||
</ButtonSolid>
|
||||
)}
|
||||
{this.state.current_organization_name && this.organizationId && (
|
||||
<div
|
||||
className="text-center-onboard mt-3"
|
||||
data-cy={`back-to-${String(this.state.current_organization_name)
|
||||
.toLowerCase()
|
||||
.replace(/\s+/g, '-')}`}
|
||||
>
|
||||
back to {' '}
|
||||
<Link
|
||||
onClick={() =>
|
||||
(window.location = `${getSubpath() ? getSubpath() : ''}/${
|
||||
authenticationService.currentSessionValue?.current_organization_id
|
||||
}`)
|
||||
}
|
||||
>
|
||||
{this.state.current_organization_name}
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div className={` d-flex flex-column align-items-center ${!configs?.form?.enabled ? 'mt-0' : ''}`}>
|
||||
{configs?.form?.enabled && (
|
||||
<ButtonSolid
|
||||
className="login-btn"
|
||||
onClick={this.authUser}
|
||||
disabled={isLoading}
|
||||
data-cy="login-button"
|
||||
>
|
||||
{isLoading ? (
|
||||
<div className="spinner-center">
|
||||
<Spinner />
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<span> {this.props.t('loginSignupPage.loginTo', 'Login')}</span>
|
||||
<EnterIcon
|
||||
className="enter-icon-onboard"
|
||||
fill={
|
||||
isLoading || !this.state?.email || !this.state?.password
|
||||
? this.darkMode
|
||||
? '#656565'
|
||||
: ' #D1D5DB'
|
||||
: '#fff'
|
||||
}
|
||||
></EnterIcon>
|
||||
</>
|
||||
)}
|
||||
</ButtonSolid>
|
||||
)}
|
||||
{this.state.current_organization_name && this.organizationId && (
|
||||
<div
|
||||
className="text-center-onboard mt-3"
|
||||
data-cy={`back-to-${String(this.state.current_organization_name)
|
||||
.toLowerCase()
|
||||
.replace(/\s+/g, '-')}`}
|
||||
>
|
||||
back to {' '}
|
||||
<Link onClick={() => redirectToDashboard()}>{this.state.current_organization_name}</Link>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -168,7 +168,10 @@ export function GeneralSettings({ settings, updateData, instanceSettings, darkMo
|
|||
<p id="login-url" data-cy="workspace-login-url">
|
||||
{`${window.public_config?.TOOLJET_HOST}${
|
||||
window.public_config?.SUB_PATH ? window.public_config?.SUB_PATH : '/'
|
||||
}login/${authenticationService?.currentSessionValue?.current_organization_id}`}
|
||||
}login/${
|
||||
authenticationService?.currentSessionValue?.current_organization_slug ||
|
||||
authenticationService?.currentSessionValue?.current_organization_id
|
||||
}`}
|
||||
</p>
|
||||
<SolidIcon name="copy" width="16" onClick={() => copyFunction('login-url')} />
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -3,7 +3,8 @@ import useRouter from '@/_hooks/use-router';
|
|||
import { authenticationService } from '@/_services';
|
||||
import { Navigate } from 'react-router-dom';
|
||||
import Configs from './Configs/Config.json';
|
||||
import { RedirectLoader } from '@/_components';
|
||||
import { RedirectLoader } from '../_components';
|
||||
import { getCookie } from '@/_helpers';
|
||||
import { redirectToWorkspace } from '@/_helpers/utils';
|
||||
|
||||
export function Authorize() {
|
||||
|
|
@ -11,6 +12,8 @@ export function Authorize() {
|
|||
const router = useRouter();
|
||||
|
||||
const organizationId = authenticationService.getLoginOrganizationId();
|
||||
const organizationSlug = authenticationService.getLoginOrganizationSlug();
|
||||
const redirectUrl = getCookie('redirectPath');
|
||||
|
||||
useEffect(() => {
|
||||
const errorMessage = router.query.error_description || router.query.error;
|
||||
|
|
@ -46,13 +49,13 @@ export function Authorize() {
|
|||
//logged users should send tj-workspace-id when login to unauthorized workspace
|
||||
if (session.authentication_status === false || session.current_organization_id) {
|
||||
signIn(authParams, configs);
|
||||
subsciption.unsubscribe();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
signIn(authParams, configs);
|
||||
}
|
||||
|
||||
() => subsciption && subsciption.unsubscribe();
|
||||
// Disabled for useEffect not being called for updation
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
|
@ -79,7 +82,9 @@ export function Authorize() {
|
|||
{error && (
|
||||
<Navigate
|
||||
replace
|
||||
to={`/login${error && organizationId ? `/${organizationId}` : ''}`}
|
||||
to={`/login${error && organizationSlug ? `/${organizationSlug}` : '/'}${
|
||||
redirectUrl ? `?redirectTo=${redirectUrl}` : ''
|
||||
}`}
|
||||
state={{ errorMessage: error && error }}
|
||||
/>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,8 @@ import OnBoardingInput from './OnBoardingInput';
|
|||
import OnBoardingRadioInput from './OnBoardingRadioInput';
|
||||
import ContinueButton from './ContinueButton';
|
||||
import OnBoardingBubbles from './OnBoardingBubbles';
|
||||
import { getuserName, getSubpath } from '@/_helpers/utils';
|
||||
import { getuserName } from '@/_helpers/utils';
|
||||
import { redirectToDashboard } from '@/_helpers/routes';
|
||||
import { ON_BOARDING_SIZE, ON_BOARDING_ROLES } from '@/_helpers/constants';
|
||||
import LogoLightMode from '@assets/images/Logomark.svg';
|
||||
import LogoDarkMode from '@assets/images/Logomark-dark-mode.svg';
|
||||
|
|
@ -50,9 +51,7 @@ function OnBoardingForm({ userDetails = {}, token = '', organizationToken = '',
|
|||
.then((data) => {
|
||||
authenticationService.deleteLoginOrganizationId();
|
||||
setIsLoading(false);
|
||||
window.location = getSubpath()
|
||||
? `${getSubpath()}/${data.current_organization_id}`
|
||||
: `/${data.current_organization_id}`;
|
||||
redirectToDashboard(data);
|
||||
setCompleted(false);
|
||||
})
|
||||
.catch((res) => {
|
||||
|
|
|
|||
|
|
@ -6,7 +6,8 @@ import OnBoardingRadioInput from './OnBoardingRadioInput';
|
|||
import AdminSetup from './AdminSetup';
|
||||
import OnboardingBubblesSH from './OnboardingBubblesSH';
|
||||
import ContinueButtonSelfHost from './ContinueButtonSelfHost';
|
||||
import { getuserName, getSubpath } from '@/_helpers/utils';
|
||||
import { getuserName } from '@/_helpers/utils';
|
||||
import { redirectToDashboard } from '@/_helpers/routes';
|
||||
import { ON_BOARDING_SIZE, ON_BOARDING_ROLES } from '@/_helpers/constants';
|
||||
import LogoLightMode from '@assets/images/Logomark.svg';
|
||||
import LogoDarkMode from '@assets/images/Logomark-dark-mode.svg';
|
||||
|
|
@ -58,9 +59,7 @@ function OnbboardingFromSH({ darkMode }) {
|
|||
.then((user) => {
|
||||
authenticationService.deleteLoginOrganizationId();
|
||||
setIsLoading(false);
|
||||
window.location = getSubpath()
|
||||
? `${getSubpath()}/${user?.current_organization_id}`
|
||||
: `/${user?.current_organization_id}`;
|
||||
redirectToDashboard(user);
|
||||
setCompleted(false);
|
||||
})
|
||||
.catch((res) => {
|
||||
|
|
|
|||
|
|
@ -14,7 +14,8 @@ import EyeHide from '../../assets/images/onboardingassets/Icons/EyeHide';
|
|||
import EyeShow from '../../assets/images/onboardingassets/Icons/EyeShow';
|
||||
import Spinner from '@/_ui/Spinner';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { buildURLWithQuery, getSubpath } from '@/_helpers/utils';
|
||||
import { buildURLWithQuery } from '@/_helpers/utils';
|
||||
import { redirectToDashboard } from '@/_helpers/routes';
|
||||
|
||||
export const VerificationSuccessInfoScreen = function VerificationSuccessInfoScreen() {
|
||||
const [showOnboarding, setShowOnboarding] = useState(false);
|
||||
|
|
@ -117,9 +118,7 @@ export const VerificationSuccessInfoScreen = function VerificationSuccessInfoScr
|
|||
.then((user) => {
|
||||
authenticationService.deleteLoginOrganizationId();
|
||||
setIsLoading(false);
|
||||
window.location = getSubpath()
|
||||
? `${getSubpath()}/${user?.current_organization_id}`
|
||||
: `/${user?.current_organization_id}`;
|
||||
redirectToDashboard(user);
|
||||
})
|
||||
.catch((res) => {
|
||||
setIsLoading(false);
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import React, { useState, useContext, useCallback, useEffect } from 'react';
|
||||
import cx from 'classnames';
|
||||
import { toast } from 'react-hot-toast';
|
||||
import { tooljetDatabaseService, appService } from '@/_services';
|
||||
import { tooljetDatabaseService, appsService } from '@/_services';
|
||||
import { ListItemPopover } from './ActionsPopover';
|
||||
import { TooljetDatabaseContext } from '../index';
|
||||
import { ToolTip } from '@/_components';
|
||||
|
|
@ -22,7 +22,7 @@ export const ListItem = ({ active, onClick, text = '', onDeleteCallback }) => {
|
|||
}
|
||||
|
||||
const handleExportTable = () => {
|
||||
appService
|
||||
appsService
|
||||
.exportResource({
|
||||
tooljet_database: [{ table_id: selectedTable.id }],
|
||||
organization_id: organizationId,
|
||||
|
|
|
|||
|
|
@ -2,7 +2,8 @@ import React from 'react';
|
|||
import moment from 'moment';
|
||||
import { toast } from 'react-hot-toast';
|
||||
import { commentNotificationsService } from '@/_services';
|
||||
import { hightlightMentionedUserInComment, appendWorkspaceId, getWorkspaceId } from '@/_helpers/utils';
|
||||
import { hightlightMentionedUserInComment, getWorkspaceId } from '@/_helpers/utils';
|
||||
import { appendWorkspaceId } from '@/_helpers/routes';
|
||||
|
||||
export const Notification = ({ id, creator, comment, updatedAt, commentLink, isRead, fetchData, darkMode }) => {
|
||||
const updateMentionedNotification = async () => {
|
||||
|
|
|
|||
|
|
@ -2,12 +2,14 @@ import React from 'react';
|
|||
import { Link } from 'react-router-dom';
|
||||
import LogoLightMode from '@assets/images/Logomark.svg';
|
||||
import LogoDarkMode from '@assets/images/Logomark-dark-mode.svg';
|
||||
import { redirectToDashboard } from '@/_helpers/routes';
|
||||
|
||||
function OnboardingNavbar({ darkMode }) {
|
||||
const Logo = darkMode ? LogoDarkMode : LogoLightMode;
|
||||
|
||||
return (
|
||||
<div className={`onboarding-navbar container-xl`}>
|
||||
<Link to="/">
|
||||
<Link onClick={() => redirectToDashboard()}>
|
||||
<Logo height="23" width="92" alt="tooljet logo" data-cy="page-logo" />
|
||||
</Link>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -3,27 +3,38 @@ import { organizationService } from '@/_services';
|
|||
import AlertDialog from '@/_ui/AlertDialog';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ButtonSolid } from '@/_ui/AppButton/AppButton';
|
||||
import { appendWorkspaceId, validateName, handleHttpErrorMessages } from '@/_helpers/utils';
|
||||
import { validateName, handleHttpErrorMessages } from '@/_helpers/utils';
|
||||
import { appendWorkspaceId, getHostURL } from '@/_helpers/routes';
|
||||
import _ from 'lodash';
|
||||
|
||||
export const CreateOrganization = ({ showCreateOrg, setShowCreateOrg }) => {
|
||||
const [isCreating, setIsCreating] = useState(false);
|
||||
const [newOrgName, setNewOrgName] = useState('');
|
||||
const [errorText, setErrorText] = useState('');
|
||||
const [fields, setFields] = useState({ name: { value: '', error: '' }, slug: { value: null, error: '' } });
|
||||
const [slugProgress, setSlugProgress] = useState(false);
|
||||
const [workspaceNameProgress, setWorkspaceNameProgress] = useState(false);
|
||||
const [isDisabled, setDisabled] = useState(true);
|
||||
const darkMode = localStorage.getItem('darkMode') === 'true';
|
||||
const { t } = useTranslation();
|
||||
|
||||
const createOrganization = () => {
|
||||
const newName = newOrgName?.trim();
|
||||
if (!newName) {
|
||||
setErrorText("Workspace name can't be empty");
|
||||
return;
|
||||
}
|
||||
let emptyError = false;
|
||||
const fieldsTemp = fields;
|
||||
Object.keys(fields).map((key) => {
|
||||
if (!fields?.[key]?.value?.trim()) {
|
||||
fieldsTemp[key] = {
|
||||
error: `Workspace ${key} can't be empty`,
|
||||
};
|
||||
emptyError = true;
|
||||
}
|
||||
});
|
||||
setFields({ ...fields, ...fieldsTemp });
|
||||
|
||||
if (!errorText) {
|
||||
if (!emptyError && !Object.keys(fields).find((key) => !_.isEmpty(fields[key].error))) {
|
||||
setIsCreating(true);
|
||||
organizationService.createOrganization(newName).then(
|
||||
(data) => {
|
||||
organizationService.createOrganization({ name: fields['name'].value, slug: fields['slug'].value }).then(
|
||||
() => {
|
||||
setIsCreating(false);
|
||||
const newPath = appendWorkspaceId(data.current_organization_id, location.pathname, true);
|
||||
const newPath = appendWorkspaceId(fields['slug'].value, location.pathname, true);
|
||||
window.history.replaceState(null, null, newPath);
|
||||
window.location.reload();
|
||||
},
|
||||
|
|
@ -35,6 +46,56 @@ export const CreateOrganization = ({ showCreateOrg, setShowCreateOrg }) => {
|
|||
}
|
||||
};
|
||||
|
||||
const handleInputChange = async (value, field) => {
|
||||
setFields({
|
||||
...fields,
|
||||
[field]: {
|
||||
...fields[field],
|
||||
error: null,
|
||||
},
|
||||
});
|
||||
let error = validateName(
|
||||
value,
|
||||
`Workspace ${field}`,
|
||||
true,
|
||||
false,
|
||||
!(field === 'slug'),
|
||||
!(field === 'slug'),
|
||||
field === 'slug'
|
||||
);
|
||||
|
||||
/* If the basic validation is passing. then check the uniqueness */
|
||||
if (error?.status === true) {
|
||||
try {
|
||||
await organizationService.checkWorkspaceUniqueness(
|
||||
field === 'name' ? value : null,
|
||||
field === 'slug' ? value : null
|
||||
);
|
||||
} catch (errResponse) {
|
||||
error = {
|
||||
status: false,
|
||||
errorMsg: errResponse?.error,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
setFields({
|
||||
...fields,
|
||||
[field]: {
|
||||
value,
|
||||
error: error?.errorMsg,
|
||||
},
|
||||
});
|
||||
|
||||
const otherInputErrors = Object.keys(fields).find(
|
||||
(key) => (key !== field && !_.isEmpty(fields[key].error)) || (key !== field && _.isEmpty(fields[key].value))
|
||||
);
|
||||
setDisabled(!error?.status || otherInputErrors);
|
||||
field === 'slug' && setSlugProgress(false);
|
||||
field === 'name' && setWorkspaceNameProgress(false);
|
||||
return;
|
||||
};
|
||||
|
||||
const handleKeyDown = (e) => {
|
||||
if (e.keyCode === 13) {
|
||||
e.preventDefault();
|
||||
|
|
@ -42,21 +103,17 @@ export const CreateOrganization = ({ showCreateOrg, setShowCreateOrg }) => {
|
|||
}
|
||||
};
|
||||
|
||||
const handleInputChange = (e) => {
|
||||
const value = e.target.value;
|
||||
setErrorText('');
|
||||
const error = validateName(value, 'Workspace name');
|
||||
if (!error.status) {
|
||||
setErrorText(error.errorMsg);
|
||||
}
|
||||
setNewOrgName(value);
|
||||
const closeModal = () => {
|
||||
setFields({ name: { value: '', error: '' }, slug: { value: null, error: '' } });
|
||||
setShowCreateOrg(false);
|
||||
setDisabled(true);
|
||||
};
|
||||
|
||||
const closeModal = () => {
|
||||
setErrorText('');
|
||||
setNewOrgName('');
|
||||
setShowCreateOrg(false);
|
||||
};
|
||||
const delayedFieldChange = _.debounce(async (value, field) => {
|
||||
field === 'name' && setWorkspaceNameProgress(true);
|
||||
field === 'slug' && setSlugProgress(true);
|
||||
await handleInputChange(value, field);
|
||||
}, 500);
|
||||
|
||||
return (
|
||||
<AlertDialog
|
||||
|
|
@ -64,35 +121,111 @@ export const CreateOrganization = ({ showCreateOrg, setShowCreateOrg }) => {
|
|||
closeModal={closeModal}
|
||||
title={t('header.organization.createWorkspace', 'Create workspace')}
|
||||
>
|
||||
<div className="row mb-3 workspace-folder-modal">
|
||||
<div className="col modal-main tj-app-input">
|
||||
<input
|
||||
type="text"
|
||||
onChange={handleInputChange}
|
||||
onKeyDown={handleKeyDown}
|
||||
className="form-control"
|
||||
placeholder={t('header.organization.workspaceName', 'workspace name')}
|
||||
disabled={isCreating}
|
||||
maxLength={50}
|
||||
data-cy="workspace-name-input-field"
|
||||
autoFocus
|
||||
/>
|
||||
<label className="tj-input-error">{errorText || ''}</label>
|
||||
<div className="workspace-folder-modal">
|
||||
<div className="row">
|
||||
<div className="tj-app-input">
|
||||
<label>Workspace name</label>
|
||||
<input
|
||||
type="text"
|
||||
onChange={async (e) => {
|
||||
e.persist();
|
||||
await delayedFieldChange(e.target.value, 'name');
|
||||
}}
|
||||
className={`form-control ${fields['name']?.error ? 'is-invalid' : 'is-valid'}`}
|
||||
placeholder={t('header.organization.workspaceName', 'Workspace name')}
|
||||
disabled={isCreating}
|
||||
onKeyDown={handleKeyDown}
|
||||
maxLength={50}
|
||||
data-cy="workspace-name-input-field"
|
||||
autoFocus
|
||||
/>
|
||||
{fields['name']?.error ? (
|
||||
<label className="label tj-input-error">{fields['name']?.error || ''}</label>
|
||||
) : (
|
||||
<label className="label label-info">Name must be unique and max 50 characters</label>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="row">
|
||||
<div className="col d-flex justify-content-end gap-2">
|
||||
<ButtonSolid variant="tertiary" onClick={closeModal} data-cy="cancel-button">
|
||||
{t('globals.cancel', 'Cancel')}
|
||||
</ButtonSolid>
|
||||
<ButtonSolid
|
||||
disabled={isCreating}
|
||||
onClick={createOrganization}
|
||||
data-cy="create-workspace-button"
|
||||
isLoading={isCreating}
|
||||
>
|
||||
{t('header.organization.createWorkspace', 'Create workspace')}
|
||||
</ButtonSolid>
|
||||
<div className="row">
|
||||
<div className="tj-app-input input-with-icon">
|
||||
<label>Unique workspace slug</label>
|
||||
<input
|
||||
type="text"
|
||||
className={`form-control ${fields['slug']?.error ? 'is-invalid' : 'is-valid'}`}
|
||||
placeholder={t('header.organization.workspaceSlug', 'Unique workspace slug')}
|
||||
disabled={isCreating}
|
||||
maxLength={50}
|
||||
onChange={async (e) => {
|
||||
e.persist();
|
||||
await delayedFieldChange(e.target.value, 'slug');
|
||||
}}
|
||||
data-cy="workspace-slug-input-field"
|
||||
autoFocusfields
|
||||
/>
|
||||
{!slugProgress && fields['slug'].value !== null && !fields['slug'].error && (
|
||||
<div className="icon-container">
|
||||
<svg width="15" height="10" viewBox="0 0 15 10" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M14.256 0.244078C14.5814 0.569515 14.5814 1.09715 14.256 1.42259L5.92263 9.75592C5.59719 10.0814 5.06956 10.0814 4.74412 9.75592L0.577452 5.58926C0.252015 5.26382 0.252015 4.73618 0.577452 4.41074C0.902889 4.08531 1.43053 4.08531 1.75596 4.41074L5.33337 7.98816L13.0775 0.244078C13.4029 -0.0813592 13.9305 -0.0813592 14.256 0.244078Z"
|
||||
fill="#46A758"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
)}
|
||||
{fields['slug']?.error ? (
|
||||
<label className="label tj-input-error">{fields['slug']?.error || ''}</label>
|
||||
) : fields['slug'].value && !slugProgress ? (
|
||||
<label className="label label-success">{`Slug accepted!`}</label>
|
||||
) : (
|
||||
<label className="label label-info">{`URL-friendly 'slug' consists of lowercase letters, numbers, and hyphens`}</label>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="row mb-3">
|
||||
<div className="col modal-main tj-app-input">
|
||||
<label>Workspace link</label>
|
||||
<div className={`tj-text-input break-all ${darkMode ? 'dark' : ''}`}>
|
||||
{!slugProgress ? (
|
||||
`${getHostURL()}/${fields['slug']?.value || '<workspace-slug>'}`
|
||||
) : (
|
||||
<div className="d-flex gap-2">
|
||||
<div class="spinner-border text-secondary workspace-spinner" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
{`Updating link`}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<label className="label label-success label-updated">
|
||||
{fields['slug'].value && !fields['slug'].error && !slugProgress ? `Link updated successfully!` : ''}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div className="row">
|
||||
<div className="col d-flex justify-content-end gap-2">
|
||||
<ButtonSolid variant="secondary" onClick={closeModal} data-cy="cancel-button" className="cancel-btn">
|
||||
{t('globals.cancel', 'Cancel')}
|
||||
</ButtonSolid>
|
||||
<ButtonSolid
|
||||
disabled={isCreating || isDisabled || slugProgress || workspaceNameProgress}
|
||||
onClick={createOrganization}
|
||||
data-cy="create-workspace-button"
|
||||
isLoading={isCreating}
|
||||
>
|
||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M6 0.666992C6.36819 0.666992 6.66667 0.965469 6.66667 1.33366V5.33366H10.6667C11.0349 5.33366 11.3333 5.63214 11.3333 6.00033C11.3333 6.36852 11.0349 6.66699 10.6667 6.66699H6.66667V10.667C6.66667 11.0352 6.36819 11.3337 6 11.3337C5.63181 11.3337 5.33333 11.0352 5.33333 10.667V6.66699H1.33333C0.965145 6.66699 0.666668 6.36852 0.666668 6.00033C0.666668 5.63214 0.965145 5.33366 1.33333 5.33366H5.33333V1.33366C5.33333 0.965469 5.63181 0.666992 6 0.666992Z"
|
||||
fill="#FDFDFE"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
{t('header.organization.createWorkspace', 'Create workspace')}
|
||||
</ButtonSolid>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</AlertDialog>
|
||||
|
|
|
|||
|
|
@ -5,28 +5,58 @@ import { toast } from 'react-hot-toast';
|
|||
import { useTranslation } from 'react-i18next';
|
||||
import { ButtonSolid } from '@/_ui/AppButton/AppButton';
|
||||
import { validateName, handleHttpErrorMessages } from '@/_helpers/utils';
|
||||
import { appendWorkspaceId, getHostURL } from '@/_helpers/routes';
|
||||
import _ from 'lodash';
|
||||
|
||||
export const EditOrganization = ({ showEditOrg, setShowEditOrg, currentValue }) => {
|
||||
const [isCreating, setIsCreating] = useState(false);
|
||||
const [newOrgName, setNewOrgName] = useState('');
|
||||
const [errorText, setErrorText] = useState('');
|
||||
const [fields, setFields] = useState({ name: { value: '', error: '' }, slug: { value: null, error: '' } });
|
||||
const [slugProgress, setSlugProgress] = useState(false);
|
||||
const [workspaceNameProgress, setWorkspaceNameProgress] = useState(false);
|
||||
const [isDisabled, setDisabled] = useState(true);
|
||||
const { t } = useTranslation();
|
||||
const darkMode = localStorage.getItem('darkMode') === 'true';
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
useEffect(() => setNewOrgName(currentValue?.name), [currentValue]);
|
||||
useEffect(
|
||||
() =>
|
||||
setFields({
|
||||
name: {
|
||||
value: currentValue?.name,
|
||||
},
|
||||
slug: {
|
||||
value: currentValue?.slug,
|
||||
},
|
||||
}),
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[currentValue]
|
||||
);
|
||||
|
||||
const editOrganization = () => {
|
||||
const trimmedName = newOrgName?.trim();
|
||||
if (errorText) {
|
||||
return;
|
||||
}
|
||||
if (currentValue?.name !== trimmedName) {
|
||||
let emptyError = false;
|
||||
const fieldsTemp = fields;
|
||||
Object.keys(fields).map((key) => {
|
||||
if (!fields?.[key]?.value?.trim()) {
|
||||
fieldsTemp[key] = {
|
||||
error: `Workspace ${key} can't be empty`,
|
||||
};
|
||||
emptyError = true;
|
||||
}
|
||||
});
|
||||
setFields({ ...fields, ...fieldsTemp });
|
||||
|
||||
if (!emptyError && !Object.keys(fields).find((key) => !_.isEmpty(fields[key].error))) {
|
||||
setIsCreating(true);
|
||||
organizationService.editOrganization({ name: trimmedName }).then(
|
||||
const data = {
|
||||
...(fields?.name?.value && fields?.name?.value !== currentValue?.name && { name: fields.name.value.trim() }),
|
||||
...(fields?.slug?.value && fields?.slug?.value !== currentValue?.slug && { slug: fields.slug.value.trim() }),
|
||||
};
|
||||
organizationService.editOrganization(data).then(
|
||||
() => {
|
||||
toast.success('Workspace updated');
|
||||
setIsCreating(false);
|
||||
setShowEditOrg(false);
|
||||
const newPath = appendWorkspaceId(fields['slug'].value, location.pathname, true);
|
||||
window.history.replaceState(null, null, newPath);
|
||||
window.location.reload();
|
||||
},
|
||||
(error) => {
|
||||
|
|
@ -34,18 +64,72 @@ export const EditOrganization = ({ showEditOrg, setShowEditOrg, currentValue })
|
|||
setIsCreating(false);
|
||||
}
|
||||
);
|
||||
} else {
|
||||
setShowEditOrg(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleInputChange = (e) => {
|
||||
setErrorText('');
|
||||
const error = validateName(e.target.value, 'Workspace name');
|
||||
if (!error.status) {
|
||||
setErrorText(error.errorMsg);
|
||||
const handleInputChange = async (value, field) => {
|
||||
const trimmedValue = value?.trim();
|
||||
const prevValue = field === 'name' ? currentValue?.name : currentValue?.slug;
|
||||
//reset fields
|
||||
setFields({
|
||||
...fields,
|
||||
[field]: {
|
||||
...fields[field],
|
||||
error: null,
|
||||
},
|
||||
});
|
||||
let error = validateName(
|
||||
value,
|
||||
`Workspace ${field}`,
|
||||
true,
|
||||
false,
|
||||
!(field === 'slug'),
|
||||
!(field === 'slug'),
|
||||
field === 'slug'
|
||||
);
|
||||
|
||||
/* If the basic validation is passing. then check the uniqueness */
|
||||
if (error?.status === true && value !== prevValue) {
|
||||
try {
|
||||
await organizationService.checkWorkspaceUniqueness(
|
||||
field === 'name' ? value : null,
|
||||
field === 'slug' ? value : null
|
||||
);
|
||||
} catch (errResponse) {
|
||||
error = {
|
||||
status: false,
|
||||
errorMsg: errResponse?.error,
|
||||
};
|
||||
}
|
||||
}
|
||||
setNewOrgName(e.target.value);
|
||||
|
||||
setFields({
|
||||
...fields,
|
||||
[field]: {
|
||||
value,
|
||||
error: error?.errorMsg,
|
||||
},
|
||||
});
|
||||
|
||||
/* Checking for if the user entered the same value or not */
|
||||
let isValueTheSame = false;
|
||||
if (error?.status) {
|
||||
if (field === 'name') {
|
||||
isValueTheSame = trimmedValue === currentValue?.name && fields?.slug?.value === currentValue?.slug;
|
||||
} else {
|
||||
isValueTheSame = trimmedValue === currentValue?.slug && fields?.name?.value === currentValue?.name;
|
||||
}
|
||||
}
|
||||
|
||||
/* recheck if the rest of fields are valid or not */
|
||||
const otherInputErrors = Object.keys(fields).find(
|
||||
(key) => (key !== field && !_.isEmpty(fields[key].error)) || (key !== field && _.isEmpty(fields[key].value))
|
||||
);
|
||||
|
||||
setDisabled(isValueTheSame || !error?.status || otherInputErrors);
|
||||
field === 'slug' && setSlugProgress(false);
|
||||
field === 'name' && setWorkspaceNameProgress(false);
|
||||
return;
|
||||
};
|
||||
|
||||
const handleKeyDown = (e) => {
|
||||
|
|
@ -56,41 +140,126 @@ export const EditOrganization = ({ showEditOrg, setShowEditOrg, currentValue })
|
|||
};
|
||||
|
||||
const closeModal = () => {
|
||||
setFields({ name: { value: currentValue?.name, error: '' }, slug: { value: currentValue?.slug, error: '' } });
|
||||
setShowEditOrg(false);
|
||||
setErrorText('');
|
||||
setNewOrgName(currentValue.name);
|
||||
setDisabled(true);
|
||||
};
|
||||
|
||||
const delayedFieldChange = _.debounce(async (value, field) => {
|
||||
field === 'name' && setWorkspaceNameProgress(true);
|
||||
field === 'slug' && setSlugProgress(true);
|
||||
await handleInputChange(value, field);
|
||||
}, 500);
|
||||
|
||||
return (
|
||||
<AlertDialog
|
||||
show={showEditOrg}
|
||||
closeModal={closeModal}
|
||||
title={t('header.organization.editWorkspace', 'Edit workspace')}
|
||||
>
|
||||
<div className="row mb-3 workspace-folder-modal">
|
||||
<div className="col modal-main tj-app-input">
|
||||
<input
|
||||
type="text"
|
||||
onChange={handleInputChange}
|
||||
onKeyDown={handleKeyDown}
|
||||
className="form-control"
|
||||
placeholder={t('header.organization.workspaceName', 'workspace name')}
|
||||
disabled={isCreating}
|
||||
value={newOrgName}
|
||||
maxLength={50}
|
||||
autoFocus
|
||||
/>
|
||||
<label className="tj-input-error">{errorText || ''}</label>
|
||||
<div className="workspace-folder-modal">
|
||||
<div className="row">
|
||||
<div className="col modal-main tj-app-input">
|
||||
<label>Workspace name</label>
|
||||
<input
|
||||
type="text"
|
||||
onChange={async (e) => {
|
||||
e.persist();
|
||||
await delayedFieldChange(e.target.value, 'name');
|
||||
}}
|
||||
onKeyDown={handleKeyDown}
|
||||
className={`form-control ${fields['name']?.error ? 'is-invalid' : 'is-valid'}`}
|
||||
placeholder={t('header.organization.workspaceName', 'Workspace name')}
|
||||
disabled={isCreating}
|
||||
maxLength={50}
|
||||
defaultValue={fields['name']?.value}
|
||||
data-cy="workspace-name-input-field"
|
||||
autoFocus
|
||||
/>
|
||||
{fields['name']?.error ? (
|
||||
<label className="label tj-input-error">{fields['name']?.error || ''}</label>
|
||||
) : (
|
||||
<label className="label label-info">Name must be unique and max 50 characters</label>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="row">
|
||||
<div className="col d-flex justify-content-end gap-2">
|
||||
<ButtonSolid variant="tertiary" onClick={closeModal}>
|
||||
{t('globals.cancel', 'Cancel')}
|
||||
</ButtonSolid>
|
||||
<ButtonSolid isLoading={isCreating} onClick={editOrganization}>
|
||||
{t('globals.save', 'Save')}
|
||||
</ButtonSolid>
|
||||
<div className="row">
|
||||
<div className="col tj-app-input input-with-icon">
|
||||
<label>Unique workspace slug</label>
|
||||
<input
|
||||
type="text"
|
||||
className={`form-control ${fields['slug']?.error ? 'is-invalid' : 'is-valid'}`}
|
||||
placeholder={t('header.organization.workspaceSlug', 'Unique workspace slug')}
|
||||
disabled={isCreating}
|
||||
maxLength={50}
|
||||
onChange={async (e) => {
|
||||
e.persist();
|
||||
await delayedFieldChange(e.target.value, 'slug');
|
||||
}}
|
||||
onKeyDown={handleKeyDown}
|
||||
defaultValue={fields['slug']?.value}
|
||||
data-cy="workspace-slug-input-field"
|
||||
autoFocusfields
|
||||
/>
|
||||
{!slugProgress && fields?.['slug']?.value !== currentValue?.slug && !fields['slug'].error && (
|
||||
<div className="icon-container">
|
||||
<svg width="15" height="10" viewBox="0 0 15 10" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M14.256 0.244078C14.5814 0.569515 14.5814 1.09715 14.256 1.42259L5.92263 9.75592C5.59719 10.0814 5.06956 10.0814 4.74412 9.75592L0.577452 5.58926C0.252015 5.26382 0.252015 4.73618 0.577452 4.41074C0.902889 4.08531 1.43053 4.08531 1.75596 4.41074L5.33337 7.98816L13.0775 0.244078C13.4029 -0.0813592 13.9305 -0.0813592 14.256 0.244078Z"
|
||||
fill="#46A758"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
)}
|
||||
{fields['slug']?.error ? (
|
||||
<label className="label tj-input-error">{fields['slug']?.error || ''}</label>
|
||||
) : fields?.['slug']?.value !== currentValue?.slug && !slugProgress ? (
|
||||
<label className="label label-success">{`Slug accepted!`}</label>
|
||||
) : (
|
||||
<label className="label label-info">{`URL-friendly 'slug' consists of lowercase letters, numbers, and hyphens`}</label>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="row mb-3">
|
||||
<div className="col modal-main tj-app-input">
|
||||
<label>Workspace link</label>
|
||||
<div className={`tj-text-input break-all ${darkMode ? 'dark' : ''}`}>
|
||||
{!slugProgress ? (
|
||||
`${getHostURL()}/${fields['slug']?.value || '<workspace-slug>'}`
|
||||
) : (
|
||||
<div className="d-flex gap-2">
|
||||
<div class="spinner-border text-secondary workspace-spinner" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
{`Updating link`}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<label className="label label-success label-updated">
|
||||
{!slugProgress &&
|
||||
fields['slug'].value &&
|
||||
!fields['slug'].error &&
|
||||
fields?.['slug']?.value !== currentValue?.slug
|
||||
? `Link updated successfully!`
|
||||
: ''}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div className="row">
|
||||
<div className="col d-flex justify-content-end gap-2">
|
||||
<ButtonSolid variant="secondary" onClick={closeModal} className="cancel-btn">
|
||||
{t('globals.cancel', 'Cancel')}
|
||||
</ButtonSolid>
|
||||
<ButtonSolid
|
||||
isLoading={isCreating}
|
||||
disabled={isCreating || isDisabled || slugProgress || workspaceNameProgress}
|
||||
onClick={editOrganization}
|
||||
>
|
||||
{t('globals.save', 'Save')}
|
||||
</ButtonSolid>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</AlertDialog>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import { authenticationService } from '@/_services';
|
||||
import { CustomSelect } from './CustomSelect';
|
||||
import { getWorkspaceIdFromURL, appendWorkspaceId, getAvatar } from '../../_helpers/utils';
|
||||
import { getAvatar } from '@/_helpers/utils';
|
||||
import { appendWorkspaceId, getWorkspaceIdOrSlugFromURL } from '@/_helpers/routes';
|
||||
import { ToolTip } from '@/_components';
|
||||
|
||||
export const OrganizationList = function () {
|
||||
|
|
@ -20,9 +21,10 @@ export const OrganizationList = function () {
|
|||
() => sessionObservable.unsubscribe();
|
||||
}, []);
|
||||
|
||||
const switchOrganization = (orgId) => {
|
||||
if (getWorkspaceIdFromURL() !== orgId) {
|
||||
const newPath = appendWorkspaceId(orgId, location.pathname, true);
|
||||
const switchOrganization = (id) => {
|
||||
const organization = organizationList.find((org) => org.id === id);
|
||||
if (![id, organization.slug].includes(getWorkspaceIdOrSlugFromURL())) {
|
||||
const newPath = appendWorkspaceId(organization.slug || id, location.pathname, true);
|
||||
window.history.replaceState(null, null, newPath);
|
||||
window.location.reload();
|
||||
}
|
||||
|
|
@ -31,6 +33,7 @@ export const OrganizationList = function () {
|
|||
const options = organizationList.map((org) => ({
|
||||
value: org.id,
|
||||
name: org.name,
|
||||
slug: org.slug,
|
||||
label: (
|
||||
<div className={`align-items-center d-flex tj-org-dropdown ${darkMode && 'dark-theme'}`}>
|
||||
<div
|
||||
|
|
|
|||
|
|
@ -1,30 +1,76 @@
|
|||
import React, { useEffect } from 'react';
|
||||
import { Navigate, useLocation } from 'react-router-dom';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Navigate, useLocation, useNavigate, useParams } from 'react-router-dom';
|
||||
import { authenticationService } from '@/_services';
|
||||
import { excludeWorkspaceIdFromURL, appendWorkspaceId } from '../_helpers/utils';
|
||||
import { appendWorkspaceId, excludeWorkspaceIdFromURL, getPathname } from '@/_helpers/routes';
|
||||
import { TJLoader } from '@/_ui/TJLoader/TJLoader';
|
||||
import { getWorkspaceId } from '@/_helpers/utils';
|
||||
import { handleAppAccess } from '@/_helpers/handleAppAccess';
|
||||
|
||||
export const PrivateRoute = ({ children }) => {
|
||||
const [session, setSession] = React.useState(authenticationService.currentSessionValue);
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
const params = useParams();
|
||||
const [extraProps, setExtraProps] = useState({});
|
||||
const [isValidatingUserAccess, setUserValidationStatus] = useState(true);
|
||||
|
||||
const pathname = getPathname(null, true);
|
||||
const isEditorOrViewerGoingToRender = pathname.startsWith('/apps/') || pathname.startsWith('/applications/');
|
||||
|
||||
const validateRoutes = async (group_permissions, callback) => {
|
||||
/* validate the app access if the route either /apps/ or /application/ and
|
||||
user has a valid session also user isn't switching between pages on editor
|
||||
*/
|
||||
const isSwitchingPages = location.state?.isSwitchingPage;
|
||||
/* replacing the state. otherwise the route will keep isSwitchingPage value `true` */
|
||||
navigate(
|
||||
{ pathname: location.pathname, search: location.search },
|
||||
{ replace: true, state: Object.assign({}, location?.state || {}, { isSwitchingPage: false }) }
|
||||
);
|
||||
if (isEditorOrViewerGoingToRender && group_permissions && !isSwitchingPages) {
|
||||
const componentType = pathname.startsWith('/apps/') ? 'editor' : 'viewer';
|
||||
const { slug } = params;
|
||||
|
||||
/* Validate the app permissions */
|
||||
const accessDetails = await handleAppAccess(componentType, slug);
|
||||
setExtraProps(accessDetails);
|
||||
callback();
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const subject = authenticationService.currentSession.subscribe((newSession) => {
|
||||
const subject = authenticationService.currentSession.subscribe(async (newSession) => {
|
||||
setSession(newSession);
|
||||
});
|
||||
|
||||
() => subject.unsubscribe();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const wid = session?.current_organization_id;
|
||||
const path = appendWorkspaceId(wid, location.pathname, true);
|
||||
if (location.pathname === '/:workspaceId' && wid) window.history.replaceState(null, null, path);
|
||||
useEffect(() => {
|
||||
setUserValidationStatus(true);
|
||||
/* When route changes (not hard reload). will validate the access */
|
||||
validateRoutes(session?.group_permissions, () => {
|
||||
setUserValidationStatus(false);
|
||||
});
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [pathname, session]);
|
||||
|
||||
//get either slug or id from the session and replace
|
||||
const { current_organization_slug, current_organization_id } = session;
|
||||
if (location.pathname.startsWith('/:workspaceId')) {
|
||||
const path = appendWorkspaceId(current_organization_slug || current_organization_id, location.pathname, true);
|
||||
(current_organization_slug || current_organization_id) && window.history.replaceState(null, null, path);
|
||||
}
|
||||
|
||||
// authorised so return component
|
||||
if (
|
||||
session?.group_permissions ||
|
||||
location.pathname.startsWith('/applications/') ||
|
||||
(location.pathname === '/switch-workspace' && session?.current_organization_id)
|
||||
(session?.group_permissions && !isValidatingUserAccess) ||
|
||||
(pathname.startsWith('/applications/') && !isValidatingUserAccess) ||
|
||||
(pathname === '/switch-workspace' && session?.current_organization_id)
|
||||
) {
|
||||
return children;
|
||||
return isEditorOrViewerGoingToRender ? React.cloneElement(children, extraProps) : children;
|
||||
} else {
|
||||
if (
|
||||
(session?.authentication_status === false || session?.authentication_failed) &&
|
||||
|
|
@ -34,7 +80,7 @@ export const PrivateRoute = ({ children }) => {
|
|||
return (
|
||||
<Navigate
|
||||
to={{
|
||||
pathname: '/login',
|
||||
pathname: `/login${getWorkspaceId() ? `/${getWorkspaceId()}` : ''}`,
|
||||
search: `?redirectTo=${excludeWorkspaceIdFromURL(location.pathname)}`,
|
||||
state: { from: location },
|
||||
}}
|
||||
|
|
@ -43,15 +89,7 @@ export const PrivateRoute = ({ children }) => {
|
|||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="spin-loader">
|
||||
<div className="load">
|
||||
<div className="one"></div>
|
||||
<div className="two"></div>
|
||||
<div className="three"></div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
return <TJLoader />;
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -89,7 +127,7 @@ export const AdminRoute = ({ children }) => {
|
|||
return (
|
||||
<Navigate
|
||||
to={{
|
||||
pathname: '/login',
|
||||
pathname: `/login${getWorkspaceId() ? `/${getWorkspaceId()}` : ''}`,
|
||||
search: `?redirectTo=${location.pathname}`,
|
||||
state: { from: location },
|
||||
}}
|
||||
|
|
@ -98,14 +136,6 @@ export const AdminRoute = ({ children }) => {
|
|||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="spin-loader">
|
||||
<div className="load">
|
||||
<div className="one"></div>
|
||||
<div className="two"></div>
|
||||
<div className="three"></div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
return <TJLoader />;
|
||||
}
|
||||
};
|
||||
|
|
|
|||
192
frontend/src/_helpers/authorizeWorkspace.js
Normal file
192
frontend/src/_helpers/authorizeWorkspace.js
Normal file
|
|
@ -0,0 +1,192 @@
|
|||
import { organizationService, authenticationService } from '@/_services';
|
||||
import {
|
||||
pathnameToArray,
|
||||
getSubpath,
|
||||
getWorkspaceIdOrSlugFromURL,
|
||||
getPathname,
|
||||
getRedirectToWithParams,
|
||||
} from './routes';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
/* [* Be cautious: READ THE CASES BEFORE TOUCHING THE CODE. OTHERWISE YOU MAY SEE ENDLESS REDIRECTIONS (AKA ROUTES-BURMUDA-TRIANGLE) *]
|
||||
What is this function?
|
||||
- This function is used to authorize the workspace that the user is currently trying to open (for multi-workspace functionality across multiple tabs).
|
||||
|
||||
Cases / Steps
|
||||
CASE-1. Process the workspace slug. get workspace-id and basic session details. If the page is app viewer then we will get the workspace from the app-id
|
||||
CASE-2. Proceed with authorizing the workspace only if the page isn't `switch-workspace`
|
||||
CASE-3. If the user doesn't have valid session then PrivateRoute component will take care the rest [redirect to the login-page]
|
||||
CASE-4. If the page is app viewer and there is no valid session. consider the app is public
|
||||
*/
|
||||
|
||||
export const authorizeWorkspace = () => {
|
||||
if (!isThisExistedRoute()) {
|
||||
const workspaceIdOrSlug = getWorkspaceIdOrSlugFromURL();
|
||||
const isApplicationsPath = getPathname(null, true).includes('/applications/');
|
||||
const appId = isApplicationsPath ? getPathname().split('/')[2] : null;
|
||||
/* CASE-1 */
|
||||
authenticationService
|
||||
.validateSession(appId, workspaceIdOrSlug)
|
||||
.then(({ current_organization_id, current_organization_slug }) => {
|
||||
if (window.location.pathname !== `${getSubpath() ?? ''}/switch-workspace`) {
|
||||
/*CASE-2*/
|
||||
authorizeUserAndHandleErrors(current_organization_id, current_organization_slug);
|
||||
} else {
|
||||
updateCurrentSession({
|
||||
current_organization_id,
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
if ((error && error?.data?.statusCode == 422) || error?.data?.statusCode == 404) {
|
||||
const subpath = getSubpath();
|
||||
if (appId) {
|
||||
/* If the user is trying to load the app viewer and the app id / slug not found */
|
||||
toast.error("Couldn't find the app. \n Please verify the app URL again.");
|
||||
setTimeout(() => {
|
||||
window.location.href = subpath ? `${subpath}` : '/';
|
||||
}, 3000);
|
||||
return;
|
||||
} else {
|
||||
window.location = subpath ? `${subpath}${'/switch-workspace'}` : '/switch-workspace';
|
||||
}
|
||||
}
|
||||
if (!isThisWorkspaceLoginPage(true) && !isApplicationsPath) {
|
||||
/* CASE-3 */
|
||||
updateCurrentSession({
|
||||
authentication_status: false,
|
||||
});
|
||||
} else if (isApplicationsPath) {
|
||||
/* CASE-4 */
|
||||
updateCurrentSession({
|
||||
authentication_failed: true,
|
||||
load_app: true,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const 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 = pathnameToArray();
|
||||
const checkPath = () => existedPaths.find((path) => pathnames[subpath ? subpathArray.length : 0] === path);
|
||||
return pathnames?.length > 0 ? (checkPath() ? true : false) : false;
|
||||
};
|
||||
|
||||
const fetchOrganizations = (current_organization_id, callback) => {
|
||||
organizationService.getOrganizations().then((response) => {
|
||||
const current_organization = response.organizations?.find((org) => org.id === current_organization_id);
|
||||
callback({
|
||||
organizations: response.organizations,
|
||||
current_organization,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const isThisWorkspaceLoginPage = (justLoginPage = false) => {
|
||||
const subpath = getSubpath();
|
||||
const pathname = location.pathname.replace(subpath, '');
|
||||
const pathnames = pathname.split('/').filter((path) => path !== '');
|
||||
return (justLoginPage && pathnames[0] === 'login') || (pathnames.length === 2 && pathnames[0] === 'login');
|
||||
};
|
||||
|
||||
const updateCurrentSession = (newSession) => {
|
||||
const currentSession = authenticationService.currentSessionValue;
|
||||
authenticationService.updateCurrentSession({ ...currentSession, ...newSession });
|
||||
};
|
||||
|
||||
/*
|
||||
Cases / Steps
|
||||
CASE-1: If the user is authorized, they will be directed to the loading page. (Check: If the token is authorized for the specific workspace ID.)
|
||||
CASE-2: If not, the function checks if the user is authenticated for a different workspace. If so, it attempts to switch workspaces.
|
||||
CASE-3: If CASE-2 fails (indicating the need to log in to the workspace or having an invalid session), the user is directed to the workspace login page.
|
||||
CASE-4: During the execution of CASE-2, if the user has a valid session but encounters errors such as an incorrect workspace ID or non-existent workspace, they will be directed to the switch-workspace page.
|
||||
*/
|
||||
export const authorizeUserAndHandleErrors = (workspace_id, workspace_slug) => {
|
||||
const subpath = getSubpath();
|
||||
//initial session details
|
||||
updateCurrentSession({
|
||||
...(workspace_id && { current_organization_id: workspace_id }),
|
||||
});
|
||||
|
||||
authenticationService
|
||||
.authorize()
|
||||
.then((data) => {
|
||||
/* CASE-1 */
|
||||
const { current_organization_id } = data;
|
||||
fetchOrganizations(current_organization_id, ({ organizations, current_organization }) => {
|
||||
const { name: current_organization_name } = current_organization;
|
||||
/* add the user details like permission and user previlliage details to the subject */
|
||||
updateCurrentSession({
|
||||
...data,
|
||||
current_organization_name,
|
||||
organizations,
|
||||
load_app: true,
|
||||
});
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
if (error && error?.data?.statusCode === 401) {
|
||||
/* CASE-2 */
|
||||
/* if the auth token didn't contain workspace-id, try switch workspace fn */
|
||||
|
||||
const unauthorized_organization_id = workspace_id;
|
||||
const unauthorized_organization_slug = workspace_slug;
|
||||
|
||||
/* get current session's workspace id */
|
||||
authenticationService
|
||||
.validateSession()
|
||||
.then(({ current_organization_id }) => {
|
||||
/* change current organization id to valid one [current logged in organization] */
|
||||
updateCurrentSession({
|
||||
current_organization_id,
|
||||
});
|
||||
|
||||
organizationService
|
||||
.switchOrganization(unauthorized_organization_id)
|
||||
.then(() => {
|
||||
authorizeUserAndHandleErrors(unauthorized_organization_id);
|
||||
})
|
||||
.catch(() => {
|
||||
/* CASE-3 */
|
||||
fetchOrganizations(current_organization_id, ({ current_organization }) => {
|
||||
const { name: current_organization_name, slug: current_organization_slug } = current_organization;
|
||||
updateCurrentSession({
|
||||
current_organization_name,
|
||||
current_organization_slug,
|
||||
load_app: true,
|
||||
});
|
||||
|
||||
if (!isThisWorkspaceLoginPage())
|
||||
return (window.location = `${
|
||||
subpath ?? ''
|
||||
}/login/${unauthorized_organization_slug}?redirectTo=${getRedirectToWithParams()}`);
|
||||
});
|
||||
});
|
||||
})
|
||||
/* CASE-3 */
|
||||
.catch(() => authenticationService.logout());
|
||||
} else if ((error && error?.data?.statusCode == 422) || error?.data?.statusCode == 404) {
|
||||
/* CASE-4 */
|
||||
window.location = subpath ? `${subpath}${'/switch-workspace'}` : '/switch-workspace';
|
||||
} else {
|
||||
/* Any other errors, leave the user on current page [Let the page or private-route component take care] */
|
||||
if (!isThisWorkspaceLoginPage() && !isThisWorkspaceLoginPage(true))
|
||||
updateCurrentSession({
|
||||
authentication_status: false,
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
|
@ -1,13 +1,11 @@
|
|||
import { authenticationService } from '@/_services';
|
||||
|
||||
export function handleResponse(response) {
|
||||
export function handleResponse(response, avoidRedirection = false) {
|
||||
return response.text().then((text) => {
|
||||
const data = text && JSON.parse(text);
|
||||
if (!response.ok) {
|
||||
if ([401].indexOf(response.status) !== -1) {
|
||||
// auto logout if 401 Unauthorized or 403 Forbidden response returned from api
|
||||
authenticationService.logout();
|
||||
// location.reload(true);
|
||||
avoidRedirection ? authenticationService.logout() : location.reload(true);
|
||||
}
|
||||
|
||||
const error = (data && data.message) || response.statusText;
|
||||
|
|
|
|||
75
frontend/src/_helpers/handleAppAccess.js
Normal file
75
frontend/src/_helpers/handleAppAccess.js
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
import { organizationService, authenticationService, appsService } from '@/_services';
|
||||
import { safelyParseJSON, getWorkspaceId } from '@/_helpers/utils';
|
||||
import { redirectToDashboard, getSubpath, getQueryParams } from '@/_helpers/routes';
|
||||
import { toast } from 'react-hot-toast';
|
||||
import _ from 'lodash';
|
||||
import queryString from 'query-string';
|
||||
|
||||
export const handleAppAccess = (componentType, slug) => {
|
||||
const previewQueryParams = getPreviewQueryParams();
|
||||
const isLocalPreview = !_.isEmpty(previewQueryParams);
|
||||
const queryParams = { ...previewQueryParams, access_type: isLocalPreview ? 'view' : 'edit' };
|
||||
const query = queryString.stringify(previewQueryParams);
|
||||
const redirectPath = !_.isEmpty(query) ? `/applications/${slug}${query ? `?${query}` : ''}` : `/apps/${slug}`;
|
||||
|
||||
if (componentType === 'editor' || isLocalPreview) {
|
||||
/* Editor or app preview */
|
||||
return appsService.validatePrivateApp(slug, queryParams).catch((error) => {
|
||||
handleError(componentType, error, slug, redirectPath);
|
||||
});
|
||||
} else {
|
||||
/* Released app link [launch/sharable link] */
|
||||
return appsService.validateReleasedApp(slug).catch((error) => {
|
||||
handleError(componentType, error, redirectPath);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const switchOrganization = (componentType, orgId, redirectPath) => {
|
||||
const path = redirectPath;
|
||||
const sub_path = getSubpath() ?? '';
|
||||
organizationService.switchOrganization(orgId).then(
|
||||
() => {
|
||||
window.location.href = componentType === 'editor' ? `${sub_path}/${orgId}${path}` : `${sub_path}${path}`;
|
||||
},
|
||||
() => {
|
||||
return (window.location.href = `${sub_path}/login/${orgId}?redirectTo=${path}`);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const handleError = (componentType, error, redirectPath) => {
|
||||
try {
|
||||
if (error?.data) {
|
||||
const statusCode = error.data?.statusCode;
|
||||
if (statusCode === 403) {
|
||||
const errorObj = safelyParseJSON(error.data?.message);
|
||||
const currentSessionValue = authenticationService.currentSessionValue;
|
||||
if (
|
||||
errorObj?.organizationId &&
|
||||
currentSessionValue.current_user &&
|
||||
currentSessionValue.current_organization_id !== errorObj?.organizationId
|
||||
) {
|
||||
switchOrganization(componentType, errorObj?.organizationId, redirectPath);
|
||||
return;
|
||||
}
|
||||
redirectToDashboard();
|
||||
} else if (statusCode === 401) {
|
||||
window.location = `${getSubpath() ?? ''}/login/${getWorkspaceId()}?redirectTo=${redirectPath}`;
|
||||
return;
|
||||
} else if (statusCode === 404 || statusCode === 422) {
|
||||
toast.error(error?.error ?? 'App not found');
|
||||
}
|
||||
redirectToDashboard();
|
||||
}
|
||||
} catch (err) {
|
||||
redirectToDashboard();
|
||||
}
|
||||
};
|
||||
|
||||
const getPreviewQueryParams = () => {
|
||||
const queryParams = getQueryParams();
|
||||
return {
|
||||
...(queryParams['version'] && { version_name: queryParams.version }),
|
||||
};
|
||||
};
|
||||
|
|
@ -68,7 +68,7 @@ class HttpClient {
|
|||
// TODO: add 403 to the below [401] array?
|
||||
if ([401].indexOf(response.status) !== -1) {
|
||||
// auto logout if 401 Unauthorized or 403 Forbidden response returned from api
|
||||
authenticationService.logout();
|
||||
location.reload();
|
||||
}
|
||||
|
||||
throw payload;
|
||||
|
|
|
|||
|
|
@ -1,12 +1,15 @@
|
|||
import { getWorkspaceIdFromURL } from '@/_helpers/utils';
|
||||
/* You can add all paths and routes related utils here */
|
||||
import { stripTrailingSlash, getWorkspaceId } from '@/_helpers/utils';
|
||||
import { authenticationService } from '@/_services/authentication.service';
|
||||
import queryString from 'query-string';
|
||||
import _ from 'lodash';
|
||||
|
||||
export const getPrivateRoute = (page, params = {}) => {
|
||||
const routes = {
|
||||
dashboard: '/',
|
||||
editor: '/apps/:id/:pageHandle?',
|
||||
preview: '/applications/:id/versions/:versionId/:pageHandle?',
|
||||
launch: '/applications/:slug/:pageHandle?',
|
||||
editor: '/apps/:slug/:pageHandle',
|
||||
preview: '/applications/:slug/versions/:versionId/:pageHandle',
|
||||
launch: '/applications/:slug/:pageHandle',
|
||||
workspace_settings: '/workspace-settings',
|
||||
settings: '/settings',
|
||||
database: '/database',
|
||||
|
|
@ -23,10 +26,149 @@ export const getPrivateRoute = (page, params = {}) => {
|
|||
});
|
||||
url = urlParams.join('/');
|
||||
|
||||
return appendWorkspaceId(url.replace(/\/$/, ''));
|
||||
const workspaceId =
|
||||
getWorkspaceIdOrSlugFromURL() ||
|
||||
authenticationService.currentSessionValue?.current_organization_slug ||
|
||||
authenticationService.currentSessionValue?.current_organization_id;
|
||||
return `/${workspaceId}${url.replace(/\/$/, '')}`;
|
||||
};
|
||||
|
||||
const appendWorkspaceId = (url) => {
|
||||
const workspaceId = getWorkspaceIdFromURL() || authenticationService.currentSessionValue?.current_organization_id;
|
||||
return `/${workspaceId}${url}`;
|
||||
export const replaceEditorURL = (slug, pageHandle) => {
|
||||
const subpath = getSubpath();
|
||||
const path = subpath
|
||||
? `${subpath}${getPrivateRoute('editor', { slug, pageHandle })}`
|
||||
: getPrivateRoute('editor', { slug, pageHandle });
|
||||
window.history.replaceState(null, null, path);
|
||||
};
|
||||
|
||||
export function getQueryParams(query) {
|
||||
const search = window.location.search.substring(1); // Remove the '?' at the beginning
|
||||
const paramsArray = search.split('&');
|
||||
const queryParams = {};
|
||||
|
||||
for (const param of paramsArray) {
|
||||
const [key, value] = param.split('=');
|
||||
queryParams[key] = decodeURIComponent(value);
|
||||
}
|
||||
|
||||
return query ? queryParams[query] : queryParams;
|
||||
}
|
||||
|
||||
export const pathnameToArray = () => window.location.pathname.split('/').filter((path) => path != '');
|
||||
|
||||
export const getPathname = (path, excludeSlug = false) => {
|
||||
const pathname = excludeSlug ? excludeWorkspaceIdFromURL(window.location.pathname) : window.location.pathname;
|
||||
return getSubpath() ? (path || pathname).replace(getSubpath(), '') : path || pathname;
|
||||
};
|
||||
|
||||
export const getHostURL = () => `${window.public_config?.TOOLJET_HOST}${getSubpath() ?? ''}`;
|
||||
|
||||
export const redirectToDashboard = (data) => {
|
||||
const { current_organization_slug, current_organization_id } = authenticationService.currentSessionValue;
|
||||
const id_slug = data
|
||||
? data?.current_organization_slug || data?.current_organization_id
|
||||
: current_organization_slug || current_organization_id || '';
|
||||
window.location = getSubpath() ? `${getSubpath()}/${id_slug}` : `/${id_slug}`;
|
||||
};
|
||||
|
||||
export const appendWorkspaceId = (slug, path, replaceId = false) => {
|
||||
const subpath = getSubpath();
|
||||
path = getPathname(path);
|
||||
|
||||
let newPath = path;
|
||||
if (path === '/:workspaceId' || path.split('/').length === 2) {
|
||||
newPath = `/${slug}`;
|
||||
} else {
|
||||
const paths = path.split('/').filter((path) => path !== '');
|
||||
if (replaceId) {
|
||||
paths[0] = slug;
|
||||
} else {
|
||||
paths.unshift(slug);
|
||||
}
|
||||
newPath = `/${paths.join('/')}`;
|
||||
}
|
||||
return subpath ? `${subpath}${newPath}` : newPath;
|
||||
};
|
||||
|
||||
export const getWorkspaceIdOrSlugFromURL = () => {
|
||||
const pathnameArray = pathnameToArray();
|
||||
const subpath = window?.public_config?.SUB_PATH;
|
||||
const subpathArray = subpath ? subpath.split('/').filter((path) => path != '') : [];
|
||||
const existedPaths = [
|
||||
'forgot-password',
|
||||
'switch-workspace',
|
||||
'reset-password',
|
||||
'invitations',
|
||||
'organization-invitations',
|
||||
'sso',
|
||||
'setup',
|
||||
'confirm',
|
||||
':workspaceId',
|
||||
'confirm-invite',
|
||||
'oauth2',
|
||||
'applications',
|
||||
'integrations',
|
||||
];
|
||||
|
||||
const workspaceId = subpath ? pathnameArray[subpathArray.length] : pathnameArray[0];
|
||||
if (workspaceId === 'login') {
|
||||
return subpath ? pathnameArray[subpathArray.length + 1] : pathnameArray[1];
|
||||
}
|
||||
|
||||
return !existedPaths.includes(workspaceId) ? workspaceId : '';
|
||||
};
|
||||
|
||||
export const excludeWorkspaceIdFromURL = (pathname) => {
|
||||
if (!['integrations', 'applications', 'switch-workspace'].find((path) => pathname.includes(path))) {
|
||||
pathname = getSubpath() ? pathname.replace(getSubpath(), '') : pathname;
|
||||
const paths = pathname?.split('/').filter((path) => path !== '');
|
||||
paths.shift();
|
||||
const newPath = paths.join('/');
|
||||
return newPath ? `/${newPath}` : '/';
|
||||
}
|
||||
return pathname;
|
||||
};
|
||||
|
||||
export const getSubpath = () =>
|
||||
window?.public_config?.SUB_PATH ? stripTrailingSlash(window?.public_config?.SUB_PATH) : null;
|
||||
|
||||
const returnWorkspaceIdIfNeed = (path) => {
|
||||
if (path) {
|
||||
return !path.includes('applications') && !path.includes('integrations') ? `/${getWorkspaceId()}` : '';
|
||||
}
|
||||
return `/${getWorkspaceId()}`;
|
||||
};
|
||||
|
||||
export const getRedirectURL = (path) => {
|
||||
let redirectLoc = '/';
|
||||
if (path) {
|
||||
redirectLoc = `${returnWorkspaceIdIfNeed(path)}${path !== '/' ? path : ''}`;
|
||||
} else {
|
||||
const redirectTo = getRedirectTo();
|
||||
const { from } = redirectTo ? { from: { pathname: redirectTo } } : { from: { pathname: '/' } };
|
||||
if (from.pathname !== '/confirm')
|
||||
from.pathname = `${returnWorkspaceIdIfNeed(from.pathname)}${from.pathname !== '/' ? from.pathname : ''}`;
|
||||
redirectLoc = from.pathname;
|
||||
}
|
||||
|
||||
return redirectLoc;
|
||||
};
|
||||
|
||||
export const getRedirectTo = () => {
|
||||
const params = new URL(window.location.href).searchParams;
|
||||
return params.get('redirectTo') || '/';
|
||||
};
|
||||
|
||||
export const getPreviewQueryParams = () => {
|
||||
const queryParams = getQueryParams();
|
||||
return {
|
||||
...(queryParams['version'] && { version: queryParams.version }),
|
||||
};
|
||||
};
|
||||
|
||||
export const getRedirectToWithParams = () => {
|
||||
const pathname = getPathname(null, true);
|
||||
const queryParams = pathname.includes('/applications/') ? getPreviewQueryParams() : {};
|
||||
const query = !_.isEmpty(queryParams) ? queryString.stringify(queryParams) : '';
|
||||
return `${pathname}${!_.isEmpty(query) ? `?${query}` : ''}`;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import { authenticationService } from '@/_services/authentication.service';
|
|||
|
||||
import { useDataQueriesStore } from '@/_stores/dataQueriesStore';
|
||||
import { getCurrentState } from '@/_stores/currentStateStore';
|
||||
import { getWorkspaceIdOrSlugFromURL, getSubpath } from './routes';
|
||||
import { getCookie, eraseCookie } from '@/_helpers/cookie';
|
||||
import { staticDataSources } from '@/Editor/QueryManager/constants';
|
||||
|
||||
|
|
@ -804,73 +805,14 @@ export const getuserName = (formData) => {
|
|||
return '';
|
||||
};
|
||||
|
||||
export const pathnameWithoutSubpath = (path) => {
|
||||
const subpath = getSubpath();
|
||||
if (subpath) return path.replace(subpath, '');
|
||||
return path;
|
||||
};
|
||||
|
||||
// will replace or append workspace-id in a path
|
||||
export const appendWorkspaceId = (workspaceId, path, replaceId = false) => {
|
||||
const subpath = getSubpath();
|
||||
path = pathnameWithoutSubpath(path);
|
||||
|
||||
let newPath = path;
|
||||
if (path === '/:workspaceId' || path.split('/').length === 2) {
|
||||
newPath = `/${workspaceId}`;
|
||||
} else {
|
||||
const paths = path.split('/').filter((path) => path !== '');
|
||||
if (replaceId) {
|
||||
paths[0] = workspaceId;
|
||||
} else {
|
||||
paths.unshift(workspaceId);
|
||||
}
|
||||
newPath = `/${paths.join('/')}`;
|
||||
}
|
||||
return subpath ? `${subpath}${newPath}` : newPath;
|
||||
};
|
||||
|
||||
export const getWorkspaceIdFromURL = () => {
|
||||
const pathname = window.location.pathname;
|
||||
const pathnameArray = pathname.split('/').filter((path) => path !== '');
|
||||
const subpath = window?.public_config?.SUB_PATH;
|
||||
const subpathArray = subpath ? subpath.split('/').filter((path) => path != '') : [];
|
||||
const existedPaths = [
|
||||
'forgot-password',
|
||||
'switch-workspace',
|
||||
'reset-password',
|
||||
'invitations',
|
||||
'organization-invitations',
|
||||
'sso',
|
||||
'setup',
|
||||
'confirm',
|
||||
':workspaceId',
|
||||
'confirm-invite',
|
||||
'oauth2',
|
||||
'applications',
|
||||
'integrations',
|
||||
];
|
||||
|
||||
const workspaceId = subpath ? pathnameArray[subpathArray.length] : pathnameArray[0];
|
||||
if (workspaceId === 'login') {
|
||||
return subpath ? pathnameArray[subpathArray.length + 1] : pathnameArray[1];
|
||||
}
|
||||
|
||||
return !existedPaths.includes(workspaceId) ? workspaceId : '';
|
||||
export const removeSpaceFromWorkspace = (name) => {
|
||||
return name?.replace(' ', '-') || '';
|
||||
};
|
||||
|
||||
export const getWorkspaceId = () =>
|
||||
getWorkspaceIdFromURL() || authenticationService.currentSessionValue?.current_organization_id;
|
||||
|
||||
export const excludeWorkspaceIdFromURL = (pathname) => {
|
||||
if (!pathname.includes('/applications/')) {
|
||||
const paths = pathname?.split('/').filter((path) => path !== '');
|
||||
paths.shift();
|
||||
const newPath = paths.join('/');
|
||||
return newPath ? `/${newPath}` : '/';
|
||||
}
|
||||
return pathname;
|
||||
};
|
||||
getWorkspaceIdOrSlugFromURL() ||
|
||||
authenticationService.currentSessionValue?.current_organization_slug ||
|
||||
authenticationService.currentSessionValue?.current_organization_id;
|
||||
|
||||
export const handleUnSubscription = (subsciption) => {
|
||||
setTimeout(() => {
|
||||
|
|
@ -891,8 +833,6 @@ export const getAvatar = (organization) => {
|
|||
}
|
||||
};
|
||||
|
||||
export const getSubpath = () =>
|
||||
window?.public_config?.SUB_PATH ? stripTrailingSlash(window?.public_config?.SUB_PATH) : null;
|
||||
export function isExpectedDataType(data, expectedDataType) {
|
||||
function getCurrentDataType(node) {
|
||||
return Object.prototype.toString.call(node).slice(8, -1).toLowerCase();
|
||||
|
|
@ -920,8 +860,16 @@ export function isExpectedDataType(data, expectedDataType) {
|
|||
return data;
|
||||
}
|
||||
|
||||
export const validateName = (name, nameType, emptyCheck = true, showError = false, allowSpecialChars = true) => {
|
||||
const newName = name.trim();
|
||||
export const validateName = (
|
||||
name,
|
||||
nameType,
|
||||
emptyCheck = true,
|
||||
showError = false,
|
||||
allowSpecialChars = true,
|
||||
allowSpaces = true,
|
||||
checkReservedWords = false
|
||||
) => {
|
||||
const newName = name;
|
||||
let errorMsg = '';
|
||||
if (emptyCheck && !newName) {
|
||||
errorMsg = `${nameType} can't be empty`;
|
||||
|
|
@ -935,33 +883,78 @@ export const validateName = (name, nameType, emptyCheck = true, showError = fals
|
|||
};
|
||||
}
|
||||
|
||||
//check for alphanumeric
|
||||
if (!allowSpecialChars && newName.match(/^[a-z0-9 -]+$/) === null) {
|
||||
if (/[A-Z]/.test(newName)) {
|
||||
errorMsg = 'Only lowercase letters are accepted.';
|
||||
} else {
|
||||
errorMsg = `Special characters are not accepted.`;
|
||||
if (newName) {
|
||||
//check for alphanumeric
|
||||
if (!allowSpecialChars && newName.match(/^[a-z0-9 -]+$/) === null) {
|
||||
if (/[A-Z]/.test(newName)) {
|
||||
errorMsg = 'Only lowercase letters are accepted.';
|
||||
} else {
|
||||
errorMsg = `Special characters are not accepted.`;
|
||||
}
|
||||
showError &&
|
||||
toast.error(errorMsg, {
|
||||
id: '2',
|
||||
});
|
||||
return {
|
||||
status: false,
|
||||
errorMsg,
|
||||
};
|
||||
}
|
||||
showError &&
|
||||
toast.error(errorMsg, {
|
||||
id: '2',
|
||||
});
|
||||
return {
|
||||
status: false,
|
||||
errorMsg,
|
||||
};
|
||||
}
|
||||
|
||||
if (newName.length > 50) {
|
||||
errorMsg = `Maximum length has been reached.`;
|
||||
showError &&
|
||||
toast.error(errorMsg, {
|
||||
id: '3',
|
||||
});
|
||||
return {
|
||||
status: false,
|
||||
errorMsg,
|
||||
};
|
||||
if (!allowSpaces && /\s/g.test(newName)) {
|
||||
errorMsg = 'Cannot contain spaces';
|
||||
showError &&
|
||||
toast.error(errorMsg, {
|
||||
id: '3',
|
||||
});
|
||||
return {
|
||||
status: false,
|
||||
errorMsg,
|
||||
};
|
||||
}
|
||||
|
||||
if (newName.length > 50) {
|
||||
errorMsg = `Maximum length has been reached.`;
|
||||
showError &&
|
||||
toast.error(errorMsg, {
|
||||
id: '3',
|
||||
});
|
||||
return {
|
||||
status: false,
|
||||
errorMsg,
|
||||
};
|
||||
}
|
||||
|
||||
/* Add more reserved paths here, which doesn't have /:workspace-id prefix */
|
||||
const reservedPaths = [
|
||||
'forgot-password',
|
||||
'switch-workspace',
|
||||
'reset-password',
|
||||
'invitations',
|
||||
'organization-invitations',
|
||||
'sso',
|
||||
'setup',
|
||||
'confirm',
|
||||
':workspaceId',
|
||||
'confirm-invite',
|
||||
'oauth2',
|
||||
'applications',
|
||||
'integrations',
|
||||
'login',
|
||||
'signup',
|
||||
];
|
||||
|
||||
if (checkReservedWords && reservedPaths.includes(newName)) {
|
||||
errorMsg = `Reserved words are not allowed.`;
|
||||
showError &&
|
||||
toast.error(errorMsg, {
|
||||
id: '3',
|
||||
});
|
||||
return {
|
||||
status: false,
|
||||
errorMsg,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -3,29 +3,9 @@ import { authHeader, handleResponse } from '@/_helpers';
|
|||
|
||||
export const appService = {
|
||||
getConfig,
|
||||
getAll,
|
||||
createApp,
|
||||
cloneApp,
|
||||
exportApp,
|
||||
importApp,
|
||||
exportResource,
|
||||
importResource,
|
||||
cloneResource,
|
||||
changeIcon,
|
||||
deleteApp,
|
||||
getApp,
|
||||
getAppBySlug,
|
||||
getAppByVersion,
|
||||
saveApp,
|
||||
getAppUsers,
|
||||
createAppUser,
|
||||
setVisibility,
|
||||
setMaintenance,
|
||||
setSlug,
|
||||
setPasswordFromToken,
|
||||
acceptInvite,
|
||||
getVersions,
|
||||
getTables,
|
||||
};
|
||||
|
||||
function getConfig() {
|
||||
|
|
@ -33,137 +13,6 @@ function getConfig() {
|
|||
return fetch(`${config.apiUrl}/config`, requestOptions).then(handleResponse);
|
||||
}
|
||||
|
||||
function getAll(page, folder, searchKey) {
|
||||
const requestOptions = { method: 'GET', headers: authHeader(), credentials: 'include' };
|
||||
if (page === 0) return fetch(`${config.apiUrl}/apps`, requestOptions).then(handleResponse);
|
||||
else
|
||||
return fetch(
|
||||
`${config.apiUrl}/apps?page=${page}&folder=${folder || ''}&searchKey=${searchKey}`,
|
||||
requestOptions
|
||||
).then(handleResponse);
|
||||
}
|
||||
|
||||
function createApp(body = {}) {
|
||||
const requestOptions = { method: 'POST', headers: authHeader(), credentials: 'include', body: JSON.stringify(body) };
|
||||
return fetch(`${config.apiUrl}/apps`, requestOptions).then(handleResponse);
|
||||
}
|
||||
|
||||
function cloneApp(id, name) {
|
||||
const requestOptions = {
|
||||
method: 'POST',
|
||||
headers: authHeader(),
|
||||
credentials: 'include',
|
||||
body: JSON.stringify({ name }),
|
||||
};
|
||||
return fetch(`${config.apiUrl}/apps/${id}/clone`, requestOptions).then(handleResponse);
|
||||
}
|
||||
|
||||
function exportApp(id, versionId) {
|
||||
const requestOptions = { method: 'GET', headers: authHeader(), credentials: 'include' };
|
||||
return fetch(`${config.apiUrl}/apps/${id}/export${versionId ? `?versionId=${versionId}` : ''}`, requestOptions).then(
|
||||
handleResponse
|
||||
);
|
||||
}
|
||||
|
||||
function exportResource(body) {
|
||||
const requestOptions = {
|
||||
method: 'POST',
|
||||
headers: authHeader(),
|
||||
body: JSON.stringify(body),
|
||||
credentials: 'include',
|
||||
};
|
||||
|
||||
return fetch(`${config.apiUrl}/v2/resources/export`, requestOptions).then(handleResponse);
|
||||
}
|
||||
|
||||
function importResource(body) {
|
||||
const requestOptions = {
|
||||
method: 'POST',
|
||||
headers: authHeader(),
|
||||
credentials: 'include',
|
||||
body: JSON.stringify(body),
|
||||
};
|
||||
return fetch(`${config.apiUrl}/v2/resources/import`, requestOptions).then(handleResponse);
|
||||
}
|
||||
|
||||
function cloneResource(body) {
|
||||
const requestOptions = {
|
||||
method: 'POST',
|
||||
headers: authHeader(),
|
||||
body: JSON.stringify(body),
|
||||
credentials: 'include',
|
||||
};
|
||||
|
||||
return fetch(`${config.apiUrl}/v2/resources/clone`, requestOptions).then(handleResponse);
|
||||
}
|
||||
|
||||
function getVersions(id) {
|
||||
const requestOptions = { method: 'GET', headers: authHeader(), credentials: 'include' };
|
||||
return fetch(`${config.apiUrl}/apps/${id}/versions`, requestOptions).then(handleResponse);
|
||||
}
|
||||
|
||||
function importApp(app, name) {
|
||||
const requestOptions = {
|
||||
method: 'POST',
|
||||
headers: authHeader(),
|
||||
credentials: 'include',
|
||||
body: JSON.stringify({ app, name }),
|
||||
};
|
||||
return fetch(`${config.apiUrl}/apps/import`, requestOptions).then(handleResponse);
|
||||
}
|
||||
|
||||
function getTables(id) {
|
||||
const requestOptions = { method: 'GET', headers: authHeader(), credentials: 'include' };
|
||||
return fetch(`${config.apiUrl}/apps/${id}/tables`, requestOptions).then(handleResponse);
|
||||
}
|
||||
|
||||
function changeIcon(icon, appId) {
|
||||
const requestOptions = {
|
||||
method: 'PUT',
|
||||
headers: authHeader(),
|
||||
credentials: 'include',
|
||||
body: JSON.stringify({ icon }),
|
||||
};
|
||||
return fetch(`${config.apiUrl}/apps/${appId}/icons`, requestOptions).then(handleResponse);
|
||||
}
|
||||
|
||||
function getApp(id, accessType) {
|
||||
const requestOptions = { method: 'GET', headers: authHeader(), credentials: 'include' };
|
||||
return fetch(`${config.apiUrl}/apps/${id}${accessType ? `?access_type=${accessType}` : ''}`, requestOptions).then(
|
||||
handleResponse
|
||||
);
|
||||
}
|
||||
|
||||
function deleteApp(id) {
|
||||
const requestOptions = { method: 'DELETE', headers: authHeader(), credentials: 'include' };
|
||||
return fetch(`${config.apiUrl}/apps/${id}`, requestOptions).then(handleResponse);
|
||||
}
|
||||
|
||||
function getAppBySlug(slug) {
|
||||
const requestOptions = { method: 'GET', headers: authHeader(), credentials: 'include' };
|
||||
return fetch(`${config.apiUrl}/apps/slugs/${slug}`, requestOptions).then(handleResponse);
|
||||
}
|
||||
|
||||
function getAppByVersion(appId, versionId) {
|
||||
const requestOptions = { method: 'GET', headers: authHeader(), credentials: 'include' };
|
||||
return fetch(`${config.apiUrl}/apps/${appId}/versions/${versionId}`, requestOptions).then(handleResponse);
|
||||
}
|
||||
|
||||
function saveApp(id, attributes) {
|
||||
const requestOptions = {
|
||||
method: 'PUT',
|
||||
headers: authHeader(),
|
||||
credentials: 'include',
|
||||
body: JSON.stringify({ app: attributes }),
|
||||
};
|
||||
return fetch(`${config.apiUrl}/apps/${id}`, requestOptions).then(handleResponse);
|
||||
}
|
||||
|
||||
function getAppUsers(id) {
|
||||
const requestOptions = { method: 'GET', headers: authHeader(), credentials: 'include' };
|
||||
return fetch(`${config.apiUrl}/apps/${id}/users`, requestOptions).then(handleResponse);
|
||||
}
|
||||
|
||||
function createAppUser(app_id, org_user_id, role) {
|
||||
const body = {
|
||||
app_id,
|
||||
|
|
@ -175,36 +24,6 @@ function createAppUser(app_id, org_user_id, role) {
|
|||
return fetch(`${config.apiUrl}/app_users`, requestOptions).then(handleResponse);
|
||||
}
|
||||
|
||||
function setVisibility(appId, visibility) {
|
||||
const requestOptions = {
|
||||
method: 'PUT',
|
||||
headers: authHeader(),
|
||||
credentials: 'include',
|
||||
body: JSON.stringify({ app: { is_public: visibility } }),
|
||||
};
|
||||
return fetch(`${config.apiUrl}/apps/${appId}`, requestOptions).then(handleResponse);
|
||||
}
|
||||
|
||||
function setMaintenance(appId, value) {
|
||||
const requestOptions = {
|
||||
method: 'PUT',
|
||||
headers: authHeader(),
|
||||
credentials: 'include',
|
||||
body: JSON.stringify({ app: { is_maintenance_on: value } }),
|
||||
};
|
||||
return fetch(`${config.apiUrl}/apps/${appId}`, requestOptions).then(handleResponse);
|
||||
}
|
||||
|
||||
function setSlug(appId, slug) {
|
||||
const requestOptions = {
|
||||
method: 'PUT',
|
||||
headers: authHeader(),
|
||||
credentials: 'include',
|
||||
body: JSON.stringify({ app: { slug: slug } }),
|
||||
};
|
||||
return fetch(`${config.apiUrl}/apps/${appId}`, requestOptions).then(handleResponse);
|
||||
}
|
||||
|
||||
function setPasswordFromToken({ token, password, organization, role, firstName, lastName, organizationToken }) {
|
||||
const body = {
|
||||
token,
|
||||
|
|
|
|||
206
frontend/src/_services/apps.service.js
Normal file
206
frontend/src/_services/apps.service.js
Normal file
|
|
@ -0,0 +1,206 @@
|
|||
import config from 'config';
|
||||
import { authHeader, handleResponse } from '@/_helpers';
|
||||
import queryString from 'query-string';
|
||||
|
||||
export const appsService = {
|
||||
validatePrivateApp,
|
||||
validateReleasedApp,
|
||||
setVisibility,
|
||||
setMaintenance,
|
||||
setSlug,
|
||||
getAll,
|
||||
createApp,
|
||||
cloneApp,
|
||||
exportApp,
|
||||
importApp,
|
||||
exportResource,
|
||||
importResource,
|
||||
cloneResource,
|
||||
changeIcon,
|
||||
deleteApp,
|
||||
getApp,
|
||||
getAppBySlug,
|
||||
getAppByVersion,
|
||||
saveApp,
|
||||
getAppUsers,
|
||||
getVersions,
|
||||
getTables,
|
||||
};
|
||||
|
||||
function validateReleasedApp(slug) {
|
||||
const requestOptions = { method: 'GET', headers: authHeader(), credentials: 'include' };
|
||||
return fetch(`${config.apiUrl}/apps/validate-released-app-access/${slug}`, requestOptions).then(handleResponse);
|
||||
}
|
||||
|
||||
function validatePrivateApp(slug, queryParams) {
|
||||
const requestOptions = { method: 'GET', headers: authHeader(), credentials: 'include' };
|
||||
const query = queryString.stringify(queryParams);
|
||||
|
||||
return fetch(
|
||||
`${config.apiUrl}/apps/validate-private-app-access/${slug}${query ? `?${query}` : ''}`,
|
||||
requestOptions
|
||||
).then(handleResponse);
|
||||
}
|
||||
|
||||
function getAll(page, folder, searchKey) {
|
||||
const requestOptions = { method: 'GET', headers: authHeader(), credentials: 'include' };
|
||||
if (page === 0) return fetch(`${config.apiUrl}/apps`, requestOptions).then(handleResponse);
|
||||
else
|
||||
return fetch(
|
||||
`${config.apiUrl}/apps?page=${page}&folder=${folder || ''}&searchKey=${searchKey}`,
|
||||
requestOptions
|
||||
).then(handleResponse);
|
||||
}
|
||||
|
||||
function createApp(body = {}) {
|
||||
const requestOptions = { method: 'POST', headers: authHeader(), credentials: 'include', body: JSON.stringify(body) };
|
||||
return fetch(`${config.apiUrl}/apps`, requestOptions).then(handleResponse);
|
||||
}
|
||||
|
||||
function cloneApp(id, name) {
|
||||
const requestOptions = {
|
||||
method: 'POST',
|
||||
headers: authHeader(),
|
||||
credentials: 'include',
|
||||
body: JSON.stringify({ name }),
|
||||
};
|
||||
return fetch(`${config.apiUrl}/apps/${id}/clone`, requestOptions).then(handleResponse);
|
||||
}
|
||||
|
||||
function exportApp(id, versionId) {
|
||||
const requestOptions = { method: 'GET', headers: authHeader(), credentials: 'include' };
|
||||
return fetch(`${config.apiUrl}/apps/${id}/export${versionId ? `?versionId=${versionId}` : ''}`, requestOptions).then(
|
||||
handleResponse
|
||||
);
|
||||
}
|
||||
|
||||
function getVersions(id) {
|
||||
const requestOptions = { method: 'GET', headers: authHeader(), credentials: 'include' };
|
||||
return fetch(`${config.apiUrl}/apps/${id}/versions`, requestOptions).then(handleResponse);
|
||||
}
|
||||
|
||||
function importApp(app, name) {
|
||||
const requestOptions = {
|
||||
method: 'POST',
|
||||
headers: authHeader(),
|
||||
credentials: 'include',
|
||||
body: JSON.stringify({ app, name }),
|
||||
};
|
||||
return fetch(`${config.apiUrl}/apps/import`, requestOptions).then(handleResponse);
|
||||
}
|
||||
|
||||
function changeIcon(icon, appId) {
|
||||
const requestOptions = {
|
||||
method: 'PUT',
|
||||
headers: authHeader(),
|
||||
credentials: 'include',
|
||||
body: JSON.stringify({ icon }),
|
||||
};
|
||||
return fetch(`${config.apiUrl}/apps/${appId}/icons`, requestOptions).then(handleResponse);
|
||||
}
|
||||
|
||||
function getApp(id, accessType) {
|
||||
const requestOptions = { method: 'GET', headers: authHeader(), credentials: 'include' };
|
||||
return fetch(`${config.apiUrl}/apps/${id}${accessType ? `?access_type=${accessType}` : ''}`, requestOptions).then(
|
||||
handleResponse
|
||||
);
|
||||
}
|
||||
|
||||
function deleteApp(id) {
|
||||
const requestOptions = { method: 'DELETE', headers: authHeader(), credentials: 'include' };
|
||||
return fetch(`${config.apiUrl}/apps/${id}`, requestOptions).then(handleResponse);
|
||||
}
|
||||
|
||||
function getAppBySlug(slug) {
|
||||
const requestOptions = { method: 'GET', headers: authHeader(), credentials: 'include' };
|
||||
return fetch(`${config.apiUrl}/apps/slugs/${slug}`, requestOptions).then((response) =>
|
||||
handleResponse(response, true)
|
||||
);
|
||||
}
|
||||
|
||||
function getAppByVersion(appId, versionId) {
|
||||
const requestOptions = { method: 'GET', headers: authHeader(), credentials: 'include' };
|
||||
return fetch(`${config.apiUrl}/apps/${appId}/versions/${versionId}`, requestOptions).then(handleResponse);
|
||||
}
|
||||
|
||||
function saveApp(id, attributes) {
|
||||
const requestOptions = {
|
||||
method: 'PUT',
|
||||
headers: authHeader(),
|
||||
credentials: 'include',
|
||||
body: JSON.stringify({ app: attributes }),
|
||||
};
|
||||
return fetch(`${config.apiUrl}/apps/${id}`, requestOptions).then(handleResponse);
|
||||
}
|
||||
|
||||
function getAppUsers(id) {
|
||||
const requestOptions = { method: 'GET', headers: authHeader(), credentials: 'include' };
|
||||
return fetch(`${config.apiUrl}/apps/${id}/users`, requestOptions).then(handleResponse);
|
||||
}
|
||||
|
||||
function setVisibility(appId, visibility) {
|
||||
const requestOptions = {
|
||||
method: 'PUT',
|
||||
headers: authHeader(),
|
||||
credentials: 'include',
|
||||
body: JSON.stringify({ app: { is_public: visibility } }),
|
||||
};
|
||||
return fetch(`${config.apiUrl}/apps/${appId}`, requestOptions).then(handleResponse);
|
||||
}
|
||||
|
||||
function setMaintenance(appId, value) {
|
||||
const requestOptions = {
|
||||
method: 'PUT',
|
||||
headers: authHeader(),
|
||||
credentials: 'include',
|
||||
body: JSON.stringify({ app: { is_maintenance_on: value } }),
|
||||
};
|
||||
return fetch(`${config.apiUrl}/apps/${appId}`, requestOptions).then(handleResponse);
|
||||
}
|
||||
|
||||
function setSlug(appId, slug) {
|
||||
const requestOptions = {
|
||||
method: 'PUT',
|
||||
headers: authHeader(),
|
||||
credentials: 'include',
|
||||
body: JSON.stringify({ app: { slug: slug } }),
|
||||
};
|
||||
return fetch(`${config.apiUrl}/apps/${appId}`, requestOptions).then(handleResponse);
|
||||
}
|
||||
|
||||
function exportResource(body) {
|
||||
const requestOptions = {
|
||||
method: 'POST',
|
||||
headers: authHeader(),
|
||||
body: JSON.stringify(body),
|
||||
credentials: 'include',
|
||||
};
|
||||
|
||||
return fetch(`${config.apiUrl}/v2/resources/export`, requestOptions).then(handleResponse);
|
||||
}
|
||||
|
||||
function importResource(body) {
|
||||
const requestOptions = {
|
||||
method: 'POST',
|
||||
headers: authHeader(),
|
||||
credentials: 'include',
|
||||
body: JSON.stringify(body),
|
||||
};
|
||||
return fetch(`${config.apiUrl}/v2/resources/import`, requestOptions).then(handleResponse);
|
||||
}
|
||||
|
||||
function cloneResource(body) {
|
||||
const requestOptions = {
|
||||
method: 'POST',
|
||||
headers: authHeader(),
|
||||
body: JSON.stringify(body),
|
||||
credentials: 'include',
|
||||
};
|
||||
|
||||
return fetch(`${config.apiUrl}/v2/resources/clone`, requestOptions).then(handleResponse);
|
||||
}
|
||||
|
||||
function getTables(id) {
|
||||
const requestOptions = { method: 'GET', headers: authHeader(), credentials: 'include' };
|
||||
return fetch(`${config.apiUrl}/apps/${id}/tables`, requestOptions).then(handleResponse);
|
||||
}
|
||||
|
|
@ -7,8 +7,10 @@ import {
|
|||
handleResponseWithoutValidation,
|
||||
authHeader,
|
||||
} from '@/_helpers';
|
||||
import { excludeWorkspaceIdFromURL, getWorkspaceId } from '@/_helpers/utils';
|
||||
import { getWorkspaceId } from '@/_helpers/utils';
|
||||
import config from 'config';
|
||||
import queryString from 'query-string';
|
||||
import { getRedirectToWithParams } from '@/_helpers/routes';
|
||||
|
||||
const currentSessionSubject = new BehaviorSubject({
|
||||
current_organization_id: null,
|
||||
|
|
@ -50,6 +52,9 @@ export const authenticationService = {
|
|||
authorize,
|
||||
validateSession,
|
||||
getUserDetails,
|
||||
getLoginOrganizationSlug,
|
||||
saveLoginOrganizationSlug,
|
||||
deleteLoginOrganizationSlug,
|
||||
};
|
||||
|
||||
function login(email, password, organizationId) {
|
||||
|
|
@ -68,12 +73,13 @@ function login(email, password, organizationId) {
|
|||
});
|
||||
}
|
||||
|
||||
function validateSession(appId) {
|
||||
function validateSession(appId, workspaceSlug) {
|
||||
const requestOptions = {
|
||||
method: 'GET',
|
||||
credentials: 'include',
|
||||
};
|
||||
return fetch(`${config.apiUrl}/session${appId ? `?appId=${appId}` : ''}`, requestOptions).then(
|
||||
const query = queryString.stringify({ appId, workspaceSlug });
|
||||
return fetch(`${config.apiUrl}/session${query ? `?${query}` : ''}`, requestOptions).then(
|
||||
handleResponseWithoutValidation
|
||||
);
|
||||
}
|
||||
|
|
@ -95,6 +101,18 @@ function deleteLoginOrganizationId() {
|
|||
eraseCookie('login-workspace');
|
||||
}
|
||||
|
||||
function saveLoginOrganizationSlug(organizationSlug) {
|
||||
organizationSlug && setCookie('login-workspace-slug', organizationSlug);
|
||||
}
|
||||
|
||||
function getLoginOrganizationSlug() {
|
||||
return getCookie('login-workspace-slug');
|
||||
}
|
||||
|
||||
function deleteLoginOrganizationSlug() {
|
||||
eraseCookie('login-workspace-slug');
|
||||
}
|
||||
|
||||
function getOrganizationConfigs(organizationId) {
|
||||
const requestOptions = {
|
||||
method: 'GET',
|
||||
|
|
@ -238,31 +256,20 @@ function logout(avoidRedirection = false) {
|
|||
credentials: 'include',
|
||||
};
|
||||
|
||||
const redirectToLoginPage = () => {
|
||||
const loginPath =
|
||||
(window.public_config?.SUB_PATH || '/') + 'login' + `${getWorkspaceId() ? `/${getWorkspaceId()}` : ''}`;
|
||||
if (avoidRedirection) {
|
||||
window.location.href = loginPath;
|
||||
} else {
|
||||
const pathname = getRedirectToWithParams();
|
||||
window.location.href = loginPath + `?redirectTo=${`${pathname.indexOf('/') === 0 ? '' : '/'}${pathname}`}`;
|
||||
}
|
||||
};
|
||||
|
||||
return fetch(`${config.apiUrl}/logout`, requestOptions)
|
||||
.then(handleResponseWithoutValidation)
|
||||
.then(() => {
|
||||
const loginPath =
|
||||
(window.public_config?.SUB_PATH || '/') + 'login' + `${getWorkspaceId() ? `/${getWorkspaceId()}` : ''}`;
|
||||
if (avoidRedirection) {
|
||||
window.location.href = loginPath;
|
||||
} else {
|
||||
const pathname = window.public_config?.SUB_PATH
|
||||
? window.location.pathname.replace(window.public_config?.SUB_PATH, '')
|
||||
: window.location.pathname;
|
||||
window.location.href =
|
||||
loginPath +
|
||||
`?redirectTo=${
|
||||
!pathname.includes('integrations')
|
||||
? excludeWorkspaceIdFromURL(pathname)
|
||||
: `${pathname.indexOf('/') === 0 ? '' : '/'}${pathname}`
|
||||
}`;
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
authenticationService.updateCurrentSession({
|
||||
authentication_status: false,
|
||||
});
|
||||
});
|
||||
.finally(() => redirectToLoginPage());
|
||||
}
|
||||
|
||||
function signInViaOAuth(configId, ssoType, ssoResponse) {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
export * from './authentication.service';
|
||||
export * from './user.service';
|
||||
export * from './app.service';
|
||||
export * from './apps.service';
|
||||
export * from './datasource.service';
|
||||
export * from './dataquery.service';
|
||||
export * from './organization.service';
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ export const organizationService = {
|
|||
switchOrganization,
|
||||
getSSODetails,
|
||||
editOrganizationConfigs,
|
||||
checkWorkspaceUniqueness,
|
||||
};
|
||||
|
||||
function getUsers(page, options) {
|
||||
|
|
@ -28,12 +29,12 @@ function getUsersByValue(searchInput) {
|
|||
);
|
||||
}
|
||||
|
||||
function createOrganization(name) {
|
||||
function createOrganization(data) {
|
||||
const requestOptions = {
|
||||
method: 'POST',
|
||||
headers: authHeader(),
|
||||
credentials: 'include',
|
||||
body: JSON.stringify({ name }),
|
||||
body: JSON.stringify(data),
|
||||
};
|
||||
return fetch(`${config.apiUrl}/organizations`, requestOptions).then(handleResponse);
|
||||
}
|
||||
|
|
@ -72,3 +73,9 @@ function editOrganizationConfigs(params) {
|
|||
};
|
||||
return fetch(`${config.apiUrl}/organizations/configs`, requestOptions).then(handleResponse);
|
||||
}
|
||||
|
||||
function checkWorkspaceUniqueness(name, slug) {
|
||||
const requestOptions = { method: 'GET', headers: authHeader(), credentials: 'include' };
|
||||
const query = queryString.stringify({ name, slug });
|
||||
return fetch(`${config.apiUrl}/organizations/is-unique?${query}`, requestOptions).then(handleResponse);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2983,7 +2983,6 @@ input:focus-visible {
|
|||
position: relative;
|
||||
display: flex;
|
||||
flex: 1;
|
||||
margin-right: 10px;
|
||||
|
||||
input {
|
||||
border-radius: 0px 6px 6px 0px !important;
|
||||
|
|
@ -10970,7 +10969,7 @@ tbody {
|
|||
.input-group-text {
|
||||
border-color: var(--slate7);
|
||||
color: var(--slate12);
|
||||
background-color: var(--base);
|
||||
background-color: var(--slate3);
|
||||
}
|
||||
|
||||
.app-name-slug-input {
|
||||
|
|
@ -10982,7 +10981,6 @@ tbody {
|
|||
|
||||
.input-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.tj-app-input textarea {
|
||||
width: 600px;
|
||||
|
|
@ -11032,8 +11030,131 @@ tbody {
|
|||
color: var(--slate12) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.shareable-link-container,
|
||||
.app-slug-container {
|
||||
.field-name {
|
||||
color: var(--slate-12) !important;
|
||||
}
|
||||
input.slug-input {
|
||||
background: #1f2936 !important;
|
||||
color: #f4f6fa !important;
|
||||
border-color: #324156 !important;
|
||||
}
|
||||
.applink-text {
|
||||
background-color: #2b394b !important;
|
||||
}
|
||||
|
||||
.input-group-text {
|
||||
background-color: #2b394b !important;
|
||||
}
|
||||
|
||||
.tj-text-input {
|
||||
border-color: #324156 !important;
|
||||
}
|
||||
|
||||
.input-with-icon {
|
||||
.form-control{
|
||||
background-color: #1f2936 !important;
|
||||
border-color: #3E4B5A !important;
|
||||
color: #fff !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.dark-theme{
|
||||
.manage-app-users-footer {
|
||||
.default-secondary-button {
|
||||
background-color: var(--indigo9);
|
||||
color: var(--base-black);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.break-all {
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.workspace-folder-modal{
|
||||
.tj-text-input.dark {
|
||||
background: #202425;
|
||||
border-color: var(--slate7) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.slug-ellipsis {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
width: 150px;
|
||||
}
|
||||
|
||||
.app-slug-container,
|
||||
.shareable-link-container,
|
||||
.workspace-folder-modal {
|
||||
.tj-app-input {
|
||||
padding-bottom: 0px !important;
|
||||
}
|
||||
|
||||
.label {
|
||||
font-weight: 400;
|
||||
font-size: 10px;
|
||||
height: 0px;
|
||||
padding: 4px 0px 16px 0px;
|
||||
}
|
||||
|
||||
.tj-input-error {
|
||||
color: var(--tomato10);
|
||||
}
|
||||
|
||||
.tj-text-input {
|
||||
width: auto !important;
|
||||
background: var(--slate3);
|
||||
color: var(--slate9);
|
||||
height: auto !important;
|
||||
margin-bottom: 5px;
|
||||
border-color: var(--slate7);
|
||||
|
||||
&:hover {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
&:active {
|
||||
border: 1px solid #D7DBDF;
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
.input-with-icon {
|
||||
flex: none;
|
||||
.icon-container {
|
||||
right: 20px;
|
||||
top: calc(50% - 13px);
|
||||
}
|
||||
}
|
||||
|
||||
.label-info {
|
||||
color: #687076;
|
||||
}
|
||||
|
||||
.label-success {
|
||||
color: #3D9A50;
|
||||
}
|
||||
|
||||
|
||||
.workspace-spinner {
|
||||
color: #889096 !important;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.cancel-btn {
|
||||
color: var(--indigo9);
|
||||
}
|
||||
}
|
||||
.confirm-dialogue-modal {
|
||||
background: var(--base);
|
||||
}
|
||||
|
|
@ -11088,18 +11209,27 @@ tbody {
|
|||
}
|
||||
}
|
||||
|
||||
.app-slug-container,
|
||||
.workspace-folder-modal {
|
||||
.tj-app-input {
|
||||
padding-bottom: 0px !important;
|
||||
|
||||
.is-invalid {
|
||||
border-color: var(--tomato10) !important;
|
||||
}
|
||||
|
||||
.is-invalid:focus {
|
||||
border-color: var(--tomato10) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.tj-input-error {
|
||||
height: 32px;
|
||||
color: #ED5F00;
|
||||
color: var(--tomato10);
|
||||
font-weight: 400;
|
||||
font-size: 10px;
|
||||
height: 0px;
|
||||
padding: 4px 0px 20px 0px;
|
||||
padding: 4px 0px 16px 0px;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -11317,6 +11447,70 @@ tbody {
|
|||
}
|
||||
}
|
||||
|
||||
.app-slug-container {
|
||||
.label {
|
||||
font-size: 9px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.shareable-link-container {
|
||||
.copy-container {
|
||||
width: 0px;
|
||||
margin-right: -12px;
|
||||
}
|
||||
|
||||
.form-check-label {
|
||||
font-size: 12px;
|
||||
margin-left: 8px;
|
||||
color: var(--base-slate-12);
|
||||
}
|
||||
|
||||
.label-success,
|
||||
.label-updated,
|
||||
.tj-input-error,
|
||||
.label-info {
|
||||
font-size: 10px;
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
.input-with-icon {
|
||||
.form-control {
|
||||
height: 100%;
|
||||
border-radius: 0px !important;
|
||||
padding-right: 40px;
|
||||
}
|
||||
|
||||
.is-invalid:focus {
|
||||
border-color: var(--tomato9) !important;
|
||||
}
|
||||
|
||||
.icon-container {
|
||||
right: 12px;
|
||||
top: calc(50% - 11px);
|
||||
|
||||
.spinner-border {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.input-group-text {
|
||||
background: var(--slate3);
|
||||
color: var(--slate9);
|
||||
}
|
||||
}
|
||||
|
||||
.manage-app-users-footer {
|
||||
padding-bottom: 20px;
|
||||
margin-top: 18px;
|
||||
.default-secondary-button {
|
||||
width: auto !important;
|
||||
padding: 18px;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Editor revamp styles
|
||||
.main-wrapper {
|
||||
.editor {
|
||||
|
|
|
|||
13
frontend/src/_ui/TJLoader/TJLoader.jsx
Normal file
13
frontend/src/_ui/TJLoader/TJLoader.jsx
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import React from 'react';
|
||||
|
||||
export const TJLoader = () => {
|
||||
return (
|
||||
<div className="spin-loader">
|
||||
<div className="load">
|
||||
<div className="one"></div>
|
||||
<div className="two"></div>
|
||||
<div className="three"></div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
1
frontend/src/_ui/TJLoader/index.js
Normal file
1
frontend/src/_ui/TJLoader/index.js
Normal file
|
|
@ -0,0 +1 @@
|
|||
export * from './TJLoader';
|
||||
|
|
@ -1 +1 @@
|
|||
2.21.1
|
||||
2.22.0
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ import {
|
|||
URL_SSO_SOURCE,
|
||||
WORKSPACE_USER_STATUS,
|
||||
} from 'src/helpers/user_lifecycle';
|
||||
import { dbTransactionWrap, generateInviteURL, generateNextName } from 'src/helpers/utils.helper';
|
||||
import { dbTransactionWrap, generateInviteURL, generateNextNameAndSlug } from 'src/helpers/utils.helper';
|
||||
import { DeepPartial, EntityManager } from 'typeorm';
|
||||
import { GitOAuthService } from './git_oauth.service';
|
||||
import { GoogleOAuthService } from './google_oauth.service';
|
||||
|
|
@ -79,8 +79,8 @@ export class OauthService {
|
|||
}
|
||||
|
||||
if (!user) {
|
||||
const organizationName = generateNextName('My workspace');
|
||||
defaultOrganization = await this.organizationService.create(organizationName, null, manager);
|
||||
const { name, slug } = generateNextNameAndSlug('My workspace');
|
||||
defaultOrganization = await this.organizationService.create(name, slug, null, manager);
|
||||
}
|
||||
|
||||
const groups = ['all_users'];
|
||||
|
|
@ -221,8 +221,8 @@ export class OauthService {
|
|||
let defaultOrganization: DeepPartial<Organization> = organization;
|
||||
|
||||
// Not logging in to specific organization, creating new
|
||||
const organizationName = generateNextName('My workspace');
|
||||
defaultOrganization = await this.organizationService.create(organizationName, null, manager);
|
||||
const { name, slug } = generateNextNameAndSlug('My workspace');
|
||||
defaultOrganization = await this.organizationService.create(name, slug, null, manager);
|
||||
|
||||
const groups = ['all_users', 'admin'];
|
||||
userDetails = await this.usersService.create(
|
||||
|
|
@ -263,8 +263,8 @@ export class OauthService {
|
|||
organizationDetails = organizationList[0];
|
||||
} else {
|
||||
// no SSO login enabled organization available for user - creating new one
|
||||
const organizationName = generateNextName('My workspace');
|
||||
organizationDetails = await this.organizationService.create(organizationName, userDetails, manager);
|
||||
const { name, slug } = generateNextNameAndSlug('My workspace');
|
||||
organizationDetails = await this.organizationService.create(name, slug, userDetails, manager);
|
||||
}
|
||||
} else if (!userDetails) {
|
||||
throw new UnauthorizedException('User does not exist, please sign up');
|
||||
|
|
|
|||
|
|
@ -17,7 +17,9 @@ export class AddUniqueConstraintToFolderName1684145489093 implements MigrationIn
|
|||
}
|
||||
|
||||
public async migrateFolderNames(entityManager: EntityManager) {
|
||||
const workspaces = await entityManager.find(Organization);
|
||||
const workspaces = await entityManager.find(Organization, {
|
||||
select: ['id'],
|
||||
});
|
||||
for (const workspace of workspaces) {
|
||||
const { id: organizationId } = workspace;
|
||||
const folders = await entityManager.query(
|
||||
|
|
|
|||
28
server/migrations/1685952833787-AddSlugToWorkspace.ts
Normal file
28
server/migrations/1685952833787-AddSlugToWorkspace.ts
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
import { DataBaseConstraints } from 'src/helpers/db_constraints.constants';
|
||||
import { MigrationInterface, QueryRunner, TableColumn, TableUnique } from 'typeorm';
|
||||
|
||||
export class AddSlugToWorkspace1685952833787 implements MigrationInterface {
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.addColumn(
|
||||
'organizations',
|
||||
new TableColumn({
|
||||
name: 'slug',
|
||||
type: 'varchar',
|
||||
length: '50',
|
||||
isNullable: true,
|
||||
})
|
||||
);
|
||||
|
||||
await queryRunner.createUniqueConstraint(
|
||||
'organizations',
|
||||
new TableUnique({
|
||||
name: DataBaseConstraints.WORKSPACE_SLUG_UNIQUE,
|
||||
columnNames: ['slug'],
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.dropColumn('organizations', 'slug');
|
||||
}
|
||||
}
|
||||
|
|
@ -9,6 +9,7 @@ import {
|
|||
BadRequestException,
|
||||
Query,
|
||||
Res,
|
||||
NotFoundException,
|
||||
} from '@nestjs/common';
|
||||
import { User } from 'src/decorators/user.decorator';
|
||||
import { JwtAuthGuard } from '../../src/modules/auth/jwt-auth.guard';
|
||||
|
|
@ -30,13 +31,16 @@ import { Response } from 'express';
|
|||
import { SessionAuthGuard } from 'src/modules/auth/session-auth-guard';
|
||||
import { UsersService } from '@services/users.service';
|
||||
import { SessionService } from '@services/session.service';
|
||||
import { OrganizationsService } from '@services/organizations.service';
|
||||
import { Organization } from 'src/entities/organization.entity';
|
||||
|
||||
@Controller()
|
||||
export class AppController {
|
||||
constructor(
|
||||
private authService: AuthService,
|
||||
private userService: UsersService,
|
||||
private sessionService: SessionService
|
||||
private sessionService: SessionService,
|
||||
private organizationService: OrganizationsService
|
||||
) {}
|
||||
|
||||
@Post('authenticate')
|
||||
|
|
@ -57,17 +61,24 @@ export class AppController {
|
|||
|
||||
@UseGuards(SessionAuthGuard)
|
||||
@Get('session')
|
||||
async getSessionDetails(@User() user, @Query('appId') appId: string) {
|
||||
let appOrganizationId: string;
|
||||
async getSessionDetails(@User() user, @Query('appId') appId: string, @Query('workspaceSlug') workspaceSlug: string) {
|
||||
let currentOrganization: Organization;
|
||||
|
||||
let app: { organizationId: string; isPublic: boolean };
|
||||
if (appId) {
|
||||
const app = await this.userService.returnOrgIdOfAnApp(appId);
|
||||
//if the user has a session and the app is public, we don't need to authorize the app organization id
|
||||
if (!app?.isPublic) appOrganizationId = app.organizationId;
|
||||
if (appOrganizationId && user.organizationIds?.includes(appOrganizationId)) {
|
||||
user.organization_id = appOrganizationId;
|
||||
}
|
||||
app = await this.userService.returnOrgIdOfAnApp(appId);
|
||||
}
|
||||
return this.authService.generateSessionPayload(user, appOrganizationId);
|
||||
|
||||
/* if the user has a session and the app is public, we don't need to authorize the app organization id */
|
||||
if ((app && !app?.isPublic) || workspaceSlug) {
|
||||
const organization = await this.organizationService.fetchOrganization(workspaceSlug || app.organizationId);
|
||||
if (!organization) {
|
||||
throw new NotFoundException("Coudn't found workspace. workspace id or slug is incorrect!.");
|
||||
}
|
||||
currentOrganization = organization;
|
||||
}
|
||||
|
||||
return this.authService.generateSessionPayload(user, currentOrganization);
|
||||
}
|
||||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import {
|
|||
Body,
|
||||
BadRequestException,
|
||||
UseInterceptors,
|
||||
NotFoundException,
|
||||
} from '@nestjs/common';
|
||||
import { JwtAuthGuard } from '../../src/modules/auth/jwt-auth.guard';
|
||||
import { AppsService } from '../services/apps.service';
|
||||
|
|
@ -63,9 +64,15 @@ export class AppsController {
|
|||
}
|
||||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@UseInterceptors(ValidAppInterceptor)
|
||||
@Get(':id')
|
||||
async show(@User() user, @AppDecorator() app: App, @Query('access_type') accessType: string) {
|
||||
@Get('validate-private-app-access/:slug')
|
||||
async appAccess(
|
||||
@User() user,
|
||||
@Param('slug') appSlug: string,
|
||||
@Query('access_type') accessType: string,
|
||||
@Query('version_name') versionName: string
|
||||
) {
|
||||
const app: App = await this.appsService.findAppWithIdOrSlug(appSlug);
|
||||
|
||||
const ability = await this.appsAbilityFactory.appsActions(user, app.id);
|
||||
if (!ability.can('viewApp', app)) {
|
||||
throw new ForbiddenException(
|
||||
|
|
@ -83,6 +90,43 @@ export class AppsController {
|
|||
);
|
||||
}
|
||||
|
||||
const { id, slug } = app;
|
||||
const response = {
|
||||
id,
|
||||
slug,
|
||||
};
|
||||
/* If the request comes from preview which needs version id */
|
||||
if (versionName) {
|
||||
if (!ability.can('fetchVersions', app)) {
|
||||
throw new ForbiddenException(
|
||||
JSON.stringify({
|
||||
organizationId: app.organizationId,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
const version = await this.appsService.findVersionFromName(versionName, id);
|
||||
if (!version) {
|
||||
throw new NotFoundException("Couldn't found app version. Please check the version name");
|
||||
}
|
||||
response['versionId'] = version.id;
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@UseInterceptors(ValidAppInterceptor)
|
||||
@Get(':id')
|
||||
async show(@User() user, @AppDecorator() app: App) {
|
||||
const ability = await this.appsAbilityFactory.appsActions(user, app.id);
|
||||
if (!ability.can('viewApp', app)) {
|
||||
throw new ForbiddenException(
|
||||
JSON.stringify({
|
||||
organizationId: app.organizationId,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
const response = decamelizeKeys(app);
|
||||
const seralizedQueries = [];
|
||||
const dataQueriesForVersion = app.editingVersion
|
||||
|
|
@ -109,6 +153,28 @@ export class AppsController {
|
|||
return response;
|
||||
}
|
||||
|
||||
@UseGuards(AppAuthGuard) // This guard will allow access for unauthenticated user if the app is public
|
||||
@Get('validate-released-app-access/:slug')
|
||||
async releasedAppAccess(@User() user, @AppDecorator() app: App) {
|
||||
if (user) {
|
||||
const ability = await this.appsAbilityFactory.appsActions(user, app.id);
|
||||
|
||||
if (!ability.can('viewApp', app)) {
|
||||
throw new ForbiddenException(
|
||||
JSON.stringify({
|
||||
organizationId: app.organizationId,
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const { id, slug } = app;
|
||||
return {
|
||||
slug: slug,
|
||||
id: id,
|
||||
};
|
||||
}
|
||||
|
||||
@UseGuards(AppAuthGuard) // This guard will allow access for unauthenticated user if the app is public
|
||||
@Get('slugs/:slug')
|
||||
async appFromSlug(@User() user, @AppDecorator() app: App) {
|
||||
|
|
|
|||
|
|
@ -67,7 +67,7 @@ export class OrganizationsController {
|
|||
@Body() organizationCreateDto: OrganizationCreateDto,
|
||||
@Res({ passthrough: true }) response: Response
|
||||
) {
|
||||
const result = await this.organizationsService.create(organizationCreateDto.name, user);
|
||||
const result = await this.organizationsService.create(organizationCreateDto.name, organizationCreateDto.slug, user);
|
||||
|
||||
if (!result) {
|
||||
throw new Error();
|
||||
|
|
@ -87,6 +87,8 @@ export class OrganizationsController {
|
|||
}
|
||||
|
||||
const result = await this.organizationsService.fetchOrganizationDetails(organizationId, [true], true, true);
|
||||
if (!result) throw new NotFoundException();
|
||||
|
||||
return decamelizeKeys({ ssoConfigs: result });
|
||||
}
|
||||
|
||||
|
|
@ -116,4 +118,10 @@ export class OrganizationsController {
|
|||
const result: any = await this.organizationsService.updateOrganizationConfigs(user.organizationId, body);
|
||||
return decamelizeKeys({ id: result.id });
|
||||
}
|
||||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Get('/is-unique')
|
||||
async checkWorkspaceUnique(@User() user, @Query('name') name: string, @Query('slug') slug: string) {
|
||||
return this.organizationsService.checkWorkspaceUniqueness(name, slug);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ export class AppUpdateDto {
|
|||
const newValue = sanitizeInput(value);
|
||||
return newValue.trim();
|
||||
})
|
||||
@IsNotEmpty()
|
||||
@IsNotEmpty({ message: 'App name should not be empty' })
|
||||
@MaxLength(50, { message: 'Maximum length has been reached.' })
|
||||
name: string;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,41 @@
|
|||
import { Transform } from 'class-transformer';
|
||||
import { IsBoolean, IsNotEmpty, IsOptional, IsString, MaxLength } from 'class-validator';
|
||||
|
||||
import {
|
||||
IsBoolean,
|
||||
IsNotEmpty,
|
||||
IsOptional,
|
||||
IsString,
|
||||
MaxLength,
|
||||
Validate,
|
||||
ValidatorConstraint,
|
||||
ValidatorConstraintInterface,
|
||||
} from 'class-validator';
|
||||
import { sanitizeInput } from '../helpers/utils.helper';
|
||||
|
||||
@ValidatorConstraint({ name: 'AllowedCharactersValidator', async: false })
|
||||
export class AllowedCharactersValidator implements ValidatorConstraintInterface {
|
||||
private errorMsg: string;
|
||||
|
||||
validate(value: string) {
|
||||
if (value.match(/^[a-z0-9 -]+$/) === null) {
|
||||
if (/[A-Z]/.test(value)) {
|
||||
this.errorMsg = 'Only lowercase letters are accepted.';
|
||||
} else {
|
||||
this.errorMsg = 'Special characters are not accepted.';
|
||||
}
|
||||
return false;
|
||||
}
|
||||
if (/\s/g.test(value)) {
|
||||
this.errorMsg = 'Cannot contain spaces.';
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
defaultMessage() {
|
||||
return this.errorMsg;
|
||||
}
|
||||
}
|
||||
|
||||
export class OrganizationCreateDto {
|
||||
@IsString()
|
||||
@Transform(({ value }) => {
|
||||
|
|
@ -14,6 +47,18 @@ export class OrganizationCreateDto {
|
|||
})
|
||||
@MaxLength(50, { message: 'Maximum length has been reached.' })
|
||||
name: string;
|
||||
|
||||
@IsString()
|
||||
@Transform(({ value }) => {
|
||||
const newValue = sanitizeInput(value);
|
||||
return newValue?.trim() || '';
|
||||
})
|
||||
@IsNotEmpty({
|
||||
message: "Workspace slug can't be empty",
|
||||
})
|
||||
@MaxLength(50, { message: 'Maximum length has been reached.' })
|
||||
@Validate(AllowedCharactersValidator)
|
||||
slug: string;
|
||||
}
|
||||
|
||||
export class OrganizationUpdateDto {
|
||||
|
|
@ -26,6 +71,16 @@ export class OrganizationUpdateDto {
|
|||
@MaxLength(50, { message: 'Maximum length has been reached.' })
|
||||
name?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@Transform(({ value }) => {
|
||||
const newValue = sanitizeInput(value);
|
||||
return newValue?.trim() || '';
|
||||
})
|
||||
@MaxLength(50, { message: 'Maximum length has been reached.' })
|
||||
@Validate(AllowedCharactersValidator)
|
||||
slug: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@Transform(({ value }) => sanitizeInput(value))
|
||||
|
|
|
|||
|
|
@ -22,6 +22,9 @@ export class Organization extends BaseEntity {
|
|||
@Column({ name: 'name', unique: true })
|
||||
name: string;
|
||||
|
||||
@Column({ name: 'slug', unique: true })
|
||||
slug: string;
|
||||
|
||||
@Column({ name: 'domain' })
|
||||
domain: string;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
export enum DataBaseConstraints {
|
||||
FOLDER_NAME_UNIQUE = 'folder_name_organization_id_unique',
|
||||
APP_NAME_UNIQUE = 'app_name_organization_id_unique',
|
||||
APP_SLUG_UNIQUE = 'UQ_35eef0fb1f3f2b435b8b6d82ba0',
|
||||
WORKSPACE_NAME_UNIQUE = 'name_organizations_unique',
|
||||
WORKSPACE_SLUG_UNIQUE = 'slug_organizations_unique',
|
||||
USER_ORGANIZATION_UNIQUE = 'user_organization_unique',
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,9 +2,9 @@ import { QueryError } from 'src/modules/data_sources/query.errors';
|
|||
import * as sanitizeHtml from 'sanitize-html';
|
||||
import { EntityManager, getManager } from 'typeorm';
|
||||
import { isEmpty } from 'lodash';
|
||||
const protobuf = require('protobufjs');
|
||||
import { ConflictException } from '@nestjs/common';
|
||||
import { DataBaseConstraints } from './db_constraints.constants';
|
||||
const protobuf = require('protobufjs');
|
||||
|
||||
export function maybeSetSubPath(path) {
|
||||
const hasSubPath = process.env.SUB_PATH !== undefined;
|
||||
|
|
@ -80,22 +80,27 @@ export async function dbTransactionWrap(operation: (...args) => any, manager?: E
|
|||
}
|
||||
}
|
||||
|
||||
export const defaultAppEnvironments = [{ name: 'production', isDefault: true, priority: 3 }];
|
||||
export async function catchDbException(
|
||||
operation: () => any,
|
||||
dbConstraint: DataBaseConstraints,
|
||||
errorMessage: string
|
||||
): Promise<any> {
|
||||
type DbContraintAndMsg = {
|
||||
dbConstraint: DataBaseConstraints;
|
||||
message: string;
|
||||
};
|
||||
|
||||
export async function catchDbException(operation: () => any, dbConstraints: DbContraintAndMsg[]): Promise<any> {
|
||||
try {
|
||||
return await operation();
|
||||
} catch (err) {
|
||||
if (err?.message?.includes(dbConstraint)) {
|
||||
throw new ConflictException(errorMessage);
|
||||
}
|
||||
dbConstraints.map((dbConstraint) => {
|
||||
if (err?.message?.includes(dbConstraint.dbConstraint)) {
|
||||
throw new ConflictException(dbConstraint.message);
|
||||
}
|
||||
});
|
||||
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
export const defaultAppEnvironments = [{ name: 'production', isDefault: true, priority: 3 }];
|
||||
|
||||
export function isPlural(data: Array<any>) {
|
||||
return data?.length > 1 ? 's' : '';
|
||||
}
|
||||
|
|
@ -153,8 +158,13 @@ export const processDataInBatches = async <T>(
|
|||
} while (data.length === batchSize);
|
||||
};
|
||||
|
||||
export const generateNextName = (firstWord: string) => {
|
||||
return `${firstWord} ${Date.now()}`;
|
||||
export const generateNextNameAndSlug = (firstWord: string) => {
|
||||
const name = `${firstWord} ${Date.now()}`;
|
||||
const slug = name.replace(/\s+/g, '-').toLowerCase();
|
||||
return {
|
||||
name,
|
||||
slug,
|
||||
};
|
||||
};
|
||||
|
||||
export const truncateAndReplace = (name) => {
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ export class ValidAppInterceptor implements NestInterceptor {
|
|||
if (!(id || slug)) {
|
||||
throw new BadRequestException();
|
||||
}
|
||||
const app = request.tj_app || (id ? await this.appsService.find(id) : this.appsService.findBySlug(slug));
|
||||
const app = request.tj_app || (id ? await this.appsService.find(id) : await this.appsService.findBySlug(slug));
|
||||
if (!app) throw new NotFoundException('App not found. Invalid app id');
|
||||
request.tj_app = app;
|
||||
return next.handle();
|
||||
|
|
|
|||
|
|
@ -83,5 +83,6 @@ import { SessionService } from '@services/session.service';
|
|||
SessionService,
|
||||
],
|
||||
controllers: [OrganizationsController, OrganizationUsersController],
|
||||
exports: [OrganizationsService],
|
||||
})
|
||||
export class OrganizationsModule {}
|
||||
|
|
|
|||
|
|
@ -196,24 +196,20 @@ export class AppImportExportService {
|
|||
}
|
||||
|
||||
async createImportedAppForUser(manager: EntityManager, appParams: any, user: User): Promise<App> {
|
||||
return await catchDbException(
|
||||
async () => {
|
||||
const importedApp = manager.create(App, {
|
||||
name: appParams.name,
|
||||
organizationId: user.organizationId,
|
||||
userId: user.id,
|
||||
slug: null,
|
||||
icon: appParams.icon,
|
||||
isPublic: false,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
});
|
||||
await manager.save(importedApp);
|
||||
return importedApp;
|
||||
},
|
||||
DataBaseConstraints.APP_NAME_UNIQUE,
|
||||
'This app name is already taken.'
|
||||
);
|
||||
return await catchDbException(async () => {
|
||||
const importedApp = manager.create(App, {
|
||||
name: appParams.name,
|
||||
organizationId: user.organizationId,
|
||||
userId: user.id,
|
||||
slug: null,
|
||||
icon: appParams.icon,
|
||||
isPublic: false,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
});
|
||||
await manager.save(importedApp);
|
||||
return importedApp;
|
||||
}, [{ dbConstraint: DataBaseConstraints.APP_NAME_UNIQUE, message: 'This app name is already taken.' }]);
|
||||
}
|
||||
|
||||
extractImportDataFromAppParams(appParams: Record<string, any>): {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { BadRequestException, Injectable, NotAcceptableException } from '@nestjs/common';
|
||||
import { BadRequestException, Injectable, NotAcceptableException, NotFoundException } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { App } from 'src/entities/app.entity';
|
||||
import { EntityManager, MoreThan, Repository } from 'typeorm';
|
||||
|
|
@ -86,6 +86,12 @@ export class AppsService {
|
|||
).app;
|
||||
}
|
||||
|
||||
async findVersionFromName(name: string, appId: string): Promise<AppVersion> {
|
||||
return await this.appVersionsRepository.findOne({
|
||||
where: { name, appId },
|
||||
});
|
||||
}
|
||||
|
||||
async findDataQueriesForVersion(appVersionId: string): Promise<DataQuery[]> {
|
||||
return await dbTransactionWrap(async (manager: EntityManager) => {
|
||||
return manager
|
||||
|
|
@ -99,37 +105,33 @@ export class AppsService {
|
|||
|
||||
async create(name: string, user: User, manager: EntityManager): Promise<App> {
|
||||
return await dbTransactionWrap(async (manager: EntityManager) => {
|
||||
return await catchDbException(
|
||||
async () => {
|
||||
const app = await manager.save(
|
||||
manager.create(App, {
|
||||
name,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
organizationId: user.organizationId,
|
||||
userId: user.id,
|
||||
})
|
||||
);
|
||||
return await catchDbException(async () => {
|
||||
const app = await manager.save(
|
||||
manager.create(App, {
|
||||
name,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
organizationId: user.organizationId,
|
||||
userId: user.id,
|
||||
})
|
||||
);
|
||||
|
||||
//create default app version
|
||||
await this.createVersion(user, app, 'v1', null, null, manager);
|
||||
//create default app version
|
||||
await this.createVersion(user, app, 'v1', null, null, manager);
|
||||
|
||||
await manager.save(
|
||||
manager.create(AppUser, {
|
||||
userId: user.id,
|
||||
appId: app.id,
|
||||
role: 'admin',
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
);
|
||||
await manager.save(
|
||||
manager.create(AppUser, {
|
||||
userId: user.id,
|
||||
appId: app.id,
|
||||
role: 'admin',
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
);
|
||||
|
||||
await this.createAppGroupPermissionsForAdmin(app, manager);
|
||||
return app;
|
||||
},
|
||||
DataBaseConstraints.APP_NAME_UNIQUE,
|
||||
'This app name is already taken.'
|
||||
);
|
||||
await this.createAppGroupPermissionsForAdmin(app, manager);
|
||||
return app;
|
||||
}, [{ dbConstraint: DataBaseConstraints.APP_NAME_UNIQUE, message: 'This app name is already taken.' }]);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -236,13 +238,12 @@ export class AppsService {
|
|||
throw new BadRequestException('You can only release when the version is promoted to production');
|
||||
}
|
||||
}
|
||||
return await catchDbException(
|
||||
async () => {
|
||||
return await manager.update(App, appId, updatableParams);
|
||||
},
|
||||
DataBaseConstraints.APP_NAME_UNIQUE,
|
||||
'This app name is already taken.'
|
||||
);
|
||||
return await catchDbException(async () => {
|
||||
return await manager.update(App, appId, updatableParams);
|
||||
}, [
|
||||
{ dbConstraint: DataBaseConstraints.APP_NAME_UNIQUE, message: 'This app name is already taken.' },
|
||||
{ dbConstraint: DataBaseConstraints.APP_SLUG_UNIQUE, message: 'This app slug is already taken.' },
|
||||
]);
|
||||
}, manager);
|
||||
}
|
||||
|
||||
|
|
@ -654,6 +655,22 @@ export class AppsService {
|
|||
});
|
||||
}
|
||||
|
||||
async findAppWithIdOrSlug(slug: string): Promise<App> {
|
||||
let app: App;
|
||||
try {
|
||||
app = await this.find(slug);
|
||||
} catch (error) {
|
||||
/* means: UUID error. so the slug isn't not the id of the app */
|
||||
if (error?.code === `22P02`) {
|
||||
/* Search against slug */
|
||||
app = await this.findBySlug(slug);
|
||||
}
|
||||
}
|
||||
|
||||
if (!app) throw new NotFoundException('App not found. Invalid app id');
|
||||
return app;
|
||||
}
|
||||
|
||||
async findTooljetDbTables(appId: string): Promise<{ table_id: string }[]> {
|
||||
return await dbTransactionWrap(async (manager: EntityManager) => {
|
||||
const tooljetDbDataQueries = await manager
|
||||
|
|
|
|||
|
|
@ -21,7 +21,12 @@ import { DeepPartial, EntityManager, Repository } from 'typeorm';
|
|||
import { OrganizationUser } from 'src/entities/organization_user.entity';
|
||||
import { CreateAdminDto, CreateUserDto } from '@dto/user.dto';
|
||||
import { AcceptInviteDto } from '@dto/accept-organization-invite.dto';
|
||||
import { dbTransactionWrap, generateInviteURL, generateNextName, generateOrgInviteURL } from 'src/helpers/utils.helper';
|
||||
import {
|
||||
dbTransactionWrap,
|
||||
generateInviteURL,
|
||||
generateNextNameAndSlug,
|
||||
generateOrgInviteURL,
|
||||
} from 'src/helpers/utils.helper';
|
||||
import {
|
||||
getUserErrorMessages,
|
||||
getUserStatusAndSource,
|
||||
|
|
@ -121,7 +126,8 @@ export class AuthService {
|
|||
organization = organizationList[0];
|
||||
} else {
|
||||
// no form login enabled organization available for user - creating new one
|
||||
organization = await this.organizationsService.create(generateNextName('My workspace'), user, manager);
|
||||
const { name, slug } = generateNextNameAndSlug('My workspace');
|
||||
organization = await this.organizationsService.create(name, slug, user, manager);
|
||||
}
|
||||
|
||||
user.organizationId = organization.id;
|
||||
|
|
@ -192,7 +198,11 @@ export class AuthService {
|
|||
if (user.defaultOrganizationId !== user.organizationId)
|
||||
await this.usersService.updateUser(user.id, { defaultOrganizationId: user.organizationId }, manager);
|
||||
|
||||
const organization = await this.organizationsService.get(user.organizationId);
|
||||
|
||||
return decamelizeKeys({
|
||||
currentOrganizationId: user.organizationId,
|
||||
currentOrganizationSlug: organization.slug,
|
||||
admin: await this.usersService.hasGroup(user, 'admin', null, manager),
|
||||
groupPermissions: await this.usersService.groupPermissions(user, manager),
|
||||
appGroupPermissions: await this.usersService.appGroupPermissions(user, null, manager),
|
||||
|
|
@ -259,8 +269,8 @@ export class AuthService {
|
|||
// Create default organization
|
||||
//TODO: check if there any case available that the firstname will be nil
|
||||
|
||||
const organizationName = generateNextName('My workspace');
|
||||
organization = await this.organizationsService.create(organizationName, null, manager);
|
||||
const { name, slug } = generateNextNameAndSlug('My workspace');
|
||||
organization = await this.organizationsService.create(name, slug, null, manager);
|
||||
const user = await this.usersService.create(
|
||||
{
|
||||
email,
|
||||
|
|
@ -326,7 +336,12 @@ export class AuthService {
|
|||
|
||||
const result = await dbTransactionWrap(async (manager: EntityManager) => {
|
||||
// Create first organization
|
||||
const organization = await this.organizationsService.create(workspace || 'My workspace', null, manager);
|
||||
const organization = await this.organizationsService.create(
|
||||
workspace || 'My workspace',
|
||||
'my-workspace',
|
||||
null,
|
||||
manager
|
||||
);
|
||||
const user = await this.usersService.create(
|
||||
{
|
||||
email,
|
||||
|
|
@ -540,14 +555,15 @@ export class AuthService {
|
|||
};
|
||||
}
|
||||
|
||||
generateSessionPayload(user: User, appOrganizationId: string) {
|
||||
generateSessionPayload(user: User, currentOrganization: Organization) {
|
||||
return decamelizeKeys({
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
firstName: user.firstName,
|
||||
lastName: user.lastName,
|
||||
currentOrganizationId: appOrganizationId
|
||||
? appOrganizationId
|
||||
currentOrganizationSlug: currentOrganization?.slug,
|
||||
currentOrganizationId: currentOrganization?.id
|
||||
? currentOrganization?.id
|
||||
: user?.organizationIds?.includes(user?.defaultOrganizationId)
|
||||
? user.defaultOrganizationId
|
||||
: user?.organizationIds?.[0],
|
||||
|
|
@ -612,6 +628,7 @@ export class AuthService {
|
|||
firstName: user.firstName,
|
||||
lastName: user.lastName,
|
||||
currentOrganizationId: organization.id,
|
||||
currentOrganizationSlug: organization.slug,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,30 +24,22 @@ export class FoldersService {
|
|||
) {}
|
||||
|
||||
async create(user: User, folderName): Promise<Folder> {
|
||||
return await catchDbException(
|
||||
async () => {
|
||||
return await this.foldersRepository.save(
|
||||
this.foldersRepository.create({
|
||||
name: folderName,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
organizationId: user.organizationId,
|
||||
})
|
||||
);
|
||||
},
|
||||
DataBaseConstraints.FOLDER_NAME_UNIQUE,
|
||||
'This folder name is already taken.'
|
||||
);
|
||||
return await catchDbException(async () => {
|
||||
return await this.foldersRepository.save(
|
||||
this.foldersRepository.create({
|
||||
name: folderName,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
organizationId: user.organizationId,
|
||||
})
|
||||
);
|
||||
}, [{ dbConstraint: DataBaseConstraints.FOLDER_NAME_UNIQUE, message: 'This folder name is already taken.' }]);
|
||||
}
|
||||
|
||||
async update(folderId: string, folderName: string): Promise<UpdateResult> {
|
||||
return await catchDbException(
|
||||
async () => {
|
||||
return await this.foldersRepository.update({ id: folderId }, { name: folderName });
|
||||
},
|
||||
DataBaseConstraints.FOLDER_NAME_UNIQUE,
|
||||
'This folder name is already taken.'
|
||||
);
|
||||
return await catchDbException(async () => {
|
||||
return await this.foldersRepository.update({ id: folderId }, { name: folderName });
|
||||
}, [{ dbConstraint: DataBaseConstraints.FOLDER_NAME_UNIQUE, message: 'This folder name is already taken.' }]);
|
||||
}
|
||||
|
||||
async allFolders(user: User, searchKey?: string): Promise<Folder[]> {
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ export class LibraryAppCreationService {
|
|||
importDto.tooljet_database = templateDefinition.tooljet_database;
|
||||
|
||||
if (this.isVersionGreaterThanOrEqual(templateDefinition.tooljet_version, '2.16.0')) {
|
||||
importDto.app[0].appName = appName;
|
||||
return await this.importExportResourcesService.import(currentUser, importDto);
|
||||
} else {
|
||||
const importedApp = await this.appImportExportService.import(currentUser, templateDefinition, appName);
|
||||
|
|
|
|||
|
|
@ -1,11 +1,17 @@
|
|||
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||
import { BadRequestException, ConflictException, Injectable, NotAcceptableException } from '@nestjs/common';
|
||||
import * as csv from 'fast-csv';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { GroupPermission } from 'src/entities/group_permission.entity';
|
||||
import { Organization } from 'src/entities/organization.entity';
|
||||
import { SSOConfigs } from 'src/entities/sso_config.entity';
|
||||
import { User } from 'src/entities/user.entity';
|
||||
import { catchDbException, cleanObject, dbTransactionWrap, isPlural, generateNextName } from 'src/helpers/utils.helper';
|
||||
import {
|
||||
catchDbException,
|
||||
cleanObject,
|
||||
dbTransactionWrap,
|
||||
isPlural,
|
||||
generateNextNameAndSlug,
|
||||
} from 'src/helpers/utils.helper';
|
||||
import { Brackets, createQueryBuilder, DeepPartial, EntityManager, getManager, Repository } from 'typeorm';
|
||||
import { OrganizationUser } from '../entities/organization_user.entity';
|
||||
import { EmailService } from './email.service';
|
||||
|
|
@ -26,6 +32,7 @@ import { decamelize } from 'humps';
|
|||
import { Response } from 'express';
|
||||
import { AppEnvironmentService } from './app_environments.service';
|
||||
import { DataBaseConstraints } from 'src/helpers/db_constraints.constants';
|
||||
import { OrganizationUpdateDto } from '@dto/organization.dto';
|
||||
|
||||
const MAX_ROW_COUNT = 500;
|
||||
|
||||
|
|
@ -48,6 +55,18 @@ interface UserCsvRow {
|
|||
email: string;
|
||||
groups?: any;
|
||||
}
|
||||
|
||||
const orgConstraints = [
|
||||
{
|
||||
dbConstraint: DataBaseConstraints.WORKSPACE_NAME_UNIQUE,
|
||||
message: 'This workspace name is already taken.',
|
||||
},
|
||||
{
|
||||
dbConstraint: DataBaseConstraints.WORKSPACE_SLUG_UNIQUE,
|
||||
message: 'This workspace slug is already taken.',
|
||||
},
|
||||
];
|
||||
|
||||
@Injectable()
|
||||
export class OrganizationsService {
|
||||
constructor(
|
||||
|
|
@ -64,28 +83,25 @@ export class OrganizationsService {
|
|||
private configService: ConfigService
|
||||
) {}
|
||||
|
||||
async create(name: string, user?: User, manager?: EntityManager): Promise<Organization> {
|
||||
async create(name: string, slug: string, user: User, manager?: EntityManager): Promise<Organization> {
|
||||
let organization: Organization;
|
||||
await dbTransactionWrap(async (manager: EntityManager) => {
|
||||
organization = await catchDbException(
|
||||
async () => {
|
||||
return await manager.save(
|
||||
manager.create(Organization, {
|
||||
ssoConfigs: [
|
||||
{
|
||||
sso: 'form',
|
||||
enabled: true,
|
||||
},
|
||||
],
|
||||
name,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
);
|
||||
},
|
||||
DataBaseConstraints.WORKSPACE_NAME_UNIQUE,
|
||||
'This workspace name is already taken.'
|
||||
);
|
||||
organization = await catchDbException(async () => {
|
||||
return await manager.save(
|
||||
manager.create(Organization, {
|
||||
ssoConfigs: [
|
||||
{
|
||||
sso: 'form',
|
||||
enabled: true,
|
||||
},
|
||||
],
|
||||
name,
|
||||
slug,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
);
|
||||
}, orgConstraints);
|
||||
|
||||
await this.appEnvironmentService.createDefaultEnvironments(organization.id, manager);
|
||||
|
||||
|
|
@ -133,6 +149,16 @@ export class OrganizationsService {
|
|||
return await this.organizationsRepository.findOne({ where: { id }, relations: ['ssoConfigs'] });
|
||||
}
|
||||
|
||||
async fetchOrganization(slug: string): Promise<Organization> {
|
||||
let organization: Organization;
|
||||
try {
|
||||
organization = await this.organizationsRepository.findOneOrFail({ where: { slug }, select: ['id', 'slug'] });
|
||||
} catch (error) {
|
||||
organization = await this.organizationsRepository.findOne({ where: { id: slug }, select: ['id', 'slug'] });
|
||||
}
|
||||
return organization;
|
||||
}
|
||||
|
||||
async getSingleOrganization(): Promise<Organization> {
|
||||
return await this.organizationsRepository.findOne({ relations: ['ssoConfigs'] });
|
||||
}
|
||||
|
|
@ -332,25 +358,35 @@ export class OrganizationsService {
|
|||
.getOne();
|
||||
}
|
||||
|
||||
constructOrgFindQuery(slug: string, id: string, statusList?: Array<boolean>) {
|
||||
const query = createQueryBuilder(Organization, 'organization').leftJoinAndSelect(
|
||||
'organization.ssoConfigs',
|
||||
'organisation_sso',
|
||||
'organisation_sso.enabled IN (:...statusList)',
|
||||
{
|
||||
statusList: statusList || [true, false], // Return enabled and disabled sso if status list not passed
|
||||
}
|
||||
);
|
||||
if (slug) {
|
||||
query.andWhere(`organization.slug = :slug`, { slug });
|
||||
} else {
|
||||
query.andWhere(`organization.id = :id`, { id });
|
||||
}
|
||||
return query;
|
||||
}
|
||||
|
||||
async fetchOrganizationDetails(
|
||||
organizationId: string,
|
||||
statusList?: Array<boolean>,
|
||||
isHideSensitiveData?: boolean,
|
||||
addInstanceLevelSSO?: boolean
|
||||
): Promise<DeepPartial<Organization>> {
|
||||
const result: DeepPartial<Organization> = await createQueryBuilder(Organization, 'organization')
|
||||
.leftJoinAndSelect(
|
||||
'organization.ssoConfigs',
|
||||
'organisation_sso',
|
||||
'organisation_sso.enabled IN (:...statusList)',
|
||||
{
|
||||
statusList: statusList || [true, false], // Return enabled and disabled sso if status list not passed
|
||||
}
|
||||
)
|
||||
.andWhere('organization.id = :organizationId', {
|
||||
organizationId,
|
||||
})
|
||||
.getOne();
|
||||
let result: DeepPartial<Organization>;
|
||||
try {
|
||||
result = await this.constructOrgFindQuery(organizationId, null, statusList).getOneOrFail();
|
||||
} catch (error) {
|
||||
result = await this.constructOrgFindQuery(null, organizationId, statusList).getOne();
|
||||
}
|
||||
|
||||
if (!result) return;
|
||||
|
||||
|
|
@ -402,11 +438,16 @@ export class OrganizationsService {
|
|||
}
|
||||
return result;
|
||||
}
|
||||
return this.hideSSOSensitiveData(result?.ssoConfigs, result?.name, result?.enableSignUp);
|
||||
return this.hideSSOSensitiveData(result?.ssoConfigs, result?.name, result?.enableSignUp, result.id);
|
||||
}
|
||||
|
||||
private hideSSOSensitiveData(ssoConfigs: DeepPartial<SSOConfigs>[], organizationName, enableSignUp): any {
|
||||
const configs = { name: organizationName, enableSignUp };
|
||||
private hideSSOSensitiveData(
|
||||
ssoConfigs: DeepPartial<SSOConfigs>[],
|
||||
organizationName: string,
|
||||
enableSignUp: boolean,
|
||||
organizationId: string
|
||||
): any {
|
||||
const configs = { name: organizationName, enableSignUp, id: organizationId };
|
||||
if (ssoConfigs?.length > 0) {
|
||||
for (const config of ssoConfigs) {
|
||||
const configId = config['id'];
|
||||
|
|
@ -459,11 +500,12 @@ export class OrganizationsService {
|
|||
);
|
||||
}
|
||||
|
||||
async updateOrganization(organizationId: string, params) {
|
||||
const { name, domain, enableSignUp, inheritSSO } = params;
|
||||
async updateOrganization(organizationId: string, params: OrganizationUpdateDto) {
|
||||
const { name, slug, domain, enableSignUp, inheritSSO } = params;
|
||||
|
||||
const updatableParams = {
|
||||
name,
|
||||
slug,
|
||||
domain,
|
||||
enableSignUp,
|
||||
inheritSSO,
|
||||
|
|
@ -472,13 +514,9 @@ export class OrganizationsService {
|
|||
// removing keys with undefined values
|
||||
cleanObject(updatableParams);
|
||||
|
||||
return await catchDbException(
|
||||
async () => {
|
||||
return await this.organizationsRepository.update(organizationId, updatableParams);
|
||||
},
|
||||
DataBaseConstraints.WORKSPACE_NAME_UNIQUE,
|
||||
'This workspace name is already taken.'
|
||||
);
|
||||
return await catchDbException(async () => {
|
||||
return await this.organizationsRepository.update(organizationId, updatableParams);
|
||||
}, orgConstraints);
|
||||
}
|
||||
|
||||
async updateOrganizationConfigs(organizationId: string, params: any) {
|
||||
|
|
@ -561,8 +599,8 @@ export class OrganizationsService {
|
|||
// User not exist
|
||||
shouldSendWelcomeMail = true;
|
||||
// Create default organization if user not exist
|
||||
const organizationName = generateNextName('My workspace');
|
||||
defaultOrganization = await this.create(organizationName, null, manager);
|
||||
const { name, slug } = generateNextNameAndSlug('My workspace');
|
||||
defaultOrganization = await this.create(name, slug, null, manager);
|
||||
} else if (user.invitationToken) {
|
||||
// User not setup
|
||||
shouldSendWelcomeMail = true;
|
||||
|
|
@ -772,4 +810,16 @@ export class OrganizationsService {
|
|||
throw error.message;
|
||||
});
|
||||
}
|
||||
|
||||
async checkWorkspaceUniqueness(name: string, slug: string) {
|
||||
if (!(slug || name)) {
|
||||
throw new NotAcceptableException('Request should contain the slug or name');
|
||||
}
|
||||
const result = await getManager().findOne(Organization, {
|
||||
...(name && { name }),
|
||||
...(slug && { slug }),
|
||||
});
|
||||
if (result) throw new ConflictException(`${name ? 'Name' : 'Slug'} must be unique`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ export class SeedsService {
|
|||
},
|
||||
],
|
||||
name: 'My workspace',
|
||||
slug: 'my-workspace',
|
||||
});
|
||||
|
||||
await manager.save(organization);
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue