diff --git a/.version b/.version index bf29619fd7..e9763f6bfe 100644 --- a/.version +++ b/.version @@ -1 +1 @@ -2.22.3 +2.23.0 diff --git a/cypress-tests/cypress/constants/selectors/common.js b/cypress-tests/cypress/constants/selectors/common.js index a61fd9ab5b..f73c6d4ab8 100644 --- a/cypress-tests/cypress/constants/selectors/common.js +++ b/cypress-tests/cypress/constants/selectors/common.js @@ -225,6 +225,7 @@ export const commonSelectors = { workspaceConstantsOption: '[data-cy="workspace-constants-list-item"]', nameErrorText: '[data-cy="name-error-text"]', valueErrorText: '[data-cy="value-error-text"]', + releaseButton: '[data-cy="button-release"]', }; export const commonWidgetSelector = { diff --git a/cypress-tests/cypress/constants/texts/manageGroups.js b/cypress-tests/cypress/constants/texts/manageGroups.js index ef30f24be4..68f90ba368 100644 --- a/cypress-tests/cypress/constants/texts/manageGroups.js +++ b/cypress-tests/cypress/constants/texts/manageGroups.js @@ -37,7 +37,7 @@ export const groupsText = { helperTextPermissions: "Add app to the group to control permissions for users in this group", helperTextAllUsersIncluded: - " All users include every users in the app. This list is not editable", + " All users within the workspace are included in this list. This list cannot be edited.", helperTextAdminAppAccess: "Admin has edit access to all apps. These are not editable", helperTextAdminPermissions: "Admin has all permissions", diff --git a/cypress-tests/cypress/e2e/editor/widget/componentsBasicHappypath.cy.js b/cypress-tests/cypress/e2e/editor/widget/componentsBasicHappypath.cy.js index 768f5567d4..d186ba3250 100644 --- a/cypress-tests/cypress/e2e/editor/widget/componentsBasicHappypath.cy.js +++ b/cypress-tests/cypress/e2e/editor/widget/componentsBasicHappypath.cy.js @@ -36,7 +36,7 @@ describe("Basic components", () => { beforeEach(() => { data.appName = `${fake.companyName}-${fake.companyName}-App`; cy.apiLogin(); - cy.apiCreateApp(); + cy.apiCreateApp(data.appName); cy.openApp(); cy.modifyCanvasSize(900, 900); cy.intercept("GET", "/api/comments/*").as("loadComments"); @@ -584,9 +584,9 @@ describe("Basic components", () => { verifyComponentWithOutLabel("Tags", "tags1", "tags2", data.appName); }); - it("Should verify Textarea", () => { + it("Should verify Text area", () => { verifyComponentWithOutLabel( - "Textarea", + "Text area", "textarea1", "textarea2", data.appName diff --git a/cypress-tests/cypress/e2e/editor/widget/csa.cy.js b/cypress-tests/cypress/e2e/editor/widget/csa.cy.js index 95a385d5bf..1094cf955d 100644 --- a/cypress-tests/cypress/e2e/editor/widget/csa.cy.js +++ b/cypress-tests/cypress/e2e/editor/widget/csa.cy.js @@ -1,5 +1,6 @@ import { commonSelectors, commonWidgetSelector } from "Selectors/common"; import { openEditorSidebar } from "Support/utils/commonWidget"; +import { fake } from "Fixtures/fake"; import { selectCSA, selectEvent, @@ -16,8 +17,9 @@ import { commonWidgetText } from "Texts/common"; describe("Editor- CSA", () => { const toolJetImage = "cypress/fixtures/Image/tooljet.png"; beforeEach(() => { + const appName1 = `${fake.companyName}-${fake.companyName}-App`; cy.apiLogin(); - cy.apiCreateApp(); + cy.apiCreateApp(appName1); cy.openApp(); }); @@ -104,7 +106,7 @@ describe("Editor- CSA", () => { }); it("Should verify Textarea CSA", () => { - cy.dragAndDropWidget("Textarea", 200, 100); + cy.dragAndDropWidget("Text area", 200, 100); verifyComponent("textarea1"); cy.get(commonWidgetSelector.draggableWidget("textarea1")) .should("be.visible") diff --git a/cypress-tests/cypress/e2e/globalDataSources/globalDataSourcePermissions.cy.js b/cypress-tests/cypress/e2e/globalDataSources/globalDataSourcePermissions.cy.js index cee157fc7f..8ae01f3912 100644 --- a/cypress-tests/cypress/e2e/globalDataSources/globalDataSourcePermissions.cy.js +++ b/cypress-tests/cypress/e2e/globalDataSources/globalDataSourcePermissions.cy.js @@ -53,7 +53,7 @@ describe("Global Datasource Manager", () => { cy.get(commonSelectors.globalDataSourceIcon).click(); cy.get(commonSelectors.pageSectionHeader).verifyVisibleElement( "have.text", - "Data Sources" + "Data sources" ); cy.get(dataSourceSelector.allDatasourceLabelAndCount).verifyVisibleElement( "have.text", @@ -109,12 +109,12 @@ describe("Global Datasource Manager", () => { .should("eq", "Search Plugins"); cy.get('[data-cy="added-ds-label"]').should(($el) => { - expect($el.contents().first().text().trim()).to.eq("Data Sources Added"); + expect($el.contents().first().text().trim()).to.eq("Data sources added"); }); cy.get(dataSourceSelector.addedDsSearchIcon).should("be.visible").click(); cy.get(dataSourceSelector.AddedDsSearchBar) .invoke("attr", "placeholder") - .should("eq", "Search for Data Sources"); + .should("eq", "Search for Data sources"); selectAndAddDataSource( "databases", @@ -223,7 +223,7 @@ describe("Global Datasource Manager", () => { cy.get(".p-2 > .tj-base-btn") .should("be.visible") - .and("have.text", "+ Add new data source"); + .and("have.text", "+ Add new Data source"); cy.get(".p-2 > .tj-base-btn").click(); selectAndAddDataSource( @@ -299,12 +299,12 @@ describe("Global Datasource Manager", () => { verifyValueOnInspector("student_data", "8 items "); }); it("Should verify the query creation and scope changing functionality.", () => { + data.appName = `${fake.companyName}-App`; logout(); cy.apiLogin(data.email, "password"); cy.visit('/') - cy.apiCreateApp(); + cy.apiCreateApp(data.appName); cy.openApp(); - cy.renameApp(data.appName); cy.dragAndDropWidget("Table", 250, 250); addQuery( diff --git a/cypress-tests/cypress/e2e/workspace/dashboard.cy.js b/cypress-tests/cypress/e2e/workspace/dashboard.cy.js index e7d5c84ffb..92a572e4f8 100644 --- a/cypress-tests/cypress/e2e/workspace/dashboard.cy.js +++ b/cypress-tests/cypress/e2e/workspace/dashboard.cy.js @@ -159,7 +159,7 @@ describe("dashboard", () => { cy.reload(); verifyTooltip(commonSelectors.dashboardIcon, "Dashboard"); verifyTooltip(commonSelectors.databaseIcon, "Database"); - verifyTooltip(commonSelectors.globalDataSourceIcon, "Data Sources"); + verifyTooltip(commonSelectors.globalDataSourceIcon, "Data sources"); verifyTooltip(commonSelectors.workspaceSettingsIcon, "Workspace settings"); verifyTooltip(commonSelectors.notificationsIcon, "Comment notifications"); verifyTooltip(dashboardSelector.modeToggle, "Mode"); diff --git a/cypress-tests/cypress/e2e/workspace/shareApp.cy.js b/cypress-tests/cypress/e2e/workspace/shareApp.cy.js index 89ca36c237..be5c35f03a 100644 --- a/cypress-tests/cypress/e2e/workspace/shareApp.cy.js +++ b/cypress-tests/cypress/e2e/workspace/shareApp.cy.js @@ -1,6 +1,6 @@ import { commonSelectors, commonWidgetSelector } from "Selectors/common"; import { fake } from "Fixtures/fake"; -import { logout, navigateToAppEditor } from "Support/utils/common"; +import { logout, navigateToAppEditor, verifyTooltip, releaseApp } from "Support/utils/common"; import { commonText } from "Texts/common"; import { addNewUserMW } from "Support/utils/userPermissions"; import { userSignUp } from "Support/utils/onboarding"; @@ -29,8 +29,12 @@ describe("App share functionality", () => { cy.openApp(data.appName); cy.dragAndDropWidget("Table", 250, 250); + verifyTooltip(commonWidgetSelector.shareAppButton, "Share URL is unavailable until current version is released") + cy.get('[data-cy="share-button-link"]>span').should("have.class", "share-disabled"); + releaseApp(); cy.get(commonWidgetSelector.shareAppButton).click(); + for (const elements in commonWidgetSelector.shareModalElements) { cy.get( commonWidgetSelector.shareModalElements[elements] @@ -47,9 +51,9 @@ describe("App share functionality", () => { cy.get(commonWidgetSelector.modalCloseButton).should("be.visible"); cy.clearAndType(commonWidgetSelector.appNameSlugInput, `${slug}`); + cy.wait(2000); cy.get(commonWidgetSelector.modalCloseButton).click(); cy.forceClickOnCanvas() - cy.dragAndDropWidget("Button", 50, 50); cy.get(commonSelectors.editorPageLogo).click(); logout(); diff --git a/cypress-tests/cypress/support/utils/common.js b/cypress-tests/cypress/support/utils/common.js index 57d3e1b128..c38c357642 100644 --- a/cypress-tests/cypress/support/utils/common.js +++ b/cypress-tests/cypress/support/utils/common.js @@ -262,4 +262,11 @@ export const createGroup = (groupName) => { export const navigateToworkspaceConstants = () => { cy.get(commonSelectors.workspaceSettingsIcon).click(); cy.get(commonSelectors.workspaceConstantsOption).click(); +}; + +export const releaseApp = () => { + cy.get(commonSelectors.releaseButton).click(); + cy.get(commonSelectors.yesButton).click(); + cy.verifyToastMessage(commonSelectors.toastMessage, "Version v1 released"); + cy.wait(1000); }; \ No newline at end of file diff --git a/frontend/.version b/frontend/.version index bf29619fd7..e9763f6bfe 100644 --- a/frontend/.version +++ b/frontend/.version @@ -1 +1 @@ -2.22.3 +2.23.0 diff --git a/frontend/src/App/App.jsx b/frontend/src/App/App.jsx index 5958d6ea70..f2712e754d 100644 --- a/frontend/src/App/App.jsx +++ b/frontend/src/App/App.jsx @@ -29,6 +29,7 @@ import SetupScreenSelfHost from '../SuccessInfoScreen/SetupScreenSelfHost'; export const BreadCrumbContext = React.createContext({}); import 'react-tooltip/dist/react-tooltip.css'; import { getWorkspaceIdOrSlugFromURL } from '@/_helpers/routes'; +import ErrorPage from '@/_components/ErrorComponents/ErrorPage'; const AppWrapper = (props) => { return ( @@ -95,6 +96,7 @@ class AppComponent extends React.Component { render() { const { updateAvailable, darkMode } = this.state; + let toastOptions = { style: { wordBreak: 'break-all', @@ -185,6 +187,15 @@ class AppComponent extends React.Component { } /> + + + + } + /> )} } /> + } + /> diff --git a/frontend/src/Editor/Inspector/Inspector.jsx b/frontend/src/Editor/Inspector/Inspector.jsx index c6f691f619..840efbdb77 100644 --- a/frontend/src/Editor/Inspector/Inspector.jsx +++ b/frontend/src/Editor/Inspector/Inspector.jsx @@ -388,19 +388,19 @@ export const Inspector = ({ setWidgetDeleteConfirmation(false); }, [switchSidebarTab, removeComponent, component, setWidgetDeleteConfirmation]); - React.useEffect(()=>{ + React.useEffect(() => { const handleKeyPress = (event) => { - if (showWidgetDeleteConfirmation && event.key === 'Enter') { - handleDeleteConfirm(); - } + if (showWidgetDeleteConfirmation && event.key === 'Enter') { + handleDeleteConfirm(); + } }; document.addEventListener('keydown', handleKeyPress); return () => { - document.removeEventListener('keydown', handleKeyPress); + document.removeEventListener('keydown', handleKeyPress); }; }, [showWidgetDeleteConfirmation, handleDeleteConfirm]); - + return (
{dataSources.length ? ( <> -
Local Data Sources
+
Local Data sources
{dataSources?.map((source, idx) => ( `; + const shouldWeDisableShareModal = !this.props.isVersionReleased; return ( -
- { - this.validateThePreExistingSlugs(); - this.setState({ showModal: true }); - }} + +
- - - - - {this.props.t('editor.share', 'Share')} - - - - - - {isLoading ? ( -
- -
- ) : ( -
{admin && ( (
-
No data sources have been added yet.
+
No Data sources have been added yet.
); diff --git a/frontend/src/Editor/QueryManager/Components/DataSourceSelect.jsx b/frontend/src/Editor/QueryManager/Components/DataSourceSelect.jsx index b0b4d42311..fb6dfc8802 100644 --- a/frontend/src/Editor/QueryManager/Components/DataSourceSelect.jsx +++ b/frontend/src/Editor/QueryManager/Components/DataSourceSelect.jsx @@ -53,7 +53,7 @@ function DataSourceSelect({ isDisabled, selectRef, closePopup }) {
{index === 0 && (
- Data Sources + Data sources
)} @@ -246,7 +246,7 @@ const MenuList = ({ children, getStyles, innerRef, ...props }) => { {admin && (
- + Add new data source + + Add new Data source
)} diff --git a/frontend/src/Editor/Viewer.jsx b/frontend/src/Editor/Viewer.jsx index 00becffa4d..dc61fb865b 100644 --- a/frontend/src/Editor/Viewer.jsx +++ b/frontend/src/Editor/Viewer.jsx @@ -34,8 +34,9 @@ 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 { getPreviewQueryParams, redirectToDashboard, redirectToErrorPage } from '@/_helpers/routes'; import toast from 'react-hot-toast'; +import { ERROR_TYPES } from '@/_helpers/constants'; class ViewerComponent extends React.Component { constructor(props) { @@ -241,6 +242,9 @@ class ViewerComponent extends React.Component { appsService .getAppBySlug(slug) .then((data) => { + if (authentication_failed && !data.current_version_id) { + redirectToErrorPage(ERROR_TYPES.URL_UNAVAILABLE, {}); + } this.setStateForApp(data); this.setStateForContainer(data); this.setWindowTitle(data.name); @@ -249,12 +253,13 @@ class ViewerComponent extends React.Component { this.setState({ isLoading: false, }); - if (authentication_failed && error?.statusCode === 404) { + if (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); + redirectToErrorPage(ERROR_TYPES.INVALID); + } else if (error?.statusCode === 403) { + redirectToErrorPage(ERROR_TYPES.RESTRICTED); + } else { + redirectToErrorPage(ERROR_TYPES.UNKNOWN); } }); }; diff --git a/frontend/src/GlobalDatasources/List/index.jsx b/frontend/src/GlobalDatasources/List/index.jsx index ff0629b4bc..95533e591c 100644 --- a/frontend/src/GlobalDatasources/List/index.jsx +++ b/frontend/src/GlobalDatasources/List/index.jsx @@ -117,7 +117,7 @@ export const List = ({ updateSelectedDatasource }) => { {!showInput ? ( <>
- Data Sources Added{' '} + Data sources added{' '} {!isLoading && filteredData && filteredData.length > 0 && `(${filteredData.length})`}
{

- All users include every users in the app. - This list is not editable + All users within the workspace are included + in this list. This list cannot be edited.

)} diff --git a/frontend/src/_components/ErrorComponents/ErrorPage.jsx b/frontend/src/_components/ErrorComponents/ErrorPage.jsx new file mode 100644 index 0000000000..d14c2e852c --- /dev/null +++ b/frontend/src/_components/ErrorComponents/ErrorPage.jsx @@ -0,0 +1,94 @@ +import { ERROR_MESSAGES } from '@/_helpers/constants'; +import { redirectToDashboard } from '@/_helpers/routes'; +import React from 'react'; +import { Modal } from 'react-bootstrap'; +import { useTranslation } from 'react-i18next'; +import { useParams } from 'react-router-dom'; +import './static-modal.scss'; + +export default function ErrorPage({ darkMode }) { + const params = useParams(); + const errorType = params?.errorType; + const errorMsg = ERROR_MESSAGES[errorType]; + + if (!errorMsg) redirectToDashboard(); + + return ( +
+ +
+ ); +} + +export const ErrorModal = ({ errorMsg, ...props }) => { + const { t } = useTranslation(); + + return ( +
+ + + + + + + + + + {t('globals.static-error-modal.title', errorMsg?.title)} +

{t('globals.static-error-modal.description', errorMsg?.message)}

+
+ + {errorMsg?.retry && ( + + )} + + +
+
+ ); +}; diff --git a/frontend/src/_components/ErrorComponents/index.js b/frontend/src/_components/ErrorComponents/index.js new file mode 100644 index 0000000000..6e8d01e81e --- /dev/null +++ b/frontend/src/_components/ErrorComponents/index.js @@ -0,0 +1 @@ +export { ErrorPage } from './ErrorPage'; diff --git a/frontend/src/_components/ErrorComponents/static-modal.scss b/frontend/src/_components/ErrorComponents/static-modal.scss new file mode 100644 index 0000000000..985a8f0311 --- /dev/null +++ b/frontend/src/_components/ErrorComponents/static-modal.scss @@ -0,0 +1,30 @@ +.static-error-modal { + .modal-footer { + border: none; + padding-top: 0px; + } + + .modal-header { + border-bottom: none; + padding-bottom: 0px; + } + + .header-text { + font-size: 16px !important; + font-weight: 500 !important; + } + + .description { + font-size: 14px; + font-weight: 400; + } + + .action-btn { + width: 100%; + height: 40px; + margin-top: 24px; + margin-bottom: 16px; + font-size: 14px !important; + font-weight: 500 !important; + } + } \ No newline at end of file diff --git a/frontend/src/_components/PrivateRoute.jsx b/frontend/src/_components/PrivateRoute.jsx index d1cb2a1da6..60292f041c 100644 --- a/frontend/src/_components/PrivateRoute.jsx +++ b/frontend/src/_components/PrivateRoute.jsx @@ -1,10 +1,11 @@ import React, { useEffect, useState } from 'react'; import { Navigate, useLocation, useNavigate, useParams } from 'react-router-dom'; import { authenticationService } from '@/_services'; -import { appendWorkspaceId, excludeWorkspaceIdFromURL, getPathname } from '@/_helpers/routes'; +import { appendWorkspaceId, excludeWorkspaceIdFromURL, getPathname, getQueryParams } from '@/_helpers/routes'; import { TJLoader } from '@/_ui/TJLoader/TJLoader'; import { getWorkspaceId } from '@/_helpers/utils'; import { handleAppAccess } from '@/_helpers/handleAppAccess'; +import queryString from 'query-string'; export const PrivateRoute = ({ children }) => { const [session, setSession] = React.useState(authenticationService.currentSessionValue); @@ -29,11 +30,24 @@ export const PrivateRoute = ({ children }) => { ); if (isEditorOrViewerGoingToRender && group_permissions && !isSwitchingPages) { const componentType = pathname.startsWith('/apps/') ? 'editor' : 'viewer'; - const { slug } = params; + const { slug, versionId, pageHandle } = params; /* Validate the app permissions */ - const accessDetails = await handleAppAccess(componentType, slug); - setExtraProps(accessDetails); + let accessDetails = await handleAppAccess(componentType, slug, versionId); + const { versionName, ...restDetails } = accessDetails; + if (versionName) { + const restQueryParams = getQueryParams(); + const search = queryString.stringify({ + version: versionName, + ...restQueryParams, + }); + /* means. the User is trying to load old preview URL. Let's change these to query params */ + navigate( + { pathname: `/applications/${slug}${pageHandle ? `/${pageHandle}` : ''}`, search }, + { replace: true, state: location?.state } + ); + } + setExtraProps(restDetails); callback(); } else { callback(); diff --git a/frontend/src/_components/ToolTip.jsx b/frontend/src/_components/ToolTip.jsx index 2e38ee3909..89f7a0c318 100644 --- a/frontend/src/_components/ToolTip.jsx +++ b/frontend/src/_components/ToolTip.jsx @@ -9,7 +9,11 @@ export function ToolTip({ placement = 'top', trigger = ['hover', 'focus'], delay = { show: 800, hide: 100 }, + show = true, }) { + if (!show) { + return children; + } return ( {message}}> {children} diff --git a/frontend/src/_helpers/authorizeWorkspace.js b/frontend/src/_helpers/authorizeWorkspace.js index 14aeb22c2b..1f8e0a0298 100644 --- a/frontend/src/_helpers/authorizeWorkspace.js +++ b/frontend/src/_helpers/authorizeWorkspace.js @@ -5,8 +5,10 @@ import { getWorkspaceIdOrSlugFromURL, getPathname, getRedirectToWithParams, + redirectToErrorPage, } from './routes'; import toast from 'react-hot-toast'; +import { ERROR_TYPES } from './constants'; /* [* Be cautious: READ THE CASES BEFORE TOUCHING THE CODE. OTHERWISE YOU MAY SEE ENDLESS REDIRECTIONS (AKA ROUTES-BURMUDA-TRIANGLE) *] What is this function? @@ -39,15 +41,13 @@ export const authorizeWorkspace = () => { }) .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; + redirectToErrorPage(ERROR_TYPES.INVALID); + } else if (error?.data?.statusCode == 422) { + redirectToErrorPage(ERROR_TYPES.UNKNOWN); } else { + const subpath = getSubpath(); window.location = subpath ? `${subpath}${'/switch-workspace'}` : '/switch-workspace'; } } diff --git a/frontend/src/_helpers/constants.js b/frontend/src/_helpers/constants.js index ba8bceaa35..43edae7876 100644 --- a/frontend/src/_helpers/constants.js +++ b/frontend/src/_helpers/constants.js @@ -27,3 +27,45 @@ export const ON_BOARDING_ROLES = [ 'Product manager', 'Other', ]; + +export const ERROR_TYPES = { + URL_UNAVAILABLE: 'url-unavailable', + RESTRICTED: 'restricted', + INVALID: 'invalid-link', + UNKNOWN: 'unknown', +}; + +export const ERROR_MESSAGES = { + 'url-unavailable': { + title: 'URL unavailable', + message: + 'This URL is not accessible because it has not been released yet. Please either release it or contact admin for access.', + cta: 'Back to home page', + queryParams: [], + }, + restricted: { + title: 'Restricted access', + message: 'You don’t have access to this app. Kindly contact admin to know more.', + cta: 'Back to home page', + retry: false, + queryParams: [], + }, + 'invalid-link': { + title: 'Invalid link', + message: 'The link you provided is invalid. Please check the link and try again.', + cta: 'Back to home page', + retry: false, + queryParams: [], + }, + unknown: { + title: 'Oops, something went wrong!', + message: 'An error occurred while loading the app. Please try again or contact admin.', + cta: 'Back to home page', + retry: true, + queryParams: [], + }, +}; + +export const TOOLTIP_MESSAGES = { + SHARE_URL_UNAVAILABLE: 'Share URL is unavailable until current version is released', +}; diff --git a/frontend/src/_helpers/handleAppAccess.js b/frontend/src/_helpers/handleAppAccess.js index b8aab1cb8b..187c1fffad 100644 --- a/frontend/src/_helpers/handleAppAccess.js +++ b/frontend/src/_helpers/handleAppAccess.js @@ -1,18 +1,25 @@ import { organizationService, authenticationService, appsService } from '@/_services'; import { safelyParseJSON, getWorkspaceId } from '@/_helpers/utils'; -import { redirectToDashboard, getSubpath, getQueryParams } from '@/_helpers/routes'; +import { redirectToDashboard, getSubpath, getQueryParams, redirectToErrorPage } from '@/_helpers/routes'; import { toast } from 'react-hot-toast'; import _ from 'lodash'; import queryString from 'query-string'; +import { ERROR_TYPES } from './constants'; -export const handleAppAccess = (componentType, slug) => { +/* appId, versionId are olny for old preview URLs */ +export const handleAppAccess = (componentType, slug, version_id) => { const previewQueryParams = getPreviewQueryParams(); + const isOldLocalPreview = version_id ? true : false; const isLocalPreview = !_.isEmpty(previewQueryParams); - const queryParams = { ...previewQueryParams, access_type: isLocalPreview ? 'view' : 'edit' }; + const queryParams = { + ...previewQueryParams, + ...(isOldLocalPreview && { version_id }), + access_type: isLocalPreview ? 'view' : 'edit', + }; const query = queryString.stringify(previewQueryParams); const redirectPath = !_.isEmpty(query) ? `/applications/${slug}${query ? `?${query}` : ''}` : `/apps/${slug}`; - if (componentType === 'editor' || isLocalPreview) { + if (componentType === 'editor' || isLocalPreview || isOldLocalPreview) { /* Editor or app preview */ return appsService.validatePrivateApp(slug, queryParams).catch((error) => { handleError(componentType, error, slug, redirectPath); @@ -42,28 +49,46 @@ 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); + switch (statusCode) { + case 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; + } + redirectToErrorPage(ERROR_TYPES.RESTRICTED); + return; + } + case 401: { + window.location = `${getSubpath() ?? ''}/login/${getWorkspaceId()}?redirectTo=${redirectPath}`; + return; + } + case 501: { + /* Restrict the users from accessing the sharable app url if the app is not released */ + redirectToErrorPage(ERROR_TYPES.URL_UNAVAILABLE, {}); + return; + } + case 404: { + redirectToErrorPage(ERROR_TYPES.INVALID, {}); + return; + } + case 422: { + redirectToErrorPage(ERROR_TYPES.UNKNOWN, {}); + return; + } + default: { + redirectToErrorPage(ERROR_TYPES.UNKNOWN, {}); 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(); + redirectToErrorPage(ERROR_TYPES.UNKNOWN); } }; diff --git a/frontend/src/_helpers/routes.js b/frontend/src/_helpers/routes.js index 40b98d8057..183f83a51b 100644 --- a/frontend/src/_helpers/routes.js +++ b/frontend/src/_helpers/routes.js @@ -48,7 +48,7 @@ export function getQueryParams(query) { for (const param of paramsArray) { const [key, value] = param.split('='); - queryParams[key] = decodeURIComponent(value); + if (key) queryParams[key] = decodeURIComponent(value); } return query ? queryParams[query] : queryParams; @@ -108,6 +108,7 @@ export const getWorkspaceIdOrSlugFromURL = () => { 'oauth2', 'applications', 'integrations', + 'error', ]; const workspaceId = subpath ? pathnameArray[subpathArray.length] : pathnameArray[0]; @@ -172,3 +173,8 @@ export const getRedirectToWithParams = () => { const query = !_.isEmpty(queryParams) ? queryString.stringify(queryParams) : ''; return `${pathname}${!_.isEmpty(query) ? `?${query}` : ''}`; }; + +export const redirectToErrorPage = (errType, queryParams) => { + const query = !_.isEmpty(queryParams) ? queryString.stringify(queryParams) : ''; + window.location = `${getHostURL()}/error/${errType}${!_.isEmpty(query) ? `?${query}` : ''}`; +}; diff --git a/frontend/src/_styles/theme.scss b/frontend/src/_styles/theme.scss index 6f9cac5c55..992a467389 100644 --- a/frontend/src/_styles/theme.scss +++ b/frontend/src/_styles/theme.scss @@ -4255,11 +4255,24 @@ input[type="text"] { } .app-name { - width: 190px; + width: 200px; margin-left: 12px; .form-control-plaintext { - background-color: var(--base) + background-color: var(--base); + border: none !important; + } + + .form-control-plaintext:hover { + outline: none; + border: 1px solid var(--slate6) !important; + background: var(--slate2); + } + + .form-control-plaintext:focus { + outline: none; + border: 1px solid var(--indigo9) !important; + background: var(--slate2); } } @@ -11515,6 +11528,10 @@ tbody { } +.share-disabled { + opacity: 0.4; +} + // Editor revamp styles .main-wrapper { .editor { diff --git a/frontend/src/_ui/Breadcrumbs/index.jsx b/frontend/src/_ui/Breadcrumbs/index.jsx index 6783996e6a..f4293440e1 100644 --- a/frontend/src/_ui/Breadcrumbs/index.jsx +++ b/frontend/src/_ui/Breadcrumbs/index.jsx @@ -35,6 +35,6 @@ const routes = [ { path: '/:worspace_id', breadcrumb: 'Applications' }, { path: '/:worspace_id/database', breadcrumb: 'Tables', props: { dataCy: 'tables-page-header' } }, { path: '/workspace-settings', breadcrumb: 'Workspace settings' }, - { path: '/data-sources', breadcrumb: 'Data Sources' }, + { path: '/data-sources', breadcrumb: 'Data sources' }, { path: '/integrations', breadcrumb: 'Integrations / plugins', props: { beta: true } }, ]; diff --git a/frontend/src/_ui/Header/index.jsx b/frontend/src/_ui/Header/index.jsx index 99799251a4..98b410bfde 100644 --- a/frontend/src/_ui/Header/index.jsx +++ b/frontend/src/_ui/Header/index.jsx @@ -16,7 +16,7 @@ function Header() { case 'workspace-settings': return 'Workspace settings'; case 'data-sources': - return 'Data Sources'; + return 'Data sources'; case 'settings': return 'Profile settings'; case 'integrations': diff --git a/frontend/src/_ui/Layout/index.jsx b/frontend/src/_ui/Layout/index.jsx index 4bbdc5935e..4f1de55694 100644 --- a/frontend/src/_ui/Layout/index.jsx +++ b/frontend/src/_ui/Layout/index.jsx @@ -91,7 +91,7 @@ function Layout({ children, switchDarkMode, darkMode }) { {/* DATASOURCES */} {admin && (
  • - + checkForUnsavedChanges(getPrivateRoute('data_sources'), event)} diff --git a/server/.version b/server/.version index bf29619fd7..e9763f6bfe 100644 --- a/server/.version +++ b/server/.version @@ -1 +1 @@ -2.22.3 +2.23.0 diff --git a/server/src/controllers/apps.controller.ts b/server/src/controllers/apps.controller.ts index 17ade51d07..26fa40033a 100644 --- a/server/src/controllers/apps.controller.ts +++ b/server/src/controllers/apps.controller.ts @@ -12,6 +12,7 @@ import { BadRequestException, UseInterceptors, NotFoundException, + NotImplementedException, } from '@nestjs/common'; import { JwtAuthGuard } from '../../src/modules/auth/jwt-auth.guard'; import { AppsService } from '../services/apps.service'; @@ -69,7 +70,8 @@ export class AppsController { @User() user, @Param('slug') appSlug: string, @Query('access_type') accessType: string, - @Query('version_name') versionName: string + @Query('version_name') versionName: string, + @Query('version_id') versionId: string ) { const app: App = await this.appsService.findAppWithIdOrSlug(appSlug); @@ -96,7 +98,7 @@ export class AppsController { slug, }; /* If the request comes from preview which needs version id */ - if (versionName) { + if (versionName || versionId) { if (!ability.can('fetchVersions', app)) { throw new ForbiddenException( JSON.stringify({ @@ -105,10 +107,14 @@ export class AppsController { ); } - const version = await this.appsService.findVersionFromName(versionName, id); + /* Adding backward compatibility for old URLs */ + const version = versionId + ? await this.appsService.findVersion(versionId) + : await this.appsService.findVersionFromName(versionName, id); if (!version) { throw new NotFoundException("Couldn't found app version. Please check the version name"); } + if (versionId) response['versionName'] = version.name; response['versionId'] = version.id; } return response; @@ -168,6 +174,10 @@ export class AppsController { } } + if (!app.currentVersionId) { + throw new NotImplementedException('App is not released yet'); + } + const { id, slug } = app; return { slug: slug,