mirror of
https://github.com/ToolJet/ToolJet
synced 2026-05-06 06:48:21 +00:00
Merge pull request #8112 from ToolJet/rebase/v2.23.0
Merge v2.23.0 to develop
This commit is contained in:
commit
4cbdbdd1d9
36 changed files with 535 additions and 237 deletions
2
.version
2
.version
|
|
@ -1 +1 @@
|
|||
2.22.3
|
||||
2.23.0
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
};
|
||||
|
|
@ -1 +1 @@
|
|||
2.22.3
|
||||
2.23.0
|
||||
|
|
|
|||
|
|
@ -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 {
|
|||
</PrivateRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path="/applications/:slug/versions/:versionId/:pageHandle?"
|
||||
element={
|
||||
<PrivateRoute>
|
||||
<Viewer switchDarkMode={this.switchDarkMode} darkMode={darkMode} />
|
||||
</PrivateRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path="/oauth2/authorize"
|
||||
|
|
@ -245,6 +256,11 @@ class AppComponent extends React.Component {
|
|||
/>
|
||||
)}
|
||||
<Route exact path="/" element={<Navigate to="/:workspaceId" />} />
|
||||
<Route
|
||||
exact
|
||||
path="/error/:errorType"
|
||||
element={<ErrorPage switchDarkMode={this.switchDarkMode} darkMode={darkMode} />}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path="/switch-workspace"
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ function InfoOrErrorBox({ active, message, isError, isWarning, darkMode, additio
|
|||
width: '200px',
|
||||
height: '32px',
|
||||
borderRadius: '6px',
|
||||
border: `1px solid ${darkMode ? 'var(--dark-border-color, #2D3748)' : 'var(--light-border-color, #FFF0EE)'}`,
|
||||
border: `1px solid ${darkMode ? 'var(--dark-border-color, #2D3748)' : 'var(--light-border-color, #E0E0E0)'}`,
|
||||
background: darkMode ? 'var(--dark-bg-01, #1E293B)' : 'var(--base-white-00, #FFF)',
|
||||
boxShadow: '0px 1px 2px 0px rgba(16, 24, 40, 0.05)',
|
||||
color: color,
|
||||
|
|
|
|||
|
|
@ -153,6 +153,7 @@ export default function EditorHeader({
|
|||
slug={slug}
|
||||
darkMode={darkMode}
|
||||
handleSlugChange={handleSlugChange}
|
||||
isVersionReleased={isVersionReleased}
|
||||
pageHandle={currentState?.page?.handle}
|
||||
M={M}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<div className="inspector">
|
||||
<ConfirmDialog
|
||||
|
|
|
|||
|
|
@ -259,7 +259,7 @@ const LeftSidebarDataSourcesContainer = ({ darkMode, RenderDataSource, dataSourc
|
|||
<div className="d-flex flex-column w-100">
|
||||
{dataSources.length ? (
|
||||
<>
|
||||
<div className="tj-text-sm my-2 datasources-category">Local Data Sources</div>
|
||||
<div className="tj-text-sm my-2 datasources-category">Local Data sources</div>
|
||||
<div className="mt-2 w-100 color-slate12" data-cy="datasource-Label">
|
||||
{dataSources?.map((source, idx) => (
|
||||
<RenderDataSource
|
||||
|
|
|
|||
|
|
@ -10,6 +10,9 @@ import { Link } from 'react-router-dom';
|
|||
import { getPrivateRoute, replaceEditorURL, getHostURL } from '@/_helpers/routes';
|
||||
import { validateName } from '@/_helpers/utils';
|
||||
import SolidIcon from '@/_ui/Icon/SolidIcons';
|
||||
import cx from 'classnames';
|
||||
import { ToolTip } from '@/_components/ToolTip';
|
||||
import { TOOLTIP_MESSAGES } from '@/_helpers/constants';
|
||||
|
||||
class ManageAppUsersComponent extends React.Component {
|
||||
constructor(props) {
|
||||
|
|
@ -192,107 +195,102 @@ class ManageAppUsersComponent extends React.Component {
|
|||
const shareableLink = appLink + (this.props.slug || appId);
|
||||
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>`;
|
||||
const shouldWeDisableShareModal = !this.props.isVersionReleased;
|
||||
|
||||
return (
|
||||
<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 });
|
||||
}}
|
||||
<ToolTip
|
||||
message={TOOLTIP_MESSAGES.SHARE_URL_UNAVAILABLE}
|
||||
placement={!this.props.isVersionReleased ? 'bottom' : 'left'}
|
||||
show={shouldWeDisableShareModal}
|
||||
>
|
||||
<div
|
||||
title={!shouldWeDisableShareModal ? 'Share' : ''}
|
||||
className="manage-app-users editor-header-icon tj-secondary-btn"
|
||||
data-cy="share-button-link"
|
||||
>
|
||||
<SolidIcon name="share" width="14" className="cursor-pointer" fill="#3E63DD" />
|
||||
</span>
|
||||
<Modal
|
||||
show={this.state.showModal}
|
||||
size="lg"
|
||||
backdrop="static"
|
||||
centered={true}
|
||||
keyboard={true}
|
||||
animation={false}
|
||||
onEscapeKeyDown={this.hideModal}
|
||||
className={`app-sharing-modal animation-fade ${this.props.darkMode ? 'dark-theme' : ''}`}
|
||||
contentClassName={this.props.darkMode ? 'dark-theme' : ''}
|
||||
>
|
||||
<Modal.Header>
|
||||
<Modal.Title data-cy="modal-header">{this.props.t('editor.share', 'Share')}</Modal.Title>
|
||||
<span onClick={this.hideModal} data-cy="modal-close-button">
|
||||
<SolidIcon name="remove" className="cursor-pointer" aria-label="Close" />
|
||||
</span>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
{isLoading ? (
|
||||
<div style={{ width: '100%' }} className="p-5">
|
||||
<Skeleton count={5} />
|
||||
</div>
|
||||
) : (
|
||||
<div class="shareable-link-container">
|
||||
<div className="make-public mb-3">
|
||||
<div className="form-check form-switch d-flex align-items-center">
|
||||
<input
|
||||
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 field-name" data-cy="make-public-app-label">
|
||||
{this.props.t('editor.shareModal.makeApplicationPublic', 'Make application public')}
|
||||
</span>
|
||||
</div>
|
||||
<span
|
||||
className={cx('d-flex', {
|
||||
'share-disabled': shouldWeDisableShareModal,
|
||||
})}
|
||||
onClick={() => {
|
||||
this.validateThePreExistingSlugs();
|
||||
!shouldWeDisableShareModal && this.setState({ showModal: true });
|
||||
}}
|
||||
>
|
||||
<SolidIcon name="share" width="14" className="cursor-pointer" fill="#3E63DD" />
|
||||
</span>
|
||||
<Modal
|
||||
show={this.state.showModal}
|
||||
size="lg"
|
||||
backdrop="static"
|
||||
centered={true}
|
||||
keyboard={true}
|
||||
animation={false}
|
||||
onEscapeKeyDown={this.hideModal}
|
||||
className={`app-sharing-modal animation-fade ${this.props.darkMode ? 'dark-theme' : ''}`}
|
||||
contentClassName={this.props.darkMode ? 'dark-theme' : ''}
|
||||
>
|
||||
<Modal.Header>
|
||||
<Modal.Title data-cy="modal-header">{this.props.t('editor.share', 'Share')}</Modal.Title>
|
||||
<span onClick={this.hideModal} data-cy="modal-close-button">
|
||||
<SolidIcon name="remove" className="cursor-pointer" aria-label="Close" />
|
||||
</span>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
{isLoading ? (
|
||||
<div style={{ width: '100%' }} className="p-5">
|
||||
<Skeleton count={5} />
|
||||
</div>
|
||||
|
||||
<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 applink-text flex-grow-1 slug-ellipsis" data-cy="app-link">
|
||||
{appLink}
|
||||
</span>
|
||||
<div className="input-with-icon">
|
||||
) : (
|
||||
<div class="shareable-link-container">
|
||||
<div className="make-public mb-3">
|
||||
<div className="form-check form-switch d-flex align-items-center">
|
||||
<input
|
||||
type="text"
|
||||
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"
|
||||
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"
|
||||
/>
|
||||
{isSlugVerificationInProgress && (
|
||||
<div className="icon-container">
|
||||
<div class="spinner-border text-secondary " role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<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="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 && (
|
||||
<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 applink-text flex-grow-1 slug-ellipsis" data-cy="app-link">
|
||||
{appLink}
|
||||
</span>
|
||||
<div className="input-with-icon">
|
||||
<input
|
||||
type="text"
|
||||
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 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"
|
||||
|
|
@ -303,51 +301,33 @@ class ManageAppUsersComponent extends React.Component {
|
|||
<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"
|
||||
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>
|
||||
</div>
|
||||
<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>
|
||||
</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')}>
|
||||
<span className="input-group-text">
|
||||
<CopyToClipboard text={shareableLink} onCopy={() => toast.success('Link copied to clipboard')}>
|
||||
<svg
|
||||
className="cursor-pointer"
|
||||
width="17"
|
||||
|
|
@ -367,27 +347,66 @@ class ManageAppUsersComponent extends React.Component {
|
|||
</svg>
|
||||
</CopyToClipboard>
|
||||
</span>
|
||||
</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>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</Modal.Body>
|
||||
{(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 className="manage-app-users-footer">
|
||||
{this.isUserAdmin && (
|
||||
<Link
|
||||
to={getPrivateRoute('workspace_settings')}
|
||||
target="_blank"
|
||||
className={`btn border-0 default-secondary-button float-right1`}
|
||||
data-cy="manage-users-button"
|
||||
>
|
||||
Manage users
|
||||
</Link>
|
||||
)}
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
</div>
|
||||
<Modal.Footer className="manage-app-users-footer">
|
||||
{this.isUserAdmin && (
|
||||
<Link
|
||||
to={getPrivateRoute('workspace_settings')}
|
||||
target="_blank"
|
||||
className={`btn border-0 default-secondary-button float-right1`}
|
||||
data-cy="manage-users-button"
|
||||
>
|
||||
Manage users
|
||||
</Link>
|
||||
)}
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
</div>
|
||||
</ToolTip>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -50,10 +50,10 @@ function DataSourcePicker({ dataSources, staticDataSources, darkMode, globalData
|
|||
return (
|
||||
<>
|
||||
<h4 className="w-100 text-center" data-cy={'label-select-datasource'} style={{ fontWeight: 500 }}>
|
||||
Connect to a Data Source
|
||||
Connect to a Data source
|
||||
</h4>
|
||||
<p className="mb-3" style={{ textAlign: 'center' }}>
|
||||
Select a Data Source to start creating a new query. To know more about queries in ToolJet, you can read our
|
||||
Select a Data source to start creating a new query. To know more about queries in ToolJet, you can read our
|
||||
|
||||
<a target="_blank" href="https://docs.tooljet.com/docs/app-builder/query-panel" rel="noreferrer">
|
||||
documentation
|
||||
|
|
@ -83,7 +83,7 @@ function DataSourcePicker({ dataSources, staticDataSources, darkMode, globalData
|
|||
</div>
|
||||
<div className="d-flex d-flex justify-content-between">
|
||||
<label className="form-label py-1" style={{ width: 'auto' }} data-cy={`label-avilable-ds`}>
|
||||
{`Available Data Sources ${!isEmpty(allUserDefinedSources) ? '(' + allUserDefinedSources.length + ')' : 0}`}
|
||||
{`Available Data sources ${!isEmpty(allUserDefinedSources) ? '(' + allUserDefinedSources.length + ')' : 0}`}
|
||||
</label>
|
||||
{admin && (
|
||||
<ButtonSolid
|
||||
|
|
@ -143,7 +143,7 @@ const EmptyDataSourceBanner = () => (
|
|||
<div className="me-2">
|
||||
<Information fill="var(--slate9)" />
|
||||
</div>
|
||||
<div>No data sources have been added yet.</div>
|
||||
<div>No Data sources have been added yet.</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ function DataSourceSelect({ isDisabled, selectRef, closePopup }) {
|
|||
<div>
|
||||
{index === 0 && (
|
||||
<div className="color-slate9 mb-2 pb-1" style={{ fontWeight: 500, marginTop: '-8px' }}>
|
||||
Data Sources
|
||||
Data sources
|
||||
</div>
|
||||
)}
|
||||
<DataSourceIcon source={sources?.[0]} height={16} />
|
||||
|
|
@ -246,7 +246,7 @@ const MenuList = ({ children, getStyles, innerRef, ...props }) => {
|
|||
{admin && (
|
||||
<div className="p-2 mt-2 border-slate3-top">
|
||||
<ButtonSolid variant="secondary" size="md" className="w-100" onClick={handleAddClick}>
|
||||
+ Add new data source
|
||||
+ Add new Data source
|
||||
</ButtonSolid>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
|
|
|||
|
|
@ -117,7 +117,7 @@ export const List = ({ updateSelectedDatasource }) => {
|
|||
{!showInput ? (
|
||||
<>
|
||||
<div className="datasources-info tj-text-xsm" data-cy="added-ds-label">
|
||||
Data Sources Added{' '}
|
||||
Data sources added{' '}
|
||||
{!isLoading && filteredData && filteredData.length > 0 && `(${filteredData.length})`}
|
||||
</div>
|
||||
<div
|
||||
|
|
@ -134,7 +134,7 @@ export const List = ({ updateSelectedDatasource }) => {
|
|||
<SearchBox
|
||||
width="248px"
|
||||
callBack={handleSearch}
|
||||
placeholder={'Search for Data Sources'}
|
||||
placeholder={'Search for Data sources'}
|
||||
customClass="tj-common-search-input"
|
||||
onClearCallback={handleClose}
|
||||
autoFocus={true}
|
||||
|
|
|
|||
|
|
@ -682,8 +682,8 @@ class ManageGroupPermissionResourcesComponent extends React.Component {
|
|||
{groupPermission.group == 'all_users' && (
|
||||
<div className="manage-group-users-info" data-cy="helper-text-all-user-included">
|
||||
<p className="tj-text-xsm">
|
||||
<SolidIcon name="information" fill="#3E63DD" /> All users include every users in the app.
|
||||
This list is not editable
|
||||
<SolidIcon name="information" fill="#3E63DD" /> All users within the workspace are included
|
||||
in this list. This list cannot be edited.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
94
frontend/src/_components/ErrorComponents/ErrorPage.jsx
Normal file
94
frontend/src/_components/ErrorComponents/ErrorPage.jsx
Normal file
|
|
@ -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 (
|
||||
<div style={{ display: 'flex', justifyContent: 'center' }}>
|
||||
<ErrorModal errorMsg={errorMsg} show={true} darkMode={darkMode} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const ErrorModal = ({ errorMsg, ...props }) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div className="custom-backdrop">
|
||||
<Modal
|
||||
{...props}
|
||||
className={`organization-switch-modal static-error-modal ${props.darkMode ? 'dark-mode' : ''}`}
|
||||
aria-labelledby="contained-modal-title-vcenter"
|
||||
centered
|
||||
>
|
||||
<Modal.Header>
|
||||
<svg width="64" height="64" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="64" height="64" rx="12" fill="#FFF0EE" />
|
||||
<g opacity="0.4">
|
||||
<path
|
||||
d="M32.8331 15.3333H31.1664C30.2459 15.3333 29.4998 16.0795 29.4998 17V47C29.4998 47.9205 30.2459 48.6666 31.1664 48.6666H32.8331C33.7536 48.6666 34.4998 47.9205 34.4998 47V17C34.4998 16.0795 33.7536 15.3333 32.8331 15.3333Z"
|
||||
fill="#E54D2E"
|
||||
/>
|
||||
</g>
|
||||
<path
|
||||
d="M18.4432 31.1669L29.4998 24.7834L34.4998 21.8966L38.0526 19.8454C38.4354 19.6244 38.8904 19.5645 39.3173 19.6789L43.8707 20.8989C44.7599 21.1372 45.2875 22.0511 45.0493 22.9402L43.8292 27.4936C43.7148 27.9206 43.4354 28.2846 43.0526 28.5056L34.4998 33.4436L29.4998 36.3304L23.4432 39.8271C22.6461 40.2873 21.6268 40.0142 21.1665 39.2171L17.8332 33.4436C17.373 32.6464 17.6461 31.6271 18.4432 31.1669Z"
|
||||
fill="#E54D2E"
|
||||
/>
|
||||
</svg>
|
||||
<span className="header-text">{t('globals.static-error-modal.title', errorMsg?.title)}</span>
|
||||
<p className="description">{t('globals.static-error-modal.description', errorMsg?.message)}</p>
|
||||
</Modal.Header>
|
||||
<Modal.Footer>
|
||||
{errorMsg?.retry && (
|
||||
<button
|
||||
className="btn btn-primary action-btn"
|
||||
style={{
|
||||
width: '315px',
|
||||
height: '40px',
|
||||
backgroundColor: '#F0F4FF',
|
||||
color: 'rgba(var(--tblr-btn-color), 1)',
|
||||
marginBottom: '5px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
onClick={() => window.history.back()}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="21"
|
||||
height="21"
|
||||
viewBox="0 0 21 21"
|
||||
fill="none"
|
||||
style={{ marginRight: '8px' }}
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M9.04781 4.74972C11.4683 4.12777 13.9525 5.09359 15.3103 7.06516L9.04781 4.74972ZM15.3103 7.06516H13.0002C12.54 7.06516 12.1669 7.43826 12.1669 7.8985C12.1669 8.35873 12.54 8.73183 13.0002 8.73183H16.6754C16.6882 8.73213 16.701 8.73213 16.7138 8.73183H17.1669C17.6271 8.73183 18.0002 8.35873 18.0002 7.8985V3.73183C18.0002 3.27159 17.6271 2.8985 17.1669 2.8985C16.7066 2.8985 16.3335 3.27159 16.3335 3.73183V5.65205C14.5274 3.42705 11.5358 2.38951 8.6328 3.13556L8.63262 3.1356C7.31403 3.47477 6.11263 4.16649 5.15728 5.13656C4.20193 6.10663 3.52867 7.31846 3.2097 8.64209C2.89073 9.96571 2.93808 11.3512 3.34669 12.65C3.75529 13.9487 4.50972 15.1118 5.52907 16.0143C6.54843 16.9169 7.79425 17.525 9.13292 17.7733C10.4716 18.0217 11.8526 17.9009 13.1279 17.4241C14.4032 16.9472 15.5246 16.1322 16.3718 15.0664C17.2191 14.0006 17.7603 12.7243 17.9373 11.3744C17.9971 10.918 17.6757 10.4996 17.2194 10.4397C16.7631 10.3799 16.3446 10.7013 16.2848 11.1576C16.1471 12.2076 15.7262 13.2003 15.0672 14.0292C14.4082 14.8582 13.536 15.4921 12.5441 15.863C11.5523 16.2339 10.4781 16.3278 9.43693 16.1346C8.39574 15.9415 7.42677 15.4685 6.63394 14.7665C5.84111 14.0645 5.25433 13.1599 4.93653 12.1498C4.61873 11.1396 4.5819 10.062 4.82998 9.03255C5.07807 8.00306 5.60172 7.06053 6.34477 6.30603C7.08778 5.55157 8.02213 5.01359 9.04763 4.74977"
|
||||
fill="rgba(var(--tblr-btn-color), 1)"
|
||||
/>
|
||||
</svg>
|
||||
{t('globals.workspace-modal.continue-btn', 'Retry')}
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
className={errorMsg?.retry ? 'btn btn-primary' : 'btn btn-primary action-btn'}
|
||||
onClick={() => redirectToDashboard()}
|
||||
>
|
||||
{t('globals.workspace-modal.continue-btn', errorMsg?.cta)}
|
||||
</button>
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
1
frontend/src/_components/ErrorComponents/index.js
Normal file
1
frontend/src/_components/ErrorComponents/index.js
Normal file
|
|
@ -0,0 +1 @@
|
|||
export { ErrorPage } from './ErrorPage';
|
||||
30
frontend/src/_components/ErrorComponents/static-modal.scss
Normal file
30
frontend/src/_components/ErrorComponents/static-modal.scss
Normal file
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -9,7 +9,11 @@ export function ToolTip({
|
|||
placement = 'top',
|
||||
trigger = ['hover', 'focus'],
|
||||
delay = { show: 800, hide: 100 },
|
||||
show = true,
|
||||
}) {
|
||||
if (!show) {
|
||||
return children;
|
||||
}
|
||||
return (
|
||||
<OverlayTrigger trigger={trigger} placement={placement} delay={delay} overlay={<Tooltip>{message}</Tooltip>}>
|
||||
{children}
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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}` : ''}`;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 } },
|
||||
];
|
||||
|
|
|
|||
|
|
@ -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':
|
||||
|
|
|
|||
|
|
@ -91,7 +91,7 @@ function Layout({ children, switchDarkMode, darkMode }) {
|
|||
{/* DATASOURCES */}
|
||||
{admin && (
|
||||
<li className="text-center cursor-pointer">
|
||||
<ToolTip message="Data Sources" placement="right">
|
||||
<ToolTip message="Data sources" placement="right">
|
||||
<Link
|
||||
to={getPrivateRoute('data_sources')}
|
||||
onClick={(event) => checkForUnsavedChanges(getPrivateRoute('data_sources'), event)}
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
2.22.3
|
||||
2.23.0
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Reference in a new issue