From 2799f2dffcd5edfe953e2009d8f11ae57bd45e91 Mon Sep 17 00:00:00 2001 From: gsmithun4 Date: Tue, 17 Oct 2023 23:57:21 +0530 Subject: [PATCH 01/14] bump version --- .version | 2 +- frontend/.version | 2 +- server/.version | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.version b/.version index db65e2167e..f48f82fa2c 100644 --- a/.version +++ b/.version @@ -1 +1 @@ -2.21.0 +2.22.0 diff --git a/frontend/.version b/frontend/.version index db65e2167e..f48f82fa2c 100644 --- a/frontend/.version +++ b/frontend/.version @@ -1 +1 @@ -2.21.0 +2.22.0 diff --git a/server/.version b/server/.version index db65e2167e..f48f82fa2c 100644 --- a/server/.version +++ b/server/.version @@ -1 +1 @@ -2.21.0 +2.22.0 From 3ea8d6e3ca5bd30ea661639917fb6007e2d46ed9 Mon Sep 17 00:00:00 2001 From: Muhsin Shah C P Date: Wed, 18 Oct 2023 13:00:17 +0530 Subject: [PATCH 02/14] [feature] Make workspace urls more readable (#6698) * working on replacing workspace id with workspace name * experimenting with zustand * added slug column to workspace table * working on workspace url * working on backward compatibility * fixing bugs * added not found error * fixed workspace switching issue * fix: switching b/w workspaces * fix: workspace login * changed workspace login url link * resolved conflicts * added create organization design * added backend validation * fixed constraint errors issue * fixed: creating new workspace * fixed: update workspace * fixed: update workspace bugs * fixed: auto created slug bugs * fixed: login slug * design changes * added folder slug * fixed: lint error * fixed: invite and first user issues * fixed: login page redirection * fixed: redirect path * added reserved word check * fix: edit workspace design * fix: folder query issue * fix: create and edit workspace modal validation issues * fixing failed test cases * fixing failed test cases - app.e2e * fixed organizations specs * fixed public app issue * working on app slug * Added app slug design to the general settings * Working on appId -> slug changes - Handling appId cases - Fixing issues * Worked on share modal design change - replaced slug functionality - Fixed backend slug issues - Fixed page handle issues * changed switch label * replace version param with query param * fix: possible uuid bug * fix: login app slug redirection issue * Worked on unique app slug * moved all apps related api calls to apps.service.js file * refactoring the code and fixing minor issues * Refactored and fixed some bugs * Fixed bugs: redirect cookie issue * Fixed dark-mode issues * removed duplicate code * Worked on develop branch conflicts * Moved handle app access check to private route * Added fix for navigate * Refactored the code. Added slug to failed sso login redirection path * again some redirect url fixes * onbaord navbar tj-icon onclick * Fix: viewer version id undefined issue * fix: multi-pages, invalid workspace slug * fix: removing the version search param while switching the pages * fix-sso: redirecting to prev tab's login organization slug * fix-sso: google signup * fix: preivew permission issue * Fixing merge issues * Fixed tjdb issues * dark mode fix of manage users button * fix: extra slash in url, tj-logo on click wrong org id * subpath workspace login url * resolved lint issue * fix: cannot clone apps * fixed switch workspace issue * fix: login page flashing issue * fix: back button issue * fix: private endless redirection * Update the modal with new UX * fixed all ui issues * fixed placeholder translation issues * fix: sso multi-request issues * fix: multi-pages crash while promoting a version * fix: error text msg delay * added default slug to the first workspace in the instance * subpath-fix: slug preview url * fix: same value check * fixed switch page query params issue * fix: folder query * fix: manage app users ui responsive issue * Backend PR changes --------- Co-authored-by: gsmithun4 --- .../cypress/constants/texts/common.js | 2 +- frontend/assets/translations/en.json | 8 +- .../LoginPage/GitSSOLoginButton.jsx | 3 +- .../LoginPage/GoogleSSOLoginButton.jsx | 1 + frontend/src/App/App.jsx | 168 +----- frontend/src/AppLoader/AppLoader.jsx | 61 +- .../src/Editor/Comment/CommentActions.jsx | 12 +- frontend/src/Editor/Comment/CommentHeader.jsx | 8 +- frontend/src/Editor/Comment/index.jsx | 23 +- .../Editor/CommentNotifications/Content.jsx | 3 +- .../src/Editor/CommentNotifications/index.jsx | 25 +- frontend/src/Editor/Comments.jsx | 12 +- frontend/src/Editor/Container.jsx | 17 +- frontend/src/Editor/Editor.jsx | 39 +- frontend/src/Editor/Header/EditAppName.jsx | 4 +- frontend/src/Editor/Header/GlobalSettings.jsx | 122 +++- frontend/src/Editor/Header/index.js | 10 +- .../src/Editor/LeftSidebar/SidebarComment.jsx | 15 +- frontend/src/Editor/LeftSidebar/index.jsx | 4 + frontend/src/Editor/ManageAppUsers.jsx | 268 ++++++--- frontend/src/Editor/RealtimeEditor.jsx | 13 +- frontend/src/Editor/ReleaseVersionButton.jsx | 4 +- frontend/src/Editor/Viewer.jsx | 166 +----- .../src/Editor/Viewer/ViewerNavigation.jsx | 3 +- frontend/src/HomePage/AppCard.jsx | 5 +- frontend/src/HomePage/ExportAppModal.jsx | 8 +- frontend/src/HomePage/Folders.jsx | 22 +- frontend/src/HomePage/HomePage.jsx | 29 +- .../HomePage/SwitchWorkspacePage/index.jsx | 19 +- frontend/src/LoginPage/LoginPage.jsx | 537 +++++++++--------- frontend/src/ManageSSO/GeneralSettings.jsx | 5 +- frontend/src/Oauth/Authorize.jsx | 11 +- .../src/OnBoardingForm/OnBoardingForm.jsx | 7 +- .../src/OnBoardingForm/OnbboardingFromSH.jsx | 7 +- .../VerificationSuccessInfoScreen.jsx | 7 +- .../TooljetDatabase/TableListItem/index.jsx | 4 +- .../NotificationCenter/Notification.jsx | 3 +- frontend/src/_components/OnboardingNavbar.jsx | 4 +- .../CreateOrganization.jsx | 238 ++++++-- .../OrganizationManager/EditOrganization.jsx | 254 +++++++-- .../_components/OrganizationManager/List.jsx | 11 +- frontend/src/_components/PrivateRoute.jsx | 94 +-- frontend/src/_helpers/authorizeWorkspace.js | 182 ++++++ frontend/src/_helpers/handle-response.js | 6 +- frontend/src/_helpers/handleAppAccess.js | 75 +++ frontend/src/_helpers/http-client.js | 2 +- frontend/src/_helpers/routes.js | 158 +++++- frontend/src/_helpers/utils.js | 179 +++--- frontend/src/_services/app.service.js | 181 ------ frontend/src/_services/apps.service.js | 206 +++++++ .../src/_services/authentication.service.js | 59 +- frontend/src/_services/index.js | 1 + .../src/_services/organization.service.js | 11 +- frontend/src/_styles/theme.scss | 204 ++++++- frontend/src/_ui/TJLoader/TJLoader.jsx | 13 + frontend/src/_ui/TJLoader/index.js | 1 + server/ee/services/oauth/oauth.service.ts | 14 +- ...5489093-AddUniqueConstraintToFolderName.ts | 4 +- .../1685952833787-AddSlugToWorkspace.ts | 28 + server/src/controllers/app.controller.ts | 31 +- server/src/controllers/apps.controller.ts | 72 ++- .../controllers/organizations.controller.ts | 10 +- server/src/dto/organization.dto.ts | 59 +- server/src/entities/organization.entity.ts | 3 + .../src/helpers/db_constraints.constants.ts | 2 + server/src/helpers/utils.helper.ts | 34 +- .../src/interceptors/valid.app.interceptor.ts | 2 +- .../organizations/organizations.module.ts | 1 + .../src/services/app_import_export.service.ts | 32 +- server/src/services/apps.service.ts | 89 +-- server/src/services/auth.service.ts | 33 +- server/src/services/folders.service.ts | 34 +- server/src/services/organizations.service.ts | 148 +++-- server/src/services/seeds.service.ts | 1 + server/src/services/users.service.ts | 1 + server/test/controllers/app.e2e-spec.ts | 4 +- .../oauth/oauth-git-instance.e2e-spec.ts | 9 +- .../controllers/oauth/oauth-git.e2e-spec.ts | 9 +- .../oauth/oauth-google-instance.e2e-spec.ts | 9 +- .../oauth/oauth-google.e2e-spec.ts | 9 +- .../controllers/organizations.e2e-spec.ts | 13 +- server/test/controllers/session.e2e-spec.ts | 2 +- 82 files changed, 2716 insertions(+), 1471 deletions(-) create mode 100644 frontend/src/_helpers/authorizeWorkspace.js create mode 100644 frontend/src/_helpers/handleAppAccess.js create mode 100644 frontend/src/_services/apps.service.js create mode 100644 frontend/src/_ui/TJLoader/TJLoader.jsx create mode 100644 frontend/src/_ui/TJLoader/index.js create mode 100644 server/migrations/1685952833787-AddSlugToWorkspace.ts diff --git a/cypress-tests/cypress/constants/texts/common.js b/cypress-tests/cypress/constants/texts/common.js index 22699a00f3..6856686179 100644 --- a/cypress-tests/cypress/constants/texts/common.js +++ b/cypress-tests/cypress/constants/texts/common.js @@ -164,7 +164,7 @@ export const commonText = { shareModalElements: { modalHeader: "Share", - makePublicAppToggleLabel: "Make application public?", + makePublicAppToggleLabel: "Make application public", shareableAppLink: "Get shareable link for this application", copyAppLinkButton: "copy", // iframeLinkLabel: "Get embeddable link for this application", diff --git a/frontend/assets/translations/en.json b/frontend/assets/translations/en.json index ff40b511e6..a8c46d9055 100644 --- a/frontend/assets/translations/en.json +++ b/frontend/assets/translations/en.json @@ -117,10 +117,10 @@ "preview": "Preview", "share": "Share", "shareModal": { - "makeApplicationPublic": "Make application public?", - "shareableLink": "Get shareable link for this application", + "makeApplicationPublic": "Make application public", + "shareableLink": "Shareable app link", "copy": "copy", - "embeddableLink": "Get embeddable link for this application", + "embeddableLink": "Embedded app link", "manageUsers": "Users" }, "appVersionManager": { @@ -234,7 +234,7 @@ "addNewWorkSpace": "Add new workspace", "loadOrganizations": "Load Organizations", "createWorkspace": "Create workspace", - "workspaceName": "workspace name", + "workspaceName": "Workspace name", "editWorkspace": "Edit workspace", "menus": { "addWorkspace": "Add workspace", diff --git a/frontend/ee/components/LoginPage/GitSSOLoginButton.jsx b/frontend/ee/components/LoginPage/GitSSOLoginButton.jsx index 3bac30ab25..c6d3d75f61 100644 --- a/frontend/ee/components/LoginPage/GitSSOLoginButton.jsx +++ b/frontend/ee/components/LoginPage/GitSSOLoginButton.jsx @@ -1,9 +1,10 @@ import React from 'react'; import { buildURLWithQuery } from '@/_helpers/utils'; -export default function GitSSOLoginButton({ configs, text }) { +export default function GitSSOLoginButton({ configs, text, setRedirectUrlToCookie }) { const gitLogin = (e) => { e.preventDefault(); + setRedirectUrlToCookie && setRedirectUrlToCookie(); window.location.href = buildURLWithQuery(`${configs.host_name || 'https://github.com'}/login/oauth/authorize`, { client_id: configs?.client_id, scope: 'user:email', diff --git a/frontend/ee/components/LoginPage/GoogleSSOLoginButton.jsx b/frontend/ee/components/LoginPage/GoogleSSOLoginButton.jsx index bd344ac31e..ff0cbe3fb1 100644 --- a/frontend/ee/components/LoginPage/GoogleSSOLoginButton.jsx +++ b/frontend/ee/components/LoginPage/GoogleSSOLoginButton.jsx @@ -12,6 +12,7 @@ export default function GoogleSSOLoginButton(props) { }; const googleLogin = (e) => { e.preventDefault(); + props.setRedirectUrlToCookie && props.setRedirectUrlToCookie(); const { client_id } = props.configs; const authUrl = buildURLWithQuery('https://accounts.google.com/o/oauth2/auth', { redirect_uri: `${window.public_config?.TOOLJET_HOST}${window.public_config?.SUB_PATH ?? '/'}sso/google${ diff --git a/frontend/src/App/App.jsx b/frontend/src/App/App.jsx index e72284077d..5958d6ea70 100644 --- a/frontend/src/App/App.jsx +++ b/frontend/src/App/App.jsx @@ -1,16 +1,8 @@ import React, { Suspense } from 'react'; // eslint-disable-next-line no-unused-vars -import config from 'config'; import { BrowserRouter, Navigate, Route, Routes } from 'react-router-dom'; - -import { - getWorkspaceIdFromURL, - appendWorkspaceId, - stripTrailingSlash, - getSubpath, - pathnameWithoutSubpath, -} from '@/_helpers/utils'; -import { authenticationService, tooljetService, organizationService } from '@/_services'; +import { authorizeWorkspace } from '@/_helpers/authorizeWorkspace'; +import { authenticationService, tooljetService } from '@/_services'; import { withRouter } from '@/_hoc/withRouter'; import { PrivateRoute, AdminRoute } from '@/_components'; import { HomePage } from '@/HomePage'; @@ -36,6 +28,7 @@ import { AppLoader } from '@/AppLoader'; import SetupScreenSelfHost from '../SuccessInfoScreen/SetupScreenSelfHost'; export const BreadCrumbContext = React.createContext({}); import 'react-tooltip/dist/react-tooltip.css'; +import { getWorkspaceIdOrSlugFromURL } from '@/_helpers/routes'; const AppWrapper = (props) => { return ( @@ -69,59 +62,8 @@ class AppComponent extends React.Component { }); }; - isThisExistedRoute = () => { - const existedPaths = [ - 'forgot-password', - 'reset-password', - 'invitations', - 'organization-invitations', - 'setup', - 'confirm', - 'confirm-invite', - ]; - - const subpath = getSubpath(); - const subpathArray = subpath ? subpath.split('/').filter((path) => path != '') : []; - const pathnames = window.location.pathname.split('/')?.filter((path) => path != ''); - const checkPath = () => existedPaths.find((path) => pathnames[subpath ? subpathArray.length : 0] === path); - return pathnames?.length > 0 ? (checkPath() ? true : false) : false; - }; - componentDidMount() { - if (!this.isThisExistedRoute()) { - const workspaceId = getWorkspaceIdFromURL(); - if (workspaceId) { - this.authorizeUserAndHandleErrors(workspaceId); - } else { - const isApplicationsPath = window.location.pathname.includes('/applications/'); - const appId = isApplicationsPath ? pathnameWithoutSubpath(window.location.pathname).split('/')[2] : null; - authenticationService - .validateSession(appId) - .then(({ current_organization_id }) => { - //check if the page is not switch-workspace, if then redirect to the page - if (window.location.pathname !== `${getSubpath() ?? ''}/switch-workspace`) { - this.authorizeUserAndHandleErrors(current_organization_id); - } else { - this.updateCurrentSession({ - current_organization_id, - }); - } - }) - .catch(() => { - if (!this.isThisWorkspaceLoginPage(true) && !isApplicationsPath) { - this.updateCurrentSession({ - authentication_status: false, - }); - } else if (isApplicationsPath) { - this.updateCurrentSession({ - authentication_failed: true, - load_app: true, - }); - } - }); - } - } - + authorizeWorkspace(); this.fetchMetadata(); setInterval(this.fetchMetadata, 1000 * 60 * 60 * 1); } @@ -136,8 +78,8 @@ class AppComponent extends React.Component { componentDidUpdate(prevProps) { // Check if the current location is the dashboard (homepage) if ( - this.props.location.pathname === `/${getWorkspaceIdFromURL()}` && - prevProps.location.pathname !== `/${getWorkspaceIdFromURL()}` && + this.props.location.pathname === `/${getWorkspaceIdOrSlugFromURL()}` && + prevProps.location.pathname !== `/${getWorkspaceIdOrSlugFromURL()}` && this.checkPreviousRoute(prevProps.location.pathname) && prevProps.location.pathname !== `/:workspaceId` ) { @@ -146,93 +88,6 @@ class AppComponent extends React.Component { } } - isThisWorkspaceLoginPage = (justLoginPage = false) => { - const subpath = window?.public_config?.SUB_PATH ? stripTrailingSlash(window?.public_config?.SUB_PATH) : null; - const pathname = location.pathname.replace(subpath, ''); - const pathnames = pathname.split('/').filter((path) => path !== ''); - return (justLoginPage && pathnames[0] === 'login') || (pathnames.length === 2 && pathnames[0] === 'login'); - }; - - authorizeUserAndHandleErrors = (workspaceId) => { - const subpath = getSubpath(); - this.updateCurrentSession({ - current_organization_id: workspaceId, - }); - authenticationService - .authorize() - .then((data) => { - organizationService.getOrganizations().then((response) => { - const current_organization_name = response.organizations.find((org) => org.id === workspaceId)?.name; - // this will add the other details like permission and user previlliage details to the subject - this.updateCurrentSession({ - ...data, - current_organization_name, - organizations: response.organizations, - load_app: true, - }); - - // if user is trying to load the workspace login page, then redirect to the dashboard - if (this.isThisWorkspaceLoginPage()) - return (window.location = appendWorkspaceId(workspaceId, '/:workspaceId')); - }); - }) - .catch((error) => { - // if the auth token didn't contain workspace-id, try switch workspace fn - if (error && error?.data?.statusCode === 401) { - //get current session workspace id - authenticationService - .validateSession() - .then(({ current_organization_id }) => { - // change invalid or not authorized org id to previous one - this.updateCurrentSession({ - current_organization_id, - }); - - organizationService - .switchOrganization(workspaceId) - .then((data) => { - this.updateCurrentSession(data); - if (this.isThisWorkspaceLoginPage()) - return (window.location = appendWorkspaceId(workspaceId, '/:workspaceId')); - this.authorizeUserAndHandleErrors(workspaceId); - }) - .catch(() => { - organizationService.getOrganizations().then((response) => { - const current_organization_name = response.organizations.find( - (org) => org.id === current_organization_id - )?.name; - - this.updateCurrentSession({ - current_organization_name, - load_app: true, - }); - - if (!this.isThisWorkspaceLoginPage()) - return (window.location = `${subpath ?? ''}/login/${workspaceId}`); - }); - }); - }) - .catch(() => this.logout()); - } else if ((error && error?.data?.statusCode == 422) || error?.data?.statusCode == 404) { - window.location = subpath ? `${subpath}${'/switch-workspace'}` : '/switch-workspace'; - } else { - if (!this.isThisWorkspaceLoginPage() && !this.isThisWorkspaceLoginPage(true)) - this.updateCurrentSession({ - authentication_status: false, - }); - } - }); - }; - - updateCurrentSession = (newSession) => { - const currentSession = authenticationService.currentSessionValue; - authenticationService.updateCurrentSession({ ...currentSession, ...newSession }); - }; - - logout = () => { - authenticationService.logout(); - }; - switchDarkMode = (newMode) => { this.setState({ darkMode: newMode }); localStorage.setItem('darkMode', newMode); @@ -314,22 +169,13 @@ class AppComponent extends React.Component { /> } /> - - - - } - /> { - const params = useParams(); - const appId = params.id; - - // eslint-disable-next-line react-hooks/exhaustive-deps - useEffect(() => loadAppDetails(), []); - - const loadAppDetails = () => { - appService.getApp(appId, 'edit').catch((error) => { - handleError(error); - }); - }; - - const switchOrganization = (orgId) => { - const path = `/apps/${appId}`; - const sub_path = window?.public_config?.SUB_PATH ? stripTrailingSlash(window?.public_config?.SUB_PATH) : ''; - organizationService.switchOrganization(orgId).then( - () => { - window.location.href = `${sub_path}/${orgId}${path}`; - }, - () => { - return (window.location.href = `${sub_path}/login/${orgId}?redirectTo=${path}`); - } - ); - }; - - const handleError = (error) => { - try { - if (error?.data) { - const statusCode = error.data?.statusCode; - if (statusCode === 403) { - const errorObj = safelyParseJSON(error.data?.message); - if ( - errorObj?.organizationId && - authenticationService.currentSessionValue.current_organization_id !== errorObj?.organizationId - ) { - switchOrganization(errorObj?.organizationId); - return; - } - redirectToDashboard(); - } else if (statusCode === 401) { - window.location = `${getSubpath() ?? ''}/login${ - !_.isEmpty(getWorkspaceId()) ? `/${getWorkspaceId()}` : '' - }?redirectTo=${this.props.location.pathname}`; - return; - } else if (statusCode === 404 || statusCode === 422) { - toast.error(error?.error ?? 'App not found'); - } - redirectToDashboard(); - } - } catch (err) { - redirectToDashboard(); - } - }; - return config.ENABLE_MULTIPLAYER_EDITING ? : ; }; diff --git a/frontend/src/Editor/Comment/CommentActions.jsx b/frontend/src/Editor/Comment/CommentActions.jsx index 836012b806..f63ee536a0 100644 --- a/frontend/src/Editor/Comment/CommentActions.jsx +++ b/frontend/src/Editor/Comment/CommentActions.jsx @@ -5,10 +5,11 @@ import { useSpring, animated } from 'react-spring'; import usePopover from '@/_hooks/use-popover'; import OptionsIcon from './icons/options.svg'; // import OptionsSelectedIcon from './icons/options-selected.svg'; -import useRouter from '@/_hooks/use-router'; import { commentsService } from '@/_services'; import { useTranslation } from 'react-i18next'; +import { useAppDataStore } from '@/_stores/appDataStore'; +import { shallow } from 'zustand/shallow'; const CommentActions = ({ socket, @@ -21,8 +22,13 @@ const CommentActions = ({ }) => { const [open, trigger, content, setOpen] = usePopover(false); const popoverFadeStyle = useSpring({ opacity: open ? 1 : 0 }); - const router = useRouter(); const { t } = useTranslation(); + const { appId } = useAppDataStore( + (state) => ({ + appId: state?.appId, + }), + shallow + ); const handleDelete = async () => { await commentsService.deleteComment(commentId); @@ -31,7 +37,7 @@ const CommentActions = ({ socket.send( JSON.stringify({ event: 'events', - data: { message: 'notifications', appId: router.query.id }, + data: { message: 'notifications', appId }, }) ); }; diff --git a/frontend/src/Editor/Comment/CommentHeader.jsx b/frontend/src/Editor/Comment/CommentHeader.jsx index 0232be8521..828c46d899 100644 --- a/frontend/src/Editor/Comment/CommentHeader.jsx +++ b/frontend/src/Editor/Comment/CommentHeader.jsx @@ -7,14 +7,12 @@ import { commentsService } from '@/_services'; import { pluralize } from '@/_helpers/utils'; import Spinner from '@/_ui/Spinner'; -import useRouter from '@/_hooks/use-router'; import UnResolvedIcon from './icons/unresolved.svg'; import ResolvedIcon from './icons/resolved.svg'; -const CommentHeader = ({ socket, count = 0, threadId, isResolved, isThreadOwner, fetchThreads, close }) => { +const CommentHeader = ({ socket, count = 0, threadId, isResolved, isThreadOwner, fetchThreads, close, appId }) => { const [spinning, setSpinning] = React.useState(false); - const router = useRouter(); const handleResolved = async () => { setSpinning(true); @@ -24,7 +22,7 @@ const CommentHeader = ({ socket, count = 0, threadId, isResolved, isThreadOwner, socket.send( JSON.stringify({ event: 'events', - data: { message: 'notifications', appId: router.query.id }, + data: { message: 'notifications', appId }, }) ); if (!isResolved) { @@ -41,7 +39,7 @@ const CommentHeader = ({ socket, count = 0, threadId, isResolved, isThreadOwner, socket.send( JSON.stringify({ event: 'events', - data: { message: 'notifications', appId: router.query.id }, + data: { message: 'notifications', appId }, }) ); }; diff --git a/frontend/src/Editor/Comment/index.jsx b/frontend/src/Editor/Comment/index.jsx index 7c126a3d54..f35912bc48 100644 --- a/frontend/src/Editor/Comment/index.jsx +++ b/frontend/src/Editor/Comment/index.jsx @@ -11,8 +11,20 @@ import { commentsService, organizationService, authenticationService } from '@/_ import useRouter from '@/_hooks/use-router'; import DOMPurify from 'dompurify'; import { capitalize } from 'lodash'; +import { getPathname } from '@/_helpers/routes'; -const Comment = ({ socket, x, y, threadId, user = {}, isResolved, fetchThreads, appVersionsId, canvasWidth }) => { +const Comment = ({ + socket, + x, + y, + threadId, + user = {}, + isResolved, + fetchThreads, + appVersionsId, + canvasWidth, + appId, +}) => { const [loading, setLoading] = React.useState(true); const [editComment, setEditComment] = React.useState(''); const [editCommentId, setEditCommentId] = React.useState(''); @@ -60,7 +72,7 @@ const Comment = ({ socket, x, y, threadId, user = {}, isResolved, fetchThreads, } else { // resetting the query param // react router updates the url with the set basename resulting invalid url unless replaced - router.history(window.location.pathname.replace(window.public_config?.SUB_PATH, '/')); + router.history(getPathname()); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [open]); @@ -82,13 +94,13 @@ const Comment = ({ socket, x, y, threadId, user = {}, isResolved, fetchThreads, socket.send( JSON.stringify({ event: 'events', - data: { message: threadId, appId: router.query.id }, + data: { message: threadId, appId }, }) ); socket.send( JSON.stringify({ event: 'events', - data: { message: 'notifications', appId: router.query.id }, + data: { message: 'notifications', appId }, }) ); fetchData(); @@ -100,7 +112,7 @@ const Comment = ({ socket, x, y, threadId, user = {}, isResolved, fetchThreads, socket.send( JSON.stringify({ event: 'events', - data: { message: 'notifications', appId: router.query.id }, + data: { message: 'notifications', appId }, }) ); }; @@ -168,6 +180,7 @@ const Comment = ({ socket, x, y, threadId, user = {}, isResolved, fetchThreads, fetchThreads={fetchThreads} isThreadOwner={currentUser?.id === user.id} isResolved={isResolved} + appId={appId} /> { const router = useRouter(); @@ -35,7 +36,7 @@ const Content = ({ notifications, loading, darkMode }) => { onClick={() => { router.push({ // react router updates the url with the set basename resulting invalid url unless replaced - pathname: window.location.pathname.replace(window.public_config?.SUB_PATH, '/'), + pathname: getPathname(), search: `?threadId=${comment.thread.id}&commentId=${comment.id}`, }); }} diff --git a/frontend/src/Editor/CommentNotifications/index.jsx b/frontend/src/Editor/CommentNotifications/index.jsx index bc7364ce6a..f2eda8323e 100644 --- a/frontend/src/Editor/CommentNotifications/index.jsx +++ b/frontend/src/Editor/CommentNotifications/index.jsx @@ -3,9 +3,9 @@ import cx from 'classnames'; import React from 'react'; import { commentsService } from '@/_services'; import TabContent from './Content'; -import useRouter from '@/_hooks/use-router'; import { useAppVersionStore } from '@/_stores/appVersionStore'; import { useEditorStore } from '@/_stores/editorStore'; +import { useAppDataStore } from '@/_stores/appDataStore'; import { shallow } from 'zustand/shallow'; const CommentNotifications = ({ socket, pageId }) => { @@ -22,24 +22,31 @@ const CommentNotifications = ({ socket, pageId }) => { }), shallow ); + const { appId } = useAppDataStore( + (state) => ({ + appId: state?.appId, + }), + shallow + ); const [notifications, setNotifications] = React.useState([]); const [loading, setLoading] = React.useState(false); const [key, setKey] = React.useState('active'); - const router = useRouter(); - async function fetchData(selectedKey) { - const isResolved = selectedKey === 'resolved'; - setLoading(true); - const { data } = await commentsService.getNotifications(router.query.id, isResolved, appVersionsId, pageId); - setLoading(false); - setNotifications(data); + if (appId) { + console.log('inside-CommentNotifications', appId); + const isResolved = selectedKey === 'resolved'; + setLoading(true); + const { data } = await commentsService.getNotifications(appId, isResolved, appVersionsId, pageId); + setLoading(false); + setNotifications(data); + } } React.useEffect(() => { fetchData(); // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + }, [appId]); React.useEffect(() => { socket?.addEventListener('message', function (event) { diff --git a/frontend/src/Editor/Comments.jsx b/frontend/src/Editor/Comments.jsx index 55c6c02013..172d0b3a39 100644 --- a/frontend/src/Editor/Comments.jsx +++ b/frontend/src/Editor/Comments.jsx @@ -5,15 +5,20 @@ import { isEmpty } from 'lodash'; import Comment from './Comment'; import { commentsService } from '@/_services'; import { useAppVersionStore } from '@/_stores/appVersionStore'; -import useRouter from '@/_hooks/use-router'; +import { useAppDataStore } from '@/_stores/appDataStore'; const Comments = ({ newThread = {}, socket, canvasWidth, currentPageId }) => { const [threads, setThreads] = React.useState([]); - const router = useRouter(); const { appVersionsId } = useAppVersionStore((state) => ({ appVersionsId: state?.editingVersion?.id }), shallow); + const { appId } = useAppDataStore( + (state) => ({ + appId: state?.appId, + }), + shallow + ); async function fetchData() { - const { data } = await commentsService.getThreads(router.query.id, appVersionsId); + const { data } = await commentsService.getThreads(appId, appVersionsId); setThreads(data); } @@ -49,6 +54,7 @@ const Comments = ({ newThread = {}, socket, canvasWidth, currentPageId }) => { socket={socket} threadId={id} canvasWidth={canvasWidth} + appId={appId} {...thread} /> ); diff --git a/frontend/src/Editor/Container.jsx b/frontend/src/Editor/Container.jsx index d196cd5213..f7f49a994c 100644 --- a/frontend/src/Editor/Container.jsx +++ b/frontend/src/Editor/Container.jsx @@ -7,7 +7,6 @@ import { DraggableBox } from './DraggableBox'; import update from 'immutability-helper'; import { componentTypes } from './WidgetManager/components'; import { resolveReferences } from '@/_helpers/utils'; -import useRouter from '@/_hooks/use-router'; import Comments from './Comments'; import { commentsService } from '@/_services'; import config from 'config'; @@ -18,6 +17,7 @@ import { addComponents, addNewWidgetToTheEditor } from '@/_helpers/appUtils'; import { useCurrentState } from '@/_stores/currentStateStore'; import { useAppVersionStore } from '@/_stores/appVersionStore'; import { useEditorStore } from '@/_stores/editorStore'; +import { useAppDataStore } from '@/_stores/appDataStore'; import { shallow } from 'zustand/shallow'; const NO_OF_GRIDS = 43; @@ -70,6 +70,12 @@ export const Container = ({ }), shallow ); + const { appId } = useAppDataStore( + (state) => ({ + appId: state?.appId, + }), + shallow + ); const [boxes, setBoxes] = useState(components); const [isDragging, setIsDragging] = useState(false); @@ -78,7 +84,6 @@ export const Container = ({ const [newThread, addNewThread] = useState({}); const [isContainerFocused, setContainerFocus] = useState(false); const [canvasHeight, setCanvasHeight] = useState(null); - const router = useRouter(); const canvasRef = useRef(null); const focusedParentIdRef = useRef(undefined); useHotkeys('meta+z, control+z', () => handleUndo()); @@ -420,7 +425,7 @@ export const Container = ({ ]); const { data } = await commentsService.createThread({ - appId: router.query.id, + appId, x: x, y: e.nativeEvent.offsetY, appVersionsId, @@ -436,7 +441,7 @@ export const Container = ({ socket.send( JSON.stringify({ event: 'events', - data: { message: 'threads', appId: router.query.id }, + data: { message: 'threads', appId }, }) ); @@ -465,7 +470,7 @@ export const Container = ({ }, ]); const { data } = await commentsService.createThread({ - appId: router.query.id, + appId, x, y: y - 130, appVersionsId, @@ -481,7 +486,7 @@ export const Container = ({ socket.send( JSON.stringify({ event: 'events', - data: { message: 'threads', appId: router.query.id }, + data: { message: 'threads', appId }, }) ); diff --git a/frontend/src/Editor/Editor.jsx b/frontend/src/Editor/Editor.jsx index ce8cb2333a..49e6f4a6f1 100644 --- a/frontend/src/Editor/Editor.jsx +++ b/frontend/src/Editor/Editor.jsx @@ -1,6 +1,6 @@ import React from 'react'; import { - appService, + appsService, authenticationService, appVersionService, orgEnvironmentVariableService, @@ -8,8 +8,7 @@ import { } from '@/_services'; import { DndProvider } from 'react-dnd'; import { HTML5Backend } from 'react-dnd-html5-backend'; -import { defaults, cloneDeep, isEqual, isEmpty, debounce, omit } from 'lodash'; -import { shallow } from 'zustand/shallow'; +import _, { defaults, cloneDeep, isEqual, isEmpty, debounce, omit } from 'lodash'; import { Container } from './Container'; import { EditorKeyHooks } from './EditorKeyHooks'; import { CustomDragLayer } from './CustomDragLayer'; @@ -60,6 +59,7 @@ import { useAppDataStore } from '@/_stores/appDataStore'; import { useCurrentStateStore, useCurrentState } from '@/_stores/currentStateStore'; import { resetAllStores } from '@/_stores/utils'; import { setCookie } from '@/_helpers/cookie'; +import { shallow } from 'zustand/shallow'; setAutoFreeze(false); enablePatches(); @@ -67,21 +67,16 @@ enablePatches(); class EditorComponent extends React.Component { constructor(props) { super(props); - resetAllStores(); - const appId = this.props.params.id; + resetAllStores(); + const appId = props.id; useAppDataStore.getState().actions.setAppId(appId); useEditorStore.getState().actions.setIsEditorActive(true); const { socket } = createWebsocketConnection(appId); - - this.renameQueryNameId = React.createRef(); - this.socket = socket; - + this.renameQueryNameId = React.createRef(); const defaultPageId = uuid(); - this.subscription = null; - this.defaultDefinition = { showViewerNavigation: true, homePageId: defaultPageId, @@ -199,7 +194,6 @@ class EditorComponent extends React.Component { threshold: 0, }, }); - const globals = { ...this.props.currentState.globals, theme: { name: this.props.darkMode ? 'dark' : 'light' }, @@ -341,7 +335,7 @@ class EditorComponent extends React.Component { const newState = !this.state.app.is_maintenance_on; // eslint-disable-next-line no-unused-vars - appService.setMaintenance(this.state.app.id, newState).then((data) => { + appsService.setMaintenance(this.state.app.id, newState).then((data) => { this.setState({ app: { ...this.state.app, @@ -358,7 +352,7 @@ class EditorComponent extends React.Component { }; fetchApps = (page) => { - appService.getAll(page).then((data) => + appsService.getAll(page).then((data) => this.setState({ apps: data.apps, }) @@ -366,7 +360,7 @@ class EditorComponent extends React.Component { }; fetchApp = (startingPageHandle) => { - const appId = this.props.params.id; + const appId = this.props.id; const callBack = async (data) => { let dataDefinition = defaults(data.definition, this.defaultDefinition); @@ -424,7 +418,7 @@ class EditorComponent extends React.Component { isLoading: true, }, () => { - appService.getApp(appId).then(callBack); + appsService.getApp(appId).then(callBack); } ); }; @@ -1399,7 +1393,11 @@ class EditorComponent extends React.Component { const queryParamsString = queryParams.map(([key, value]) => `${key}=${value}`).join('&'); - this.props.navigate(`/${getWorkspaceId()}/apps/${this.state.appId}/${handle}?${queryParamsString}`); + this.props.navigate(`/${getWorkspaceId()}/apps/${this.state.slug}/${handle}?${queryParamsString}`, { + state: { + isSwitchingPage: true, + }, + }); const { globals: existingGlobals } = this.props.currentState; @@ -1511,8 +1509,11 @@ class EditorComponent extends React.Component { const selectedComponents = this?.props?.selectedComponents; const currentState = this.props?.currentState; const editingVersion = this.props?.editingVersion; + const previewQuery = queryString.stringify({ version: editingVersion?.name }); const appVersionPreviewLink = editingVersion - ? `/applications/${app.id}/versions/${editingVersion.id}/${currentState.page.handle}` + ? `/applications/${slug || appId}/${currentState.page.handle}${ + !_.isEmpty(previewQuery) ? `?${previewQuery}` : '' + }` : ''; return (
@@ -1605,6 +1606,8 @@ class EditorComponent extends React.Component { updateOnSortingPages={this.updateOnSortingPages} apps={apps} setEditorMarginLeft={this.handleEditorMarginLeftChange} + slug={slug} + handleSlugChange={this.handleSlugChange} /> {!this.props.showComments && ( diff --git a/frontend/src/Editor/Header/EditAppName.jsx b/frontend/src/Editor/Header/EditAppName.jsx index 304e964eb2..d1514d60e7 100644 --- a/frontend/src/Editor/Header/EditAppName.jsx +++ b/frontend/src/Editor/Header/EditAppName.jsx @@ -1,6 +1,6 @@ import React, { useRef, useEffect, useState } from 'react'; import { ToolTip } from '@/_components'; -import { appService } from '@/_services'; +import { appsService } from '@/_services'; import { handleHttpErrorMessages, validateName } from '@/_helpers/utils'; import InfoOrErrorBox from './InfoOrErrorBox'; import { toast } from 'react-hot-toast'; @@ -47,7 +47,7 @@ function EditAppName({ appId, appName = '', onNameChanged }) { } try { - await appService.saveApp(appId, { name: trimmedName }); + await appsService.saveApp(appId, { name: trimmedName }); onNameChanged(trimmedName); setIsValid(true); setIsEditing(false); diff --git a/frontend/src/Editor/Header/GlobalSettings.jsx b/frontend/src/Editor/Header/GlobalSettings.jsx index 81b16c426b..d0654d317c 100644 --- a/frontend/src/Editor/Header/GlobalSettings.jsx +++ b/frontend/src/Editor/Header/GlobalSettings.jsx @@ -5,9 +5,11 @@ import { Confirm } from '../Viewer/Confirm'; import { HeaderSection } from '@/_ui/LeftSidebar'; import FxButton from '../CodeBuilder/Elements/FxButton'; import { CodeHinter } from '../CodeBuilder/CodeHinter'; -import { resolveReferences } from '@/_helpers/utils'; +import { resolveReferences, validateName, getWorkspaceId } from '@/_helpers/utils'; import { useTranslation } from 'react-i18next'; import _ from 'lodash'; +import { appsService } from '@/_services'; +import { replaceEditorURL, getHostURL } from '@/_helpers/routes'; import ExportAppModal from '../../HomePage/ExportAppModal'; import { useAppVersionStore } from '@/_stores/appVersionStore'; import { shallow } from 'zustand/shallow'; @@ -22,6 +24,8 @@ export const GlobalSettings = ({ app, backgroundFxQuery, realState, + handleSlugChange, + slug: oldSlug, }) => { const { t } = useTranslation(); const { hideHeader, canvasMaxWidth, canvasMaxWidthType, canvasBackgroundColor } = globalSettings; @@ -29,6 +33,10 @@ export const GlobalSettings = ({ const [forceCodeBox, setForceCodeBox] = useState(true); const [showConfirmation, setConfirmationShow] = useState(false); const [isExportingApp, setIsExportingApp] = React.useState(false); + /* Unique app slug states */ + const [slug, setSlug] = useState({ value: null, error: '' }); + const [slugProgress, setSlugProgress] = useState(false); + const [isSlugUpdated, setSlugUpdatedState] = useState(false); const { isVersionReleased } = useAppVersionStore( (state) => ({ isVersionReleased: state.isVersionReleased, @@ -44,6 +52,54 @@ export const GlobalSettings = ({ left: '0px', }; + useEffect(() => { + setSlug({ value: oldSlug, error: '' }); + }, [oldSlug]); + + const handleInputChange = (value, field) => { + setSlug({ + value: slug?.value, + error: null, + }); + + const error = validateName(value, `App ${field}`, 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 +137,72 @@ export const GlobalSettings = ({ darkMode={darkMode} /> )} -
+
-
+
+
+
+
+ + { + e.persist(); + delayedSlugChange(e.target.value, 'slug'); + }} + data-cy="app-slug-input-field" + defaultValue={oldSlug} + /> + {isSlugUpdated && ( +
+ + + +
+ )} + {slug?.error ? ( + + ) : isSlugUpdated ? ( + + ) : ( + + )} +
+
+
+
+ +
+ {!slugProgress ? ( + `${getHostURL()}/${getWorkspaceId()}/apps/${slug?.value || oldSlug || ''}` + ) : ( +
+
+ Loading... +
+ {`Updating link`} +
+ )} +
+ +
+
+
+
+
diff --git a/frontend/src/Editor/Header/index.js b/frontend/src/Editor/Header/index.js index 52476b009e..005d4a8ff3 100644 --- a/frontend/src/Editor/Header/index.js +++ b/frontend/src/Editor/Header/index.js @@ -12,8 +12,10 @@ import config from 'config'; // eslint-disable-next-line import/no-unresolved import { useUpdatePresence } from '@y-presence/react'; import { useAppVersionStore } from '@/_stores/appVersionStore'; +import { useCurrentState } from '@/_stores/currentStateStore'; import { shallow } from 'zustand/shallow'; import SolidIcon from '@/_ui/Icon/SolidIcons'; +import { redirectToDashboard } from '@/_helpers/routes'; export default function EditorHeader({ M, @@ -43,6 +45,7 @@ export default function EditorHeader({ }), shallow ); + const currentState = useCurrentState(); const updatePresence = useUpdatePresence(); useEffect(() => { @@ -61,7 +64,7 @@ export default function EditorHeader({ }, [currentUser]); const handleLogoClick = () => { // Force a reload for clearing interval triggers - window.location.href = '/'; + redirectToDashboard(); }; return ( @@ -148,9 +151,10 @@ export default function EditorHeader({ )}
diff --git a/frontend/src/Editor/LeftSidebar/SidebarComment.jsx b/frontend/src/Editor/LeftSidebar/SidebarComment.jsx index 8fe411c1ca..9ce9fb6a01 100644 --- a/frontend/src/Editor/LeftSidebar/SidebarComment.jsx +++ b/frontend/src/Editor/LeftSidebar/SidebarComment.jsx @@ -2,9 +2,9 @@ import React, { forwardRef } from 'react'; import cx from 'classnames'; import { LeftSidebarItem } from './SidebarItem'; import { commentsService } from '@/_services'; -import useRouter from '@/_hooks/use-router'; import { useAppVersionStore } from '@/_stores/appVersionStore'; import { useEditorStore } from '@/_stores/editorStore'; +import { useAppDataStore } from '@/_stores/appDataStore'; import { shallow } from 'zustand/shallow'; export const LeftSidebarComment = forwardRef(({ selectedSidebarItem, currentPageId }, ref) => { @@ -20,18 +20,23 @@ export const LeftSidebarComment = forwardRef(({ selectedSidebarItem, currentPage }), shallow ); + const { appId } = useAppDataStore( + (state) => ({ + appId: state?.appId, + }), + shallow + ); const [isActive, toggleActive] = React.useState(false); const [notifications, setNotifications] = React.useState([]); - const router = useRouter(); React.useEffect(() => { - if (appVersionsId) { - commentsService.getNotifications(router.query.id, false, appVersionsId, currentPageId).then(({ data }) => { + if (appVersionsId && appId) { + commentsService.getNotifications(appId, false, appVersionsId, currentPageId).then(({ data }) => { setNotifications(data); }); } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [appVersionsId, currentPageId]); + }, [appVersionsId, currentPageId, appId]); return ( 0} diff --git a/frontend/src/Editor/LeftSidebar/index.jsx b/frontend/src/Editor/LeftSidebar/index.jsx index f946b6df2f..55a8e11107 100644 --- a/frontend/src/Editor/LeftSidebar/index.jsx +++ b/frontend/src/Editor/LeftSidebar/index.jsx @@ -54,6 +54,8 @@ export const LeftSidebar = forwardRef((props, ref) => { toggleAppMaintenance, app, disableEnablePage, + slug, + handleSlugChange, } = props; const { is_maintenance_on } = app; @@ -223,6 +225,8 @@ export const LeftSidebar = forwardRef((props, ref) => { app={app} backgroundFxQuery={backgroundFxQuery} realState={realState} + slug={slug} + handleSlugChange={handleSlugChange} /> ), }; diff --git a/frontend/src/Editor/ManageAppUsers.jsx b/frontend/src/Editor/ManageAppUsers.jsx index 04860f1622..6f7ea88eca 100644 --- a/frontend/src/Editor/ManageAppUsers.jsx +++ b/frontend/src/Editor/ManageAppUsers.jsx @@ -1,16 +1,15 @@ import React from 'react'; -import { appService, authenticationService } from '@/_services'; +import { appService, appsService, authenticationService } from '@/_services'; import Modal from 'react-bootstrap/Modal'; import { toast } from 'react-hot-toast'; import { CopyToClipboard } from 'react-copy-to-clipboard'; import Skeleton from 'react-loading-skeleton'; -import { debounce } from 'lodash'; -import Textarea from '@/_ui/Textarea'; +import _, { debounce } from 'lodash'; import { withTranslation } from 'react-i18next'; import { Link } from 'react-router-dom'; -import { getPrivateRoute } from '@/_helpers/routes'; +import { getPrivateRoute, replaceEditorURL, getHostURL } from '@/_helpers/routes'; +import { validateName } from '@/_helpers/utils'; import SolidIcon from '@/_ui/Icon/SolidIcons'; -import { getSubpath } from '@/_helpers/utils'; class ManageAppUsersComponent extends React.Component { constructor(props) { @@ -20,11 +19,15 @@ 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, }; } @@ -35,7 +38,7 @@ class ManageAppUsersComponent extends React.Component { } fetchAppUsers = () => { - appService + appsService .getAppUsers(this.props.app.id) .then((data) => this.setState({ @@ -52,6 +55,12 @@ class ManageAppUsersComponent extends React.Component { hideModal = () => { this.setState({ showModal: false, + newSlug: { + value: this.props.slug, + error: '', + }, + isSlugVerificationInProgress: false, + isSlugUpdated: false, }); }; @@ -82,7 +91,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,41 +116,71 @@ 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}`, false, !(field === 'slug'), !(field === 'slug')); + + if (!_.isEmpty(value) && value !== this.props.slug && _.isEmpty(error.errorMsg)) { + this.setState({ + isSlugVerificationInProgress: true, + }); + appsService + .setSlug(this.state.app.id, value) + .then(() => { + this.setState({ + newSlug: { + value: value, + error: '', + }, + isSlugVerificationInProgress: false, + isSlugUpdated: true, + }); + this.props.handleSlugChange(value); + replaceEditorURL(value, this.props.pageHandle); + }) + .catch(({ error }) => { + this.setState({ + newSlug: { + value, + error, + }, + isSlugVerificationInProgress: false, + isSlugUpdated: false, + }); + }); + } else { + this.setState({ + newSlug: { + value, + error: error?.errorMsg, + }, + isSlugVerificationInProgress: false, + isSlugUpdated: false, + }); + } + }; + render() { - const { isLoading, app, slugError, isSlugVerificationInProgress } = this.state; + const { isLoading, app, isSlugVerificationInProgress, newSlug, isSlugUpdated } = this.state; const appId = app.id; - const appLink = `${window.public_config?.TOOLJET_HOST}${getSubpath() ? getSubpath() : ''}/applications/`; + const appLink = `${getHostURL()}/applications/`; const shareableLink = appLink + (this.props.slug || appId); - const slugButtonClass = isSlugVerificationInProgress ? '' : slugError !== null ? 'is-invalid' : 'is-valid'; + const slugButtonClass = !_.isEmpty(newSlug.error) ? 'is-invalid' : 'is-valid'; const embeddableLink = ``; return ( -
+
this.setState({ showModal: true })}> @@ -168,96 +207,159 @@ class ManageAppUsersComponent extends React.Component {
) : ( -
+