From 522bab99cdd3a7c95da4f9ce3e20ef7d981029ff Mon Sep 17 00:00:00 2001 From: gsmithun4 Date: Tue, 17 Oct 2023 12:44:28 +0530 Subject: [PATCH 1/8] bump version --- .version | 2 +- frontend/.version | 2 +- server/.version | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.version b/.version index 83ecbf1d7a..db65e2167e 100644 --- a/.version +++ b/.version @@ -1 +1 @@ -2.20.2 +2.21.0 diff --git a/frontend/.version b/frontend/.version index 83ecbf1d7a..db65e2167e 100644 --- a/frontend/.version +++ b/frontend/.version @@ -1 +1 @@ -2.20.2 +2.21.0 diff --git a/server/.version b/server/.version index 83ecbf1d7a..db65e2167e 100644 --- a/server/.version +++ b/server/.version @@ -1 +1 @@ -2.20.2 +2.21.0 From 03e3fd950b02d62d3b6340beab30b2aa45959d82 Mon Sep 17 00:00:00 2001 From: Anantshree Chandola Date: Tue, 17 Oct 2023 13:18:18 +0530 Subject: [PATCH 2/8] New Improved App creation flow (#7209) * App creation flow * Add separate footer and divider * added create app dto, updated tests * update test * Update server/src/dto/app-create.dto.ts Co-authored-by: Midhun G S * Update server/src/dto/app-create.dto.ts Co-authored-by: Midhun G S * updates * Removed comments * small updates * rename app flow * Import App, Create App From Template, Clone App (BE+FE) * Edit app updates * remove comments * updates * updates * styling updates * handle spaces in app name * update * styling updates * Update permissions * updates * don't show toast failure message * Update frontend/src/Editor/Header/EditAppName.jsx Co-authored-by: Muhsin Shah C P * styling updates * Update server/src/controllers/app_import_export.controller.ts Co-authored-by: Muhsin Shah C P * remove comments * remove comments and small corrections * removed logs and deleted unwanted files * correct lint error * resolve failing tests + handled trimmed app names * resolve failing tests + handle trimmed app names * updates * duplicate imports removed * updates * Rebase corrections and updates * update * resolve failing e2e test * fix error * fix * length fix * fix --------- Co-authored-by: Midhun G S Co-authored-by: Muhsin Shah C P --- frontend/src/Editor/Header/EditAppName.jsx | 137 ++++++-- frontend/src/Editor/Header/InfoOrErrorBox.jsx | 38 +++ frontend/src/HomePage/AppCard.jsx | 8 +- frontend/src/HomePage/AppMenu.jsx | 12 +- frontend/src/HomePage/BlankPage.jsx | 44 +-- frontend/src/HomePage/HomePage.jsx | 298 +++++++++++++----- frontend/src/HomePage/Modal.jsx | 8 +- .../TemplateLibraryModal.jsx | 36 +-- frontend/src/_components/AppModal.jsx | 187 +++++++++++ frontend/src/_components/index.js | 1 + frontend/src/_helpers/utils.js | 16 + frontend/src/_services/app.service.js | 18 +- frontend/src/_services/library-app.service.js | 3 +- frontend/src/_styles/theme.scss | 13 + .../app_import_export.controller.ts | 6 +- server/src/controllers/apps.controller.ts | 15 +- .../controllers/library_apps.controller.ts | 7 +- server/src/dto/app-clone.dto.ts | 8 + server/src/dto/app-create.dto.ts | 12 + server/src/dto/app-import.dto.ts | 11 + server/src/dto/import-resources.dto.ts | 3 + .../src/services/app_import_export.service.ts | 36 ++- server/src/services/apps.service.ts | 71 ++--- .../import_export_resources.service.ts | 3 +- .../services/library_app_creation.service.ts | 4 +- server/test/controllers/apps.e2e-spec.ts | 34 +- .../test/controllers/library_apps.e2e-spec.ts | 4 +- .../app_import_export.service.spec.ts | 10 +- 28 files changed, 793 insertions(+), 250 deletions(-) create mode 100644 frontend/src/Editor/Header/InfoOrErrorBox.jsx create mode 100644 frontend/src/_components/AppModal.jsx create mode 100644 server/src/dto/app-clone.dto.ts create mode 100644 server/src/dto/app-create.dto.ts create mode 100644 server/src/dto/app-import.dto.ts diff --git a/frontend/src/Editor/Header/EditAppName.jsx b/frontend/src/Editor/Header/EditAppName.jsx index 35400201e9..4ece167329 100644 --- a/frontend/src/Editor/Header/EditAppName.jsx +++ b/frontend/src/Editor/Header/EditAppName.jsx @@ -1,57 +1,132 @@ -import React from 'react'; +import React, { useRef, useEffect, useState } from 'react'; import { ToolTip } from '@/_components'; import { appService } from '@/_services'; -import { handleHttpErrorMessages, validateName } from '../../_helpers/utils'; +import { handleHttpErrorMessages, validateAppName, validateName } from '@/_helpers/utils'; +import InfoOrErrorBox from './InfoOrErrorBox'; +import { toast } from 'react-hot-toast'; function EditAppName({ appId, appName = '', onNameChanged }) { const darkMode = localStorage.getItem('darkMode') === 'true'; - const [name, setName] = React.useState(appName); + const [name, setName] = useState(appName); + const [isValid, setIsValid] = useState(true); + const [isEditing, setIsEditing] = useState(false); + const [isError, setIsError] = useState(false); + const [errorMessage, setErrorMessage] = useState(''); + const [warningText, setWarningText] = useState(''); - React.useEffect(() => { + const inputRef = useRef(null); + + useEffect(() => { setName(appName); }, [appName]); - const saveAppName = async (name) => { - const newName = name.trim(); - if (!validateName(name, 'App name').status) { - return; - } - if (newName === appName) { - //will set back name without starting and ending spaces - setName(newName); - return; - } - await appService - .saveApp(appId, { name: newName }) - .then(() => { - onNameChanged(newName); - }) - .catch((error) => { - handleHttpErrorMessages(error, 'app'); - }); + const clearError = () => { + setIsError(false); + setErrorMessage(''); }; + const setError = (message) => { + setIsError(true); + setErrorMessage(message); + }; + + const saveAppName = async (newName) => { + const trimmedName = newName.trim(); + if (validateName(trimmedName, 'App name', true)?.errorMsg) { + setName(appName); + clearError(); + setIsEditing(false); + return; + } + + if (trimmedName === appName) { + setIsValid(true); + setIsEditing(false); + setName(appName); + return; + } + + try { + await appService.saveApp(appId, { name: trimmedName }); + onNameChanged(trimmedName); + setIsValid(true); + setIsEditing(false); + toast.success('App name successfully updated!'); + } catch (error) { + if (error.statusCode === 409) { + setError('App name already exists'); + } else { + clearError(); + setName(appName); + setIsEditing(false); + handleHttpErrorMessages(error, 'app'); + } + } + }; + + const handleBlur = () => { + saveAppName(name); + }; + + const handleFocus = () => { + setIsValid(true); + setIsEditing(true); + }; + + const handleInput = (e) => { + const newValue = e.target.value; + setName(newValue); + if (newValue.length >= 50) { + setWarningText('Maximum length has been reached'); + } else { + setWarningText(''); + clearError(); + } + }; + + const borderColor = isError + ? 'var(--light-tomato-10, #DB4324)' // Apply error border color + : darkMode + ? 'var(--dark-border-color, #2D3748)' // Change this to the appropriate dark border color + : 'var(--light-border-color, #FFF0EE)'; + return ( - -
+
+ { + onChange={() => { //this was quick fix. replace this with actual tooltip props and state later if (document.getElementsByClassName('tooltip').length) { document.getElementsByClassName('tooltip')[0].style.display = 'none'; } - validateName(e.target.value, 'App name', true); - setName(e.target.value); }} - onBlur={(e) => saveAppName(e.target.value)} - className="form-control-plaintext form-control-plaintext-sm" + onInput={handleInput} + onBlur={handleBlur} + onFocus={handleFocus} + onClick={() => { + inputRef.current.select(); + setIsEditing(true); + }} + className={`form-control-plaintext form-control-plaintext-sm ${ + (!isError && !isEditing) || isValid ? '' : 'is-invalid' + } ${isError ? 'error' : ''}`} // Add the 'error' class when there's an error + style={{ border: `1px solid ${borderColor}` }} value={name} maxLength={50} data-cy="app-name-input" /> -
- + + +
); } diff --git a/frontend/src/Editor/Header/InfoOrErrorBox.jsx b/frontend/src/Editor/Header/InfoOrErrorBox.jsx new file mode 100644 index 0000000000..755e20630a --- /dev/null +++ b/frontend/src/Editor/Header/InfoOrErrorBox.jsx @@ -0,0 +1,38 @@ +import React from 'react'; + +function InfoOrErrorBox({ active, message, isError, isWarning, darkMode, additionalClassName }) { + const color = isError ? 'var(--light-tomato-10, #DB4324)' : isWarning ? '#ED5F00' : 'var(--slate-light-10, #7E868C)'; + const boxStyle = { + display: active ? 'flex' : 'none', + flexDirection: 'column', + justifyContent: 'center', + alignItems: 'flex-start', + gap: '2px', + width: '200px', + height: '32px', + borderRadius: '6px', + border: `1px solid ${darkMode ? 'var(--dark-border-color, #2D3748)' : 'var(--light-border-color, #FFF0EE)'}`, + 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, + zIndex: 10000, + position: 'absolute', + fontFamily: 'IBM Plex Sans', + fontSize: '10px', + fontStyle: 'normal', + fontWeight: 500, + lineHeight: '16px', + padding: '2px 8px', + ...(additionalClassName && { + ...additionalClassName.split(' ').reduce((acc, cls) => ({ ...acc, [cls]: true }), {}), + }), + }; + + return ( +
+ {message &&
{message}
} +
+ ); +} + +export default InfoOrErrorBox; diff --git a/frontend/src/HomePage/AppCard.jsx b/frontend/src/HomePage/AppCard.jsx index b0612991e5..79cd2b88e6 100644 --- a/frontend/src/HomePage/AppCard.jsx +++ b/frontend/src/HomePage/AppCard.jsx @@ -2,7 +2,7 @@ import React, { useState, useCallback, useEffect } from 'react'; import cx from 'classnames'; import { AppMenu } from './AppMenu'; import moment from 'moment'; -import { ToolTip } from '@/_components'; +import { ToolTip } from '@/_components/index'; import useHover from '@/_hooks/useHover'; import configs from './Configs/AppIcon.json'; import { Link, useNavigate } from 'react-router-dom'; @@ -66,18 +66,18 @@ export default function AppCard({ return (
-
+
-
+
{AppIcon && AppIcon}
- {(canCreateApp(app) || canDeleteApp(app)) && ( + {(canCreateApp(app) || canDeleteApp(app) || canUpdateApp(app)) && (
+ {canUpdateApp && ( + openAppActionModal('rename-app')} + /> + )} {canUpdateApp && ( openAppActionModal('remove-app-from-folder')} /> )} - + openAppActionModal('clone-app')} + /> )} diff --git a/frontend/src/HomePage/BlankPage.jsx b/frontend/src/HomePage/BlankPage.jsx index 433da87e99..4be5433ea8 100644 --- a/frontend/src/HomePage/BlankPage.jsx +++ b/frontend/src/HomePage/BlankPage.jsx @@ -1,20 +1,18 @@ import React, { useState } from 'react'; -import { toast } from 'react-hot-toast'; import TemplateLibraryModal from './TemplateLibraryModal/'; import { useTranslation } from 'react-i18next'; -import { libraryAppService } from '@/_services'; import EmptyIllustration from '@assets/images/no-apps.svg'; import { ButtonSolid } from '@/_ui/AppButton/AppButton'; -import { getWorkspaceId } from '../_helpers/utils'; import { useNavigate } from 'react-router-dom'; export const BlankPage = function BlankPage({ - createApp, - darkMode, - creatingApp, - handleImportApp, + readAndImport, isImportingApp, fileInput, + openCreateAppModal, + openCreateAppFromTemplateModal, + creatingApp, + darkMode, showTemplateLibraryModal, hideTemplateLibraryModal, viewTemplateLibraryModal, @@ -29,26 +27,6 @@ export const BlankPage = function BlankPage({ { id: 'whatsapp-and-sms-crm', name: 'Whatsapp and sms crm' }, ]; - function deployApp(id) { - if (!deploying) { - const loadingToastId = toast.loading('Deploying app...'); - setDeploying(true); - libraryAppService - .deploy(id) - .then((data) => { - setDeploying(false); - toast.dismiss(loadingToastId); - toast.success('App created.'); - navigate(`/${getWorkspaceId()}/apps/${data.id}`); - }) - .catch((e) => { - toast.dismiss(loadingToastId); - toast.error(e.error); - setDeploying(false); - }); - } - } - return (
@@ -70,7 +48,7 @@ export const BlankPage = function BlankPage({
{staticTemplates.map(({ id, name }) => { return ( -
deployApp(id)}> +
{ + openCreateAppFromTemplateModal({ id, name }); + }} + >
{ + createApp = async (appName) => { let _self = this; _self.setState({ creatingApp: true }); - appService - .createApp({ icon: sample(iconList) }) - .then((data) => { - const workspaceId = getWorkspaceId(); - _self.props.navigate(`/${workspaceId}/apps/${data.id}`); - }) - .catch(({ error }) => { - toast.error(error); + try { + const data = await appService.createApp({ icon: sample(iconList), name: appName }); + + const workspaceId = getWorkspaceId(); + _self.props.navigate(`/${workspaceId}/apps/${data.id}`); + toast.success('App created successfully!'); + return true; + } catch (errorResponse) { + if (errorResponse.statusCode === 409) { _self.setState({ creatingApp: false }); - }); + return false; + } else { + throw errorResponse; + } + } + }; + + renameApp = async (newAppName, appId) => { + let _self = this; + _self.setState({ renamingApp: true }); + try { + await appService.saveApp(appId, { name: newAppName }); + await this.fetchApps(); + toast.success('App name has been updated!'); + return true; + } catch (errorResponse) { + if (errorResponse.statusCode === 409) { + console.log(errorResponse); + _self.setState({ renamingApp: false }); + return false; + } else { + throw errorResponse; + } + } }; deleteApp = (app) => { this.setState({ showAppDeletionConfirmation: true, appToBeDeleted: app }); }; - cloneApp = (app) => { + cloneApp = async (appId, appName) => { this.setState({ isCloningApp: true }); - appService - .cloneResource({ app: [{ id: app.id }], organization_id: getWorkspaceId() }) - .then((data) => { - toast.success('App cloned successfully.'); - this.setState({ isCloningApp: false }); - this.props.navigate(`/${getWorkspaceId()}/apps/${data.imports.app[0].id}`); - }) - .catch(({ _error }) => { - toast.error('Could not clone the app.'); - this.setState({ isCloningApp: false }); - console.log(_error); - }); + try { + const data = await appService.cloneApp(appName, appId); + toast.success('App cloned successfully!'); + this.setState({ isCloningApp: false }); + this.props.navigate(`/${getWorkspaceId()}/apps/${data.id}`); + return true; + } catch (_error) { + this.setState({ isCloningApp: false }); + if (_error.statusCode === 409) { + return false; + } else { + throw _error; + } + } }; exportApp = async (app) => { this.setState({ isExportingApp: true, app: app }); }; - handleImportApp = (event) => { - const fileReader = new FileReader(); - fileReader.readAsText(event.target.files[0], 'UTF-8'); - fileReader.onload = (event) => { - const fileContent = event.target.result; - this.setState({ isImportingApp: true }); - try { - const organization_id = getWorkspaceId(); - let importJSON = JSON.parse(fileContent); - // For backward compatibility with legacy app import - const isLegacyImport = isEmpty(importJSON.tooljet_version); - if (isLegacyImport) { - importJSON = { app: [{ definition: importJSON }], tooljet_version: importJSON.tooljetVersion }; - } - const requestBody = { organization_id, ...importJSON }; - appService - .importResource(requestBody) - .then((data) => { - toast.success('Imported successfully.'); - this.setState({ - isImportingApp: false, - }); - if (!isEmpty(data.imports.app)) { - this.props.navigate(`/${getWorkspaceId()}/apps/${data.imports.app[0].id}`); - } else if (!isEmpty(data.imports.tooljet_database)) { - this.props.navigate(`/${getWorkspaceId()}/database`); - } - }) - .catch(({ error }) => { - toast.error(`Could not import: ${error}`); - this.setState({ - isImportingApp: false, - }); - }); - } catch (error) { - toast.error(`Could not import: ${error}`); - this.setState({ - isImportingApp: false, - }); - } - // set file input as null to handle same file upload + readAndImport = (event) => { + try { + const file = event.target.files[0]; + if (!file) return; + + const fileReader = new FileReader(); + const fileName = file.name.replace('.json', '').substring(0, 50); + fileReader.readAsText(file, 'UTF-8'); + fileReader.onload = (event) => { + const result = event.target.result; + const fileContent = JSON.parse(result); + this.setState({ fileContent, fileName, showImportAppModal: true }); + }; + fileReader.onerror = (error) => { + throw new Error(`Could not import the app: ${error}`); + }; event.target.value = null; - }; + } catch (error) { + toast.error(error.message); + } + }; + + importFile = async (importJSON, appName) => { + this.setState({ isImportingApp: true }); + // For backward compatibility with legacy app import + const organization_id = getWorkspaceId(); + const isLegacyImport = isEmpty(importJSON.tooljet_version); + if (isLegacyImport) { + importJSON = { app: [{ definition: importJSON }], tooljet_version: importJSON.tooljetVersion }; + } + const requestBody = { organization_id, appName, ...importJSON }; + try { + const data = await appService.importResource(requestBody); + toast.success('App imported successfully.'); + this.setState({ + isImportingApp: false, + }); + if (!isEmpty(data.imports.app)) { + this.props.navigate(`/${getWorkspaceId()}/apps/${data.imports.app[0].id}`); + } else if (!isEmpty(data.imports.tooljet_database)) { + this.props.navigate(`/${getWorkspaceId()}/database`); + } + } catch (error) { + this.setState({ + isImportingApp: false, + }); + if (error.statusCode === 409) { + return false; + } + } + }; + + deployApp = async (event, appName, selectedApp) => { + event.preventDefault(); + const id = selectedApp.id; + this.setState({ deploying: true }); + try { + const data = await libraryAppService.deploy(id, appName); + this.setState({ deploying: false }); + toast.success('App created successfully!', { position: 'top-center' }); + this.props.navigate(`/${getWorkspaceId()}/apps/${data.app[0].id}`); + } catch (e) { + this.setState({ deploying: false }); + if (e.statusCode === 409) { + return false; + } else { + return e; + } + } }; canUserPerform(user, action, app) { @@ -387,6 +448,18 @@ class HomePageComponent extends React.Component { showRemoveAppFromFolderConfirmation: true, }); break; + case 'clone-app': + this.setState({ + appOperations: { ...appOperations, selectedApp: app, selectedIcon: app?.icon }, + showCloneAppModal: true, + }); + break; + case 'rename-app': + this.setState({ + appOperations: { ...appOperations, selectedApp: app }, + showRenameAppModal: true, + }); + break; } }; @@ -442,6 +515,22 @@ class HomePageComponent extends React.Component { this.setState({ showTemplateLibraryModal: false }); }; + openCreateAppFromTemplateModal = (template) => { + this.setState({ showCreateAppFromTemplateModal: true, selectedTemplate: template }); + }; + + closeCreateAppFromTemplateModal = () => { + this.setState({ showCreateAppFromTemplateModal: false, selectedTemplate: null }); + }; + + openCreateAppModal = () => { + this.setState({ showCreateAppModal: true }); + }; + + closeCreateAppModal = () => { + this.setState({ showCreateAppModal: false }); + }; + render() { const { apps, @@ -457,14 +546,78 @@ class HomePageComponent extends React.Component { appSearchKey, showAddToFolderModal, showChangeIconModal, + showCloneAppModal, appOperations, isExportingApp, appToBeDeleted, app, + showCreateAppModal, + showImportAppModal, + fileContent, + fileName, + showRenameAppModal, + showCreateAppFromTemplateModal, } = this.state; return (
+ {showCreateAppModal && ( + + )} + {showCloneAppModal && ( + this.setState({ showCloneAppModal: false })} + processApp={this.cloneApp} + show={() => this.setState({ showCloneAppModal: true })} + selectedAppId={appOperations?.selectedApp?.id} + selectedAppName={appOperations?.selectedApp?.name} + title={'Clone app'} + actionButton={'Clone app'} + actionLoadingButton={'Cloning'} + /> + )} + {showImportAppModal && ( + this.setState({ showImportAppModal: false })} + processApp={this.importFile} + fileContent={fileContent} + show={() => this.setState({ showImportAppModal: true })} + selectedAppName={fileName} + title={'Import app'} + actionButton={'Import app'} + actionLoadingButton={'Importing'} + /> + )} + {showCreateAppFromTemplateModal && ( + + )} + {showRenameAppModal && ( + this.setState({ showRenameAppModal: true })} + closeModal={() => this.setState({ showRenameAppModal: false })} + processApp={this.renameApp} + selectedAppId={appOperations.selectedApp.id} + selectedAppName={appOperations.selectedApp.name} + title={'Rename app'} + actionButton={'Rename app'} + actionLoadingButton={'Renaming'} + /> + )} this.cancelDeleteAppDialog()} darkMode={this.props.darkMode} /> -
diff --git a/frontend/src/HomePage/Modal.jsx b/frontend/src/HomePage/Modal.jsx index e752886a79..ce33b08d45 100644 --- a/frontend/src/HomePage/Modal.jsx +++ b/frontend/src/HomePage/Modal.jsx @@ -1,8 +1,13 @@ import React from 'react'; import { default as BootstrapModal } from 'react-bootstrap/Modal'; -export default function Modal({ title, show, closeModal, customClassName, children }) { +export default function Modal({ title, show, closeModal, customClassName, children, footerContent = null }) { const darkMode = localStorage.getItem('darkMode') === 'true'; + const modalFooter = footerContent ? ( + + {footerContent} + + ) : null; return ( closeModal(false)} @@ -31,6 +36,7 @@ export default function Modal({ title, show, closeModal, customClassName, childr > {children} + {modalFooter ? modalFooter : <>} ); } diff --git a/frontend/src/HomePage/TemplateLibraryModal/TemplateLibraryModal.jsx b/frontend/src/HomePage/TemplateLibraryModal/TemplateLibraryModal.jsx index 7ada977916..5151c670e1 100644 --- a/frontend/src/HomePage/TemplateLibraryModal/TemplateLibraryModal.jsx +++ b/frontend/src/HomePage/TemplateLibraryModal/TemplateLibraryModal.jsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from 'react'; +import React, { useState, useEffect } from 'react'; import { Modal, Container, Row, Col } from 'react-bootstrap'; import Categories from './Categories'; import AppList from './AppList'; @@ -25,6 +25,7 @@ export default function TemplateLibraryModal(props) { (app) => selectedCategory.id === 'all' || app.category === selectedCategory.id ); const [selectedApp, selectApp] = useState(undefined); + const [showCreateAppFromTemplateModal, setShowCreateAppFromTemplateModal] = useState(false); const { t } = useTranslation(); useEffect(() => { @@ -51,31 +52,10 @@ export default function TemplateLibraryModal(props) { const [deploying, setDeploying] = useState(false); - function deployApp(event) { - event.preventDefault(); - const id = selectedApp.id; - setDeploying(true); - libraryAppService - .deploy(id) - .then((data) => { - setDeploying(false); - props.onCloseButtonClick(); - toast.success('App created.', { - position: 'top-center', - }); - navigate(`/${getWorkspaceId()}/apps/${data.app[0].id}`); - }) - .catch((e) => { - toast.error(e.error, { - position: 'top-center', - }); - setDeploying(false); - }); - } - return ( { - deployApp(e); + onClick={() => { + props.openCreateAppFromTemplateModal(selectedApp); + setShowCreateAppFromTemplateModal(false); + props.onCloseButtonClick(); }} isLoading={deploying} - className=" ms-2 " + className="ms-2" > {t('homePage.templateLibraryModal.createAppfromTemplate', 'Create application from template')} diff --git a/frontend/src/_components/AppModal.jsx b/frontend/src/_components/AppModal.jsx new file mode 100644 index 0000000000..5e5ea64c2e --- /dev/null +++ b/frontend/src/_components/AppModal.jsx @@ -0,0 +1,187 @@ +import React, { useState, useEffect, useContext, useRef } from 'react'; +import { toast } from 'react-hot-toast'; +import Modal from '../HomePage/Modal'; +import { ButtonSolid } from '@/_ui/AppButton/AppButton'; +import _ from 'lodash'; +import { validateAppName } from '@/_helpers/utils'; + +export function AppModal({ + closeModal, + processApp, + show, + fileContent = null, + templateDetails = null, + selectedAppId = null, + selectedAppName = null, + title, + actionButton, + actionLoadingButton, +}) { + if (!selectedAppName && templateDetails) { + selectedAppName = templateDetails?.name || ''; + } else if (!selectedAppName) { + selectedAppName = ''; + } + + if (actionButton === 'Clone app') { + if (selectedAppName.length >= 45) { + selectedAppName = selectedAppName.slice(0, 45) + '_Copy'; + } else { + selectedAppName = selectedAppName + '_Copy'; + } + } + + const [deploying, setDeploying] = useState(false); + const [newAppName, setNewAppName] = useState(selectedAppName); + const [errorText, setErrorText] = useState(''); + const [infoText, setInfoText] = useState(''); + const [isLoading, setIsLoading] = useState(false); + const [isNameChanged, setIsNameChanged] = useState(false); + const [isSuccess, setIsSuccess] = useState(false); + const [clearInput, setClearInput] = useState(false); + const inputRef = useRef(null); + + useEffect(() => { + setIsNameChanged(newAppName?.trim() !== selectedAppName); + }, [newAppName, selectedAppName]); + + useEffect(() => { + setIsSuccess(false); + }, [show]); + + useEffect(() => { + inputRef.current?.select(); + }, [show]); + + useEffect(() => { + setIsSuccess(false); + setClearInput(false); + setNewAppName(selectedAppName); + }, [selectedAppName]); + + const handleAction = async (e) => { + setDeploying(true); + const trimmedAppName = newAppName.trim(); + setNewAppName(trimmedAppName); + if (!errorText) { + setIsLoading(true); + try { + let success = true; + //create app from template + if (templateDetails) { + success = await processApp(e, trimmedAppName, templateDetails); + //import app + } else if (fileContent) { + success = await processApp(fileContent, trimmedAppName); + //rename app/clone existing app + } else if (selectedAppId) { + success = await processApp(trimmedAppName, selectedAppId); + //create app from scratch + } else { + success = await processApp(trimmedAppName); + } + if (success === false) { + setErrorText('App name already exists'); + setInfoText(''); + } else { + setErrorText(''); + setInfoText(''); + closeModal(); + } + } catch (error) { + toast.error(e.error, { + position: 'top-center', + }); + } + } + setIsLoading(false); + }; + + const handleInputChange = (e) => { + const newAppName = e.target.value; + const trimmedName = newAppName.trim(); + setNewAppName(newAppName); + if (newAppName.length >= 50) { + setInfoText('Maximum length has been reached'); + } else { + setInfoText(''); + const error = validateAppName(trimmedName); + setErrorText(error?.errorMsg || ''); + } + }; + + const createBtnDisableState = + isLoading || + errorText || + (actionButton === 'Rename app' && (!isNameChanged || newAppName.trim().length === 0 || newAppName.length > 50)) || // For rename case + (actionButton !== 'Rename app' && (newAppName.length > 50 || newAppName.trim().length === 0)); + + return ( + + + Cancel + + handleAction(e)} data-cy={actionButton} disabled={createBtnDisableState}> + {isLoading ? actionLoadingButton : actionButton} + + + } + > +
+
+ + + {errorText ? ( + + {errorText} + + ) : infoText ? ( + + {infoText} + + ) : ( + + App name must be unique and max 50 characters + + )} +
+
+
+ ); +} diff --git a/frontend/src/_components/index.js b/frontend/src/_components/index.js index 9eee8a94bb..cc691843ba 100644 --- a/frontend/src/_components/index.js +++ b/frontend/src/_components/index.js @@ -4,6 +4,7 @@ export * from './ConfirmDialog'; export * from './DarkModeToggle'; export * from './SearchBox'; export * from './ToolTip'; +export * from './AppModal'; export * from './ImageWithSpinner'; export * from './Menu'; export * from './LoginLoader'; diff --git a/frontend/src/_helpers/utils.js b/frontend/src/_helpers/utils.js index 0233d1e96f..f3fb99001d 100644 --- a/frontend/src/_helpers/utils.js +++ b/frontend/src/_helpers/utils.js @@ -920,6 +920,22 @@ export function isExpectedDataType(data, expectedDataType) { return data; } +export const validateAppName = (name, showError = false) => { + const newName = name.trim(); + let errorMsg = ''; + if (newName.length > 50) { + errorMsg = `Maximum length has been reached`; + showError && + toast.error(errorMsg, { + id: '1', + }); + } + return { + status: !(errorMsg.length > 0), + errorMsg, + }; +}; + export const validateName = (name, nameType, showError = false, allowSpecialChars = true) => { const newName = name.trim(); let errorMsg = ''; diff --git a/frontend/src/_services/app.service.js b/frontend/src/_services/app.service.js index 0329954eac..a937abba5c 100644 --- a/frontend/src/_services/app.service.js +++ b/frontend/src/_services/app.service.js @@ -48,8 +48,13 @@ function createApp(body = {}) { return fetch(`${config.apiUrl}/apps`, requestOptions).then(handleResponse); } -function cloneApp(id) { - const requestOptions = { method: 'POST', headers: authHeader(), credentials: 'include' }; +function cloneApp(id, name) { + const requestOptions = { + method: 'POST', + headers: authHeader(), + credentials: 'include', + body: JSON.stringify({ name }), + }; return fetch(`${config.apiUrl}/apps/${id}/clone`, requestOptions).then(handleResponse); } @@ -97,8 +102,13 @@ function getVersions(id) { return fetch(`${config.apiUrl}/apps/${id}/versions`, requestOptions).then(handleResponse); } -function importApp(body) { - const requestOptions = { method: 'POST', headers: authHeader(), credentials: 'include', body: JSON.stringify(body) }; +function importApp(app, name) { + const requestOptions = { + method: 'POST', + headers: authHeader(), + credentials: 'include', + body: JSON.stringify({ app, name }), + }; return fetch(`${config.apiUrl}/apps/import`, requestOptions).then(handleResponse); } diff --git a/frontend/src/_services/library-app.service.js b/frontend/src/_services/library-app.service.js index 799e9cca92..4f60ae4b3a 100644 --- a/frontend/src/_services/library-app.service.js +++ b/frontend/src/_services/library-app.service.js @@ -6,9 +6,10 @@ export const libraryAppService = { templateManifests, }; -function deploy(identifier) { +function deploy(identifier, appName) { const body = { identifier, + appName, }; const requestOptions = { method: 'POST', headers: authHeader(), credentials: 'include', body: JSON.stringify(body) }; diff --git a/frontend/src/_styles/theme.scss b/frontend/src/_styles/theme.scss index ab62b43304..96e8ce13d2 100644 --- a/frontend/src/_styles/theme.scss +++ b/frontend/src/_styles/theme.scss @@ -10226,6 +10226,9 @@ tbody { border: 1px solid var(--indigo9) !important; box-shadow: none !important; } + &.input-error-border { + border-color: #DB4324 !important; + } &:-webkit-autofill { box-shadow: 0 0 0 1000px var(--base) inset !important; @@ -11298,6 +11301,16 @@ tbody { background-color: #F1F3F5; color: #C1C8CD; } +} + +.modal-divider { + border-top: 1px solid #dee2e6; + padding: 10px; +} + +.dark-theme-modal-divider { + border-top: 1px solid var(--slate5) !important; + padding: 10px; .nav-item { background-color: transparent !important; diff --git a/server/src/controllers/app_import_export.controller.ts b/server/src/controllers/app_import_export.controller.ts index 5269aa092a..4865706868 100644 --- a/server/src/controllers/app_import_export.controller.ts +++ b/server/src/controllers/app_import_export.controller.ts @@ -6,6 +6,7 @@ import { AppsAbilityFactory } from 'src/modules/casl/abilities/apps-ability.fact import { App } from 'src/entities/app.entity'; import { AppImportExportService } from '@services/app_import_export.service'; import { User } from 'src/decorators/user.decorator'; +import { AppImportDto } from '@dto/app-import.dto'; @Controller('apps') export class AppsImportExportController { @@ -17,13 +18,14 @@ export class AppsImportExportController { @UseGuards(JwtAuthGuard) @Post('/import') - async import(@User() user, @Body() body) { + async import(@User() user, @Body() appImportDto: AppImportDto) { const ability = await this.appsAbilityFactory.appsActions(user); if (!ability.can('createApp', App)) { throw new ForbiddenException('You do not have permissions to perform this action'); } - const app = await this.appImportExportService.import(user, body); + const { name: appName, app: appContent } = appImportDto; + const app = await this.appImportExportService.import(user, appContent, appName); return decamelizeKeys(app); } diff --git a/server/src/controllers/apps.controller.ts b/server/src/controllers/apps.controller.ts index 45873792d0..7564af0850 100644 --- a/server/src/controllers/apps.controller.ts +++ b/server/src/controllers/apps.controller.ts @@ -21,12 +21,14 @@ import { FoldersService } from '@services/folders.service'; import { App } from 'src/entities/app.entity'; import { User } from 'src/decorators/user.decorator'; import { AppUpdateDto } from '@dto/app-update.dto'; +import { AppCreateDto } from '@dto/app-create.dto'; import { VersionCreateDto } from '@dto/version-create.dto'; import { VersionEditDto } from '@dto/version-edit.dto'; import { dbTransactionWrap } from 'src/helpers/utils.helper'; import { EntityManager } from 'typeorm'; import { ValidAppInterceptor } from 'src/interceptors/valid.app.interceptor'; import { AppDecorator } from 'src/decorators/app.decorator'; +import { AppCloneDto } from '@dto/app-clone.dto'; @Controller('apps') export class AppsController { @@ -38,17 +40,20 @@ export class AppsController { @UseGuards(JwtAuthGuard) @Post() - async create(@User() user, @Body('icon') icon: string) { + async create(@User() user, @Body() appCreateDto: AppCreateDto) { const ability = await this.appsAbilityFactory.appsActions(user); + const name = appCreateDto.name; + const icon = appCreateDto.icon; if (!ability.can('createApp', App)) { throw new ForbiddenException('You do not have permissions to perform this action'); } return await dbTransactionWrap(async (manager: EntityManager) => { - const app = await this.appsService.create(user, manager); + const app = await this.appsService.create(name, user, manager); const appUpdateDto = new AppUpdateDto(); + appUpdateDto.name = name; appUpdateDto.slug = app.id; appUpdateDto.icon = icon; await this.appsService.update(app.id, appUpdateDto, manager); @@ -154,14 +159,14 @@ export class AppsController { @UseGuards(JwtAuthGuard) @UseInterceptors(ValidAppInterceptor) @Post(':id/clone') - async clone(@User() user, @AppDecorator() app: App) { + async clone(@User() user, @AppDecorator() app: App, @Body() appCloneDto: AppCloneDto) { const ability = await this.appsAbilityFactory.appsActions(user, app.id); if (!ability.can('cloneApp', app)) { throw new ForbiddenException('You do not have permissions to perform this action'); } - - const result = await this.appsService.clone(app, user); + const appName = appCloneDto.name; + const result = await this.appsService.clone(app, user, appName); const response = decamelizeKeys(result); return response; diff --git a/server/src/controllers/library_apps.controller.ts b/server/src/controllers/library_apps.controller.ts index 67c96f83d8..78a058ceef 100644 --- a/server/src/controllers/library_apps.controller.ts +++ b/server/src/controllers/library_apps.controller.ts @@ -15,14 +15,15 @@ export class LibraryAppsController { @Post() @UseGuards(JwtAuthGuard) - async create(@User() user, @Body('identifier') identifier) { + async create(@User() user, @Body('identifier') identifier, @Body('appName') appName) { const ability = await this.appsAbilityFactory.appsActions(user); if (!ability.can('createApp', App)) { throw new ForbiddenException('You do not have permissions to perform this action'); } - const result = await this.libraryAppCreationService.perform(user, identifier); - return result; + const newApp = await this.libraryAppCreationService.perform(user, identifier, appName); + + return newApp; } @Get() diff --git a/server/src/dto/app-clone.dto.ts b/server/src/dto/app-clone.dto.ts new file mode 100644 index 0000000000..139883d736 --- /dev/null +++ b/server/src/dto/app-clone.dto.ts @@ -0,0 +1,8 @@ +import { IsString, IsNotEmpty, MaxLength } from 'class-validator'; + +export class AppCloneDto { + @IsNotEmpty() + @IsString() + @MaxLength(50, { message: 'Maximum length has been reached.' }) + name: string; +} diff --git a/server/src/dto/app-create.dto.ts b/server/src/dto/app-create.dto.ts new file mode 100644 index 0000000000..e2b959838b --- /dev/null +++ b/server/src/dto/app-create.dto.ts @@ -0,0 +1,12 @@ +import { IsString, IsOptional, IsNotEmpty, MaxLength } from 'class-validator'; + +export class AppCreateDto { + @IsNotEmpty() + @IsString() + @MaxLength(50, { message: 'Maximum length has been reached.' }) + name: string; + + @IsOptional() + @IsString() + icon?: string; +} diff --git a/server/src/dto/app-import.dto.ts b/server/src/dto/app-import.dto.ts new file mode 100644 index 0000000000..85863c08e2 --- /dev/null +++ b/server/src/dto/app-import.dto.ts @@ -0,0 +1,11 @@ +import { IsString, IsNotEmpty, MaxLength } from 'class-validator'; + +export class AppImportDto { + @IsNotEmpty() + @IsString() + @MaxLength(50, { message: 'Maximum length has been reached.' }) + name: string; + + @IsNotEmpty() + app: object; +} diff --git a/server/src/dto/import-resources.dto.ts b/server/src/dto/import-resources.dto.ts index 50cefcfb21..9d6257d4af 100644 --- a/server/src/dto/import-resources.dto.ts +++ b/server/src/dto/import-resources.dto.ts @@ -10,6 +10,9 @@ export class ImportResourcesDto { @IsOptional() app: ImportAppDto[]; + @IsOptional() + appName: string; + @IsOptional() tooljet_database: ImportTooljetDatabaseDto[]; } diff --git a/server/src/services/app_import_export.service.ts b/server/src/services/app_import_export.service.ts index f9e2f611c4..3d3066ba15 100644 --- a/server/src/services/app_import_export.service.ts +++ b/server/src/services/app_import_export.service.ts @@ -11,11 +11,12 @@ import { GroupPermission } from 'src/entities/group_permission.entity'; import { User } from 'src/entities/user.entity'; import { EntityManager } from 'typeorm'; import { DataSourcesService } from './data_sources.service'; -import { dbTransactionWrap, defaultAppEnvironments, truncateAndReplace } from 'src/helpers/utils.helper'; +import { dbTransactionWrap, defaultAppEnvironments, catchDbException } from 'src/helpers/utils.helper'; import { AppEnvironmentService } from './app_environments.service'; import { convertAppDefinitionFromSinglePageToMultiPage } from '../../lib/single-page-to-and-from-multipage-definition-conversion'; import { DataSourceScopes, DataSourceTypes } from 'src/helpers/data_source.constants'; import { Organization } from 'src/entities/organization.entity'; +import { DataBaseConstraints } from 'src/helpers/db_constraints.constants'; import { QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity'; import { Plugin } from 'src/entities/plugin.entity'; @@ -151,7 +152,7 @@ export class AppImportExportService { }); } - async import(user: User, appParamsObj: any, externalResourceMappings = {}): Promise { + async import(user: User, appParamsObj: any, appName: string, externalResourceMappings = {}): Promise { if (typeof appParamsObj !== 'object') { throw new BadRequestException('Invalid params for app import'); } @@ -171,6 +172,7 @@ export class AppImportExportService { const schemaUnifiedAppParams = appParams?.schemaDetails?.multiPages ? appParams : convertSinglePageSchemaToMultiPageSchema(appParams); + schemaUnifiedAppParams.name = appName; await dbTransactionWrap(async (manager) => { importedApp = await this.createImportedAppForUser(manager, schemaUnifiedAppParams, user); @@ -194,18 +196,24 @@ export class AppImportExportService { } async createImportedAppForUser(manager: EntityManager, appParams: any, user: User): Promise { - const importedApp = manager.create(App, { - name: truncateAndReplace(appParams.name), - organizationId: user.organizationId, - userId: user.id, - slug: null, // Prevent db unique constraint error. - icon: appParams.icon, - isPublic: false, - createdAt: new Date(), - updatedAt: new Date(), - }); - await manager.save(importedApp); - return importedApp; + return await catchDbException( + async () => { + const importedApp = manager.create(App, { + name: appParams.name, + organizationId: user.organizationId, + userId: user.id, + slug: null, + icon: appParams.icon, + isPublic: false, + createdAt: new Date(), + updatedAt: new Date(), + }); + await manager.save(importedApp); + return importedApp; + }, + DataBaseConstraints.APP_NAME_UNIQUE, + 'This app name is already taken.' + ); } extractImportDataFromAppParams(appParams: Record): { diff --git a/server/src/services/apps.service.ts b/server/src/services/apps.service.ts index a55a90cd72..8996fb368d 100644 --- a/server/src/services/apps.service.ts +++ b/server/src/services/apps.service.ts @@ -12,13 +12,7 @@ import { AppGroupPermission } from 'src/entities/app_group_permission.entity'; import { AppImportExportService } from './app_import_export.service'; import { DataSourcesService } from './data_sources.service'; import { Credential } from 'src/entities/credential.entity'; -import { - catchDbException, - cleanObject, - dbTransactionWrap, - defaultAppEnvironments, - generateNextName, -} from 'src/helpers/utils.helper'; +import { catchDbException, cleanObject, dbTransactionWrap, defaultAppEnvironments } from 'src/helpers/utils.helper'; import { AppUpdateDto } from '@dto/app-update.dto'; import { viewableAppsQuery } from 'src/helpers/queries'; import { VersionEditDto } from '@dto/version-edit.dto'; @@ -103,35 +97,40 @@ export class AppsService { }); } - async create(user: User, manager: EntityManager): Promise { + async create(name: string, user: User, manager: EntityManager): Promise { return await dbTransactionWrap(async (manager: EntityManager) => { - const name = await generateNextName('My app'); - const app = await manager.save( - manager.create(App, { - name, - createdAt: new Date(), - updatedAt: new Date(), - organizationId: user.organizationId, - userId: user.id, - }) + return await catchDbException( + async () => { + const app = await manager.save( + manager.create(App, { + name, + createdAt: new Date(), + updatedAt: new Date(), + organizationId: user.organizationId, + userId: user.id, + }) + ); + + //create default app version + await this.createVersion(user, app, 'v1', null, null, manager); + + await manager.save( + manager.create(AppUser, { + userId: user.id, + appId: app.id, + role: 'admin', + createdAt: new Date(), + updatedAt: new Date(), + }) + ); + + await this.createAppGroupPermissionsForAdmin(app, manager); + return app; + }, + DataBaseConstraints.APP_NAME_UNIQUE, + 'This app name is already taken.' ); - - //create default app version - await this.createVersion(user, app, 'v1', null, null, manager); - - await manager.save( - manager.create(AppUser, { - userId: user.id, - appId: app.id, - role: 'admin', - createdAt: new Date(), - updatedAt: new Date(), - }) - ); - - await this.createAppGroupPermissionsForAdmin(app, manager); - return app; - }, manager); + }); } async createAppGroupPermissionsForAdmin(app: App, manager: EntityManager): Promise { @@ -170,9 +169,9 @@ export class AppsService { } } - async clone(existingApp: App, user: User): Promise { + async clone(existingApp: App, user: User, appName: string): Promise { const appWithRelations = await this.appImportExportService.export(user, existingApp.id); - const clonedApp = await this.appImportExportService.import(user, appWithRelations); + const clonedApp = await this.appImportExportService.import(user, appWithRelations, appName); return clonedApp; } diff --git a/server/src/services/import_export_resources.service.ts b/server/src/services/import_export_resources.service.ts index 2ca413c15e..fce758c0cf 100644 --- a/server/src/services/import_export_resources.service.ts +++ b/server/src/services/import_export_resources.service.ts @@ -59,9 +59,10 @@ export class ImportExportResourcesService { } if (importResourcesDto.app) { + const appName = importResourcesDto.appName; for (const appImportDto of importResourcesDto.app) { user.organizationId = importResourcesDto.organization_id; - const createdApp = await this.appImportExportService.import(user, appImportDto.definition, { + const createdApp = await this.appImportExportService.import(user, appImportDto.definition, appName, { tooljet_database: tableNameMapping, }); imports.app.push({ id: createdApp.id, name: createdApp.name }); diff --git a/server/src/services/library_app_creation.service.ts b/server/src/services/library_app_creation.service.ts index 49a231418b..1830c12883 100644 --- a/server/src/services/library_app_creation.service.ts +++ b/server/src/services/library_app_creation.service.ts @@ -14,7 +14,7 @@ export class LibraryAppCreationService { private readonly logger: Logger ) {} - async perform(currentUser: User, identifier: string) { + async perform(currentUser: User, identifier: string, appName: string) { const templateDefinition = this.findTemplateDefinition(identifier); const importDto = new ImportResourcesDto(); importDto.organization_id = currentUser.organizationId; @@ -24,7 +24,7 @@ export class LibraryAppCreationService { if (this.isVersionGreaterThanOrEqual(templateDefinition.tooljet_version, '2.16.0')) { return await this.importExportResourcesService.import(currentUser, importDto); } else { - const importedApp = await this.appImportExportService.import(currentUser, templateDefinition); + const importedApp = await this.appImportExportService.import(currentUser, templateDefinition, appName); return { app: [importedApp], tooljet_database: [], diff --git a/server/test/controllers/apps.e2e-spec.ts b/server/test/controllers/apps.e2e-spec.ts index 754d54d93a..278654f13d 100644 --- a/server/test/controllers/apps.e2e-spec.ts +++ b/server/test/controllers/apps.e2e-spec.ts @@ -82,11 +82,15 @@ describe('apps controller', () => { }); await createApplicationVersion(app, application); + const appName = 'My app'; for (const userData of [viewerUserData, developerUserData]) { const response = await request(app.getHttpServer()) .post(`/api/apps`) .set('tj-workspace-id', userData.user.defaultOrganizationId) - .set('Cookie', userData['tokenCookie']); + .set('Cookie', userData['tokenCookie']) + .send({ + name: appName, + }); expect(response.statusCode).toBe(403); } @@ -94,7 +98,10 @@ describe('apps controller', () => { const response = await request(app.getHttpServer()) .post(`/api/apps`) .set('tj-workspace-id', adminUserData.user.defaultOrganizationId) - .set('Cookie', adminUserData['tokenCookie']); + .set('Cookie', adminUserData['tokenCookie']) + .send({ + name: appName, + }); expect(response.statusCode).toBe(201); expect(response.body.name).toContain('My app'); @@ -115,10 +122,14 @@ describe('apps controller', () => { await createAppEnvironments(app, adminUserData.organization.id); + const appName = 'My app'; const response = await request(app.getHttpServer()) .post(`/api/apps`) .set('tj-workspace-id', adminUserData.user.defaultOrganizationId) - .set('Cookie', loggedUser.tokenCookie); + .set('Cookie', loggedUser.tokenCookie) + .send({ + name: appName, + }); expect(response.statusCode).toBe(201); expect(response.body.name).toContain('My app'); @@ -535,7 +546,8 @@ describe('apps controller', () => { let response = await request(app.getHttpServer()) .post(`/api/apps/${application.id}/clone`) .set('tj-workspace-id', adminUserData.user.defaultOrganizationId) - .set('Cookie', adminUserData['tokenCookie']); + .set('Cookie', adminUserData['tokenCookie']) + .send({ name: 'App to clone_Copy' }); expect(response.statusCode).toBe(201); @@ -546,14 +558,16 @@ describe('apps controller', () => { response = await request(app.getHttpServer()) .post(`/api/apps/${application.id}/clone`) .set('tj-workspace-id', developerUserData.user.defaultOrganizationId) - .set('Cookie', developerUserData['tokenCookie']); + .set('Cookie', developerUserData['tokenCookie']) + .send({ name: 'App to clone_Copy' }); expect(response.statusCode).toBe(403); response = await request(app.getHttpServer()) .post(`/api/apps/${application.id}/clone`) .set('tj-workspace-id', viewerUserData.user.defaultOrganizationId) - .set('Cookie', viewerUserData['tokenCookie']); + .set('Cookie', viewerUserData['tokenCookie']) + .send({ name: 'App to clone_Copy' }); expect(response.statusCode).toBe(403); @@ -583,7 +597,8 @@ describe('apps controller', () => { const response = await request(app.getHttpServer()) .post(`/api/apps/${application.id}/clone`) .set('tj-workspace-id', anotherOrgAdminUserData.user.defaultOrganizationId) - .set('Cookie', loggedUser.tokenCookie); + .set('Cookie', loggedUser.tokenCookie) + .send({ name: 'name_Copy' }); expect(response.statusCode).toBe(403); @@ -2121,7 +2136,8 @@ describe('apps controller', () => { const response = await request(app.getHttpServer()) .post('/api/apps/import') .set('tj-workspace-id', userData.user.defaultOrganizationId) - .set('Cookie', userData['tokenCookie']); + .set('Cookie', userData['tokenCookie']) + .send({ app: application, name: 'name' }); expect(response.statusCode).toBe(403); } @@ -2130,7 +2146,7 @@ describe('apps controller', () => { .post('/api/apps/import') .set('tj-workspace-id', adminUserData.user.defaultOrganizationId) .set('Cookie', adminUserData['tokenCookie']) - .send({ name: 'Imported App' }); + .send({ app: application, name: 'Imported App' }); expect(response.statusCode).toBe(201); diff --git a/server/test/controllers/library_apps.e2e-spec.ts b/server/test/controllers/library_apps.e2e-spec.ts index 64573b145a..95b02133e2 100644 --- a/server/test/controllers/library_apps.e2e-spec.ts +++ b/server/test/controllers/library_apps.e2e-spec.ts @@ -34,7 +34,7 @@ describe('library apps controller', () => { let response = await request(app.getHttpServer()) .post('/api/library_apps') - .send({ identifier: 'github-contributors' }) + .send({ identifier: 'github-contributors', appName: 'Github Contributors' }) .set('tj-workspace-id', nonAdminUserData.user.defaultOrganizationId) .set('Cookie', nonAdminUserData['tokenCookie']); @@ -61,7 +61,7 @@ describe('library apps controller', () => { const response = await request(app.getHttpServer()) .post('/api/library_apps') - .send({ identifier: 'non-existent-template' }) + .send({ identifier: 'non-existent-template', appName: 'Non existent template' }) .set('tj-workspace-id', adminUserData.user.defaultOrganizationId) .set('Cookie', adminUserData['tokenCookie']); diff --git a/server/test/services/app_import_export.service.spec.ts b/server/test/services/app_import_export.service.spec.ts index eea2c15bd0..a3e39ea8e9 100644 --- a/server/test/services/app_import_export.service.spec.ts +++ b/server/test/services/app_import_export.service.spec.ts @@ -164,7 +164,8 @@ describe('AppImportExportService', () => { groups: ['all_users', 'admin'], }); const adminUser = adminUserData.user; - await expect(service.import(adminUser, 'hello world')).rejects.toThrow('Invalid params for app import'); + const appName = 'my app'; + await expect(service.import(adminUser, 'hello world', appName)).rejects.toThrow('Invalid params for app import'); }); it('should import app with empty related associations', async () => { @@ -180,8 +181,8 @@ describe('AppImportExportService', () => { }); const { appV2: exportedApp } = await service.export(adminUser, app.id); - - const result = await service.import(adminUser, exportedApp); + const appName = 'my app'; + const result = await service.import(adminUser, exportedApp, appName); const importedApp = await getAppWithAllDetails(result.id); expect(importedApp.id == exportedApp.id).toBeFalsy(); @@ -261,7 +262,8 @@ describe('AppImportExportService', () => { }); const { appV2: exportedApp } = await service.export(adminUser, application.id); - const result = await service.import(adminUser, exportedApp); + const appName = 'my app'; + const result = await service.import(adminUser, exportedApp, appName); const importedApp = await getAppWithAllDetails(result.id); expect(importedApp.id == exportedApp.id).toBeFalsy(); From d4b436dd3157de7f78482725bb2f6cce5ea25870 Mon Sep 17 00:00:00 2001 From: Anantshree Chandola Date: Tue, 17 Oct 2023 14:58:57 +0530 Subject: [PATCH 3/8] fix failing test --- server/test/controllers/library_apps.e2e-spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/test/controllers/library_apps.e2e-spec.ts b/server/test/controllers/library_apps.e2e-spec.ts index 95b02133e2..34f4c77b06 100644 --- a/server/test/controllers/library_apps.e2e-spec.ts +++ b/server/test/controllers/library_apps.e2e-spec.ts @@ -42,7 +42,7 @@ describe('library apps controller', () => { response = await request(app.getHttpServer()) .post('/api/library_apps') - .send({ identifier: 'github-contributors' }) + .send({ identifier: 'github-contributors', appName: 'GitHub Contributor Leaderboard' }) .set('tj-workspace-id', adminUserData.user.defaultOrganizationId) .set('Cookie', adminUserData['tokenCookie']); From 82a8b096faf01935e747f352f6a07dca8711fe04 Mon Sep 17 00:00:00 2001 From: Anantshree Chandola Date: Tue, 17 Oct 2023 15:36:24 +0530 Subject: [PATCH 4/8] fix --- frontend/src/Editor/Header/EditAppName.jsx | 8 ++++++-- frontend/src/_components/AppModal.jsx | 4 ++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/frontend/src/Editor/Header/EditAppName.jsx b/frontend/src/Editor/Header/EditAppName.jsx index 4ece167329..097ae8aa31 100644 --- a/frontend/src/Editor/Header/EditAppName.jsx +++ b/frontend/src/Editor/Header/EditAppName.jsx @@ -120,8 +120,12 @@ function EditAppName({ appId, appName = '', onNameChanged }) { = 50 + ? 'Maximum length has been reached' + : 'App name should be unique and max 50 characters' + } + isWarning={warningText || name.length >= 50} isError={isError} darkMode={darkMode} additionalClassName={isError ? 'error' : ''} diff --git a/frontend/src/_components/AppModal.jsx b/frontend/src/_components/AppModal.jsx index 5e5ea64c2e..6ff81c6b79 100644 --- a/frontend/src/_components/AppModal.jsx +++ b/frontend/src/_components/AppModal.jsx @@ -159,7 +159,7 @@ export function AppModal({ > {errorText} - ) : infoText ? ( + ) : infoText || newAppName.length >= 50 ? ( - {infoText} + {infoText || 'Maximum length has been reached'} ) : ( Date: Tue, 17 Oct 2023 16:56:54 +0530 Subject: [PATCH 5/8] updates (#7931) --- frontend/src/HomePage/BlankPage.jsx | 78 +++++++++++-------- frontend/src/HomePage/HomePage.jsx | 2 + .../TemplateLibraryModal.jsx | 1 + 3 files changed, 48 insertions(+), 33 deletions(-) diff --git a/frontend/src/HomePage/BlankPage.jsx b/frontend/src/HomePage/BlankPage.jsx index 4be5433ea8..c7a8c54a25 100644 --- a/frontend/src/HomePage/BlankPage.jsx +++ b/frontend/src/HomePage/BlankPage.jsx @@ -16,6 +16,7 @@ export const BlankPage = function BlankPage({ showTemplateLibraryModal, hideTemplateLibraryModal, viewTemplateLibraryModal, + canCreateApp, }) { const { t } = useTranslation(); const [deploying, setDeploying] = useState(false); @@ -27,6 +28,8 @@ export const BlankPage = function BlankPage({ { id: 'whatsapp-and-sms-crm', name: 'Whatsapp and sms crm' }, ]; + const appCreationDisabled = !canCreateApp(); + return (
@@ -53,6 +56,7 @@ export const BlankPage = function BlankPage({ data-cy="button-new-app-from-scratch" className="col" fill={'#FDFDFE'} + disabled={appCreationDisabled} > Create new application @@ -63,12 +67,14 @@ export const BlankPage = function BlankPage({ isLoading={isImportingApp} data-cy="button-import-an-app" className="col" - variant="tertiary" + disabled={appCreationDisabled} + variant={!appCreationDisabled ? 'tertiary' : 'primary'} > @@ -86,41 +93,45 @@ export const BlankPage = function BlankPage({
-
- Or choose from templates -
-
- {staticTemplates.map(({ id, name }) => { - return ( -
{ - openCreateAppFromTemplateModal({ id, name }); - }} - > -
+ {!appCreationDisabled && ( +
+
+ Or choose from templates +
+
+ {staticTemplates.map(({ id, name }) => { + return (
-
-

{ + openCreateAppFromTemplateModal({ id, name }); + }} + > +
- {name} -

+
+
+

+ {name} +

+
+
-
-
- ); - })} -
+ ); + })} +
+
+ )}
); diff --git a/frontend/src/HomePage/HomePage.jsx b/frontend/src/HomePage/HomePage.jsx index 2b9a4c03b5..84b8130b8a 100644 --- a/frontend/src/HomePage/HomePage.jsx +++ b/frontend/src/HomePage/HomePage.jsx @@ -823,6 +823,7 @@ class HomePageComponent extends React.Component { showTemplateLibraryModal={this.state.showTemplateLibraryModal} viewTemplateLibraryModal={this.showTemplateLibraryModal} hideTemplateLibraryModal={this.hideTemplateLibraryModal} + canCreateApp={this.canCreateApp} /> )} {!isLoading && meta.total_count === 0 && appSearchKey && ( @@ -868,6 +869,7 @@ class HomePageComponent extends React.Component { onCloseButtonClick={() => this.setState({ showTemplateLibraryModal: false })} darkMode={this.props.darkMode} openCreateAppFromTemplateModal={this.openCreateAppFromTemplateModal} + appCreationDisabled={!this.canCreateApp()} />
diff --git a/frontend/src/HomePage/TemplateLibraryModal/TemplateLibraryModal.jsx b/frontend/src/HomePage/TemplateLibraryModal/TemplateLibraryModal.jsx index 5151c670e1..0773a98fc2 100644 --- a/frontend/src/HomePage/TemplateLibraryModal/TemplateLibraryModal.jsx +++ b/frontend/src/HomePage/TemplateLibraryModal/TemplateLibraryModal.jsx @@ -101,6 +101,7 @@ export default function TemplateLibraryModal(props) { }} isLoading={deploying} className="ms-2" + disabled={props.appCreationDisabled} > {t('homePage.templateLibraryModal.createAppfromTemplate', 'Create application from template')} From bfdd2880789ceba99926328e38caeeddbbfe7b07 Mon Sep 17 00:00:00 2001 From: Anantshree Chandola Date: Tue, 17 Oct 2023 17:15:52 +0530 Subject: [PATCH 6/8] very small change --- frontend/src/Editor/Header/EditAppName.jsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/src/Editor/Header/EditAppName.jsx b/frontend/src/Editor/Header/EditAppName.jsx index 097ae8aa31..8ac727e5be 100644 --- a/frontend/src/Editor/Header/EditAppName.jsx +++ b/frontend/src/Editor/Header/EditAppName.jsx @@ -121,9 +121,9 @@ function EditAppName({ appId, appName = '', onNameChanged }) { = 50 - ? 'Maximum length has been reached' - : 'App name should be unique and max 50 characters' + errorMessage || + warningText || + (name.length >= 50 ? 'Maximum length has been reached' : 'App name should be unique and max 50 characters') } isWarning={warningText || name.length >= 50} isError={isError} From a6b6935327a9fc201aac104e88a0af27dca9893e Mon Sep 17 00:00:00 2001 From: Anantshree Chandola Date: Tue, 17 Oct 2023 17:41:06 +0530 Subject: [PATCH 7/8] remove option to see all templates for user with no permission --- frontend/src/HomePage/BlankPage.jsx | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/frontend/src/HomePage/BlankPage.jsx b/frontend/src/HomePage/BlankPage.jsx index c7a8c54a25..e2407b31cc 100644 --- a/frontend/src/HomePage/BlankPage.jsx +++ b/frontend/src/HomePage/BlankPage.jsx @@ -130,17 +130,17 @@ export const BlankPage = function BlankPage({ ); })}
+
+ +
)} -
- -
From cbb92dfbe243c337654c43e88e57e3c60a6471b3 Mon Sep 17 00:00:00 2001 From: Ajith KV Date: Tue, 17 Oct 2023 17:58:12 +0530 Subject: [PATCH 8/8] Modify test cases (#7935) --- cypress-tests/cypress/commands/commands.js | 2 + .../cypress/constants/texts/dashboard.js | 2 +- .../e2e/editor/app-version/version.cy.js | 3 +- .../cypress/e2e/workspace/dashboard.cy.js | 31 +-- .../cypress/e2e/workspace/shareApp.cy.js | 204 +++++++++--------- .../e2e/workspace/userPermissions.cy.js | 29 ++- .../e2e/workspace/workspaceConstants.cy.js | 3 +- 7 files changed, 136 insertions(+), 138 deletions(-) diff --git a/cypress-tests/cypress/commands/commands.js b/cypress-tests/cypress/commands/commands.js index 53ca1b7375..016af0bf06 100644 --- a/cypress-tests/cypress/commands/commands.js +++ b/cypress-tests/cypress/commands/commands.js @@ -56,6 +56,8 @@ Cypress.Commands.add("createApp", (appName) => { cy.get("body").then(($title) => { cy.get(getAppButtonSelector($title)).click(); + cy.clearAndType('[data-cy="app-name-input"]', appName); + cy.get('[data-cy="+ Create app"]').click(); }); cy.waitForAppLoad(); cy.skipEditorPopover(); diff --git a/cypress-tests/cypress/constants/texts/dashboard.js b/cypress-tests/cypress/constants/texts/dashboard.js index 47131ff553..3e8f1ab913 100644 --- a/cypress-tests/cypress/constants/texts/dashboard.js +++ b/cypress-tests/cypress/constants/texts/dashboard.js @@ -37,7 +37,7 @@ export const dashboardText = { }, seeAllAppsTemplateButton: "See all templates", addToFolderTitle: "Add to folder", - appClonedToast: "App cloned successfully.", + appClonedToast: "App cloned successfully!", darkModeText: "Dark Mode", lightModeText: "Light Mode", dashboardAppsHeaderLabel: " All apps", diff --git a/cypress-tests/cypress/e2e/editor/app-version/version.cy.js b/cypress-tests/cypress/e2e/editor/app-version/version.cy.js index a890da5760..b3ade9630e 100644 --- a/cypress-tests/cypress/e2e/editor/app-version/version.cy.js +++ b/cypress-tests/cypress/e2e/editor/app-version/version.cy.js @@ -42,9 +42,8 @@ describe("App Version Functionality", () => { }); it("Verify the elements of the version module", () => { - cy.createApp(); + cy.createApp(data.appName); cy.get(appVersionSelectors.appVersionLabel).should("be.visible"); - cy.renameApp(data.appName); cy.get(commonSelectors.appNameInput).verifyVisibleElement( "have.value", data.appName diff --git a/cypress-tests/cypress/e2e/workspace/dashboard.cy.js b/cypress-tests/cypress/e2e/workspace/dashboard.cy.js index 3a9274b1a7..e7d5c84ffb 100644 --- a/cypress-tests/cypress/e2e/workspace/dashboard.cy.js +++ b/cypress-tests/cypress/e2e/workspace/dashboard.cy.js @@ -168,12 +168,10 @@ describe("dashboard", () => { it("Should verify app card elements and app card operations", () => { cy.apiLogin(); - cy.apiCreateApp(); + cy.apiCreateApp(data.appName); cy.openApp(); - cy.renameApp(data.appName); cy.dragAndDropWidget("Table", 250, 250); - cy.get(commonSelectors.editorPageLogo).click(); cy.wait(500); @@ -192,7 +190,6 @@ describe("dashboard", () => { expect($el.contents().last().text().trim()).to.eq("The Developer"); }); }); - cy.reloadAppForTheElement(data.appName); viewAppCardOptions(data.appName); cy.get( @@ -213,7 +210,6 @@ describe("dashboard", () => { modifyAndVerifyAppCardIcon(data.appName); createFolder(data.folderName); - cy.reloadAppForTheElement(data.appName); viewAppCardOptions(data.appName); cy.get( @@ -246,7 +242,7 @@ describe("dashboard", () => { cy.get(commonSelectors.appCard(data.appName)) .contains(data.appName) .should("be.visible"); - cy.reloadAppForTheElement(data.appName); + viewAppCardOptions(data.appName); cy.get(commonSelectors.appCardOptions(commonText.removeFromFolderOption)) @@ -256,7 +252,6 @@ describe("dashboard", () => { cancelModal(commonText.cancelButton); - cy.reloadAppForTheElement(data.appName); viewAppCardOptions(data.appName); cy.get( commonSelectors.appCardOptions(commonText.removeFromFolderOption) @@ -276,17 +271,13 @@ describe("dashboard", () => { deleteFolder(data.folderName); cy.get(commonSelectors.allApplicationsLink).click(); - cy.reloadAppForTheElement(data.appName); viewAppCardOptions(data.appName); cy.get(commonSelectors.appCardOptions(commonText.cloneAppOption)).click(); - cy.verifyToastMessage( - commonSelectors.toastMessage, - dashboardText.appClonedToast - ); - // cy.waitForAppLoad(); - cy.wait(2000); - cy.clearAndType(commonSelectors.appNameInput, data.cloneAppName); + cy.get('[data-cy="Clone app"]').click(); + cy.get('.go3958317564').should('be.visible').and('have.text', dashboardText.appClonedToast) + cy.wait(3000); + cy.renameApp(data.cloneAppName); cy.dragAndDropWidget("button", 25, 25); cy.get(commonSelectors.editorPageLogo).click(); cy.wait("@appLibrary"); @@ -341,12 +332,11 @@ describe("dashboard", () => { it("Should verify the app CRUD operation", () => { data.appName = `${fake.companyName}-App`; cy.appUILogin(); - cy.createApp(); - cy.renameApp(data.appName); + cy.createApp(data.appName); cy.dragAndDropWidget("Button", 450, 450); cy.get(commonSelectors.editorPageLogo).click(); - cy.reloadAppForTheElement(data.appName); + cy.get(commonSelectors.appCard(data.appName)).should( "contain.text", data.appName @@ -368,8 +358,7 @@ describe("dashboard", () => { it("Should verify the folder CRUD operation", () => { data.appName = `${fake.companyName}-App`; cy.appUILogin(); - cy.createApp(); - cy.renameApp(data.appName); + cy.createApp(data.appName); cy.dragAndDropWidget("Button", 100, 100); cy.get(commonSelectors.editorPageLogo).click(); @@ -431,7 +420,7 @@ describe("dashboard", () => { .should("be.visible") .and("have.text", "Edit folder"); - cy.get(commonSelectors.folderNameInput).should("be.visible") + cy.get(commonSelectors.folderNameInput).should("be.visible"); // verifyModal( // commonText.updateFolderTitle, diff --git a/cypress-tests/cypress/e2e/workspace/shareApp.cy.js b/cypress-tests/cypress/e2e/workspace/shareApp.cy.js index ff6c9452ed..b9391d303d 100644 --- a/cypress-tests/cypress/e2e/workspace/shareApp.cy.js +++ b/cypress-tests/cypress/e2e/workspace/shareApp.cy.js @@ -13,119 +13,121 @@ describe("App share functionality", () => { data.email = fake.email.toLowerCase(); const slug = data.appName.toLowerCase().replace(/\s+/g, "-"); const firstUserEmail = data.email + const envVar = Cypress.env("environment"); beforeEach(() => { cy.appUILogin(); }); - it("Verify private and public app share funtionality", () => { - cy.apiLogin(); - cy.apiCreateApp(); - cy.openApp(); - cy.renameApp(data.appName); - cy.dragAndDropWidget("Table", 250, 250); + if (envVar === "Community") { + it("Verify private and public app share funtionality", () => { + cy.apiLogin(); + cy.apiCreateApp(data.appName); + cy.openApp(); + cy.dragAndDropWidget("Table", 250, 250); - cy.get(commonWidgetSelector.shareAppButton).click(); + cy.get(commonWidgetSelector.shareAppButton).click(); - for (const elements in commonWidgetSelector.shareModalElements) { - cy.get( - commonWidgetSelector.shareModalElements[elements] - ).verifyVisibleElement( + for (const elements in commonWidgetSelector.shareModalElements) { + cy.get( + commonWidgetSelector.shareModalElements[elements] + ).verifyVisibleElement( + "have.text", + commonText.shareModalElements[elements] + ); + } + + cy.get(commonWidgetSelector.makePublicAppToggle).should("be.visible"); + cy.get(commonWidgetSelector.appLink).should("be.visible"); + cy.get(commonWidgetSelector.appNameSlugInput).should("be.visible"); + // cy.get(commonWidgetSelector.iframeLink).should("be.visible"); + cy.get(commonWidgetSelector.modalCloseButton).should("be.visible"); + + cy.clearAndType(commonWidgetSelector.appNameSlugInput, `${slug}`); + cy.get(commonWidgetSelector.modalCloseButton).click(); + cy.forceClickOnCanvas() + cy.dragAndDropWidget("Button", 50, 50); + cy.get(commonSelectors.editorPageLogo).click(); + + logout(); + cy.visit(`/applications/${slug}`); + + cy.get(commonSelectors.loginButton).should("be.visible"); + + cy.clearAndType(commonSelectors.workEmailInputField, "dev@tooljet.io"); + cy.clearAndType(commonSelectors.passwordInputField, "password"); + cy.get(commonSelectors.loginButton).click(); + + cy.wait(500); + cy.get('[data-cy="draggable-widget-table1"]').should("be.visible"); + cy.get(commonSelectors.viewerPageLogo).click(); + + navigateToAppEditor(data.appName); + cy.get(commonWidgetSelector.shareAppButton).click(); + cy.get(commonWidgetSelector.makePublicAppToggle).check(); + cy.get(commonWidgetSelector.modalCloseButton).click(); + cy.get(commonSelectors.editorPageLogo).click(); + + logout(); + cy.visit(`/applications/${slug}`); + cy.wait(500); + cy.get('[data-cy="draggable-widget-table1"]').should("be.visible"); + }); + + it("Verify app private and public app visibility for the same workspace user", () => { + addNewUserMW(data.firstName, data.email); + + logout(); + cy.visit(`/applications/${slug}`); + cy.get('[data-cy="draggable-widget-table1"]').should("be.visible"); + + cy.appUILogin(); + navigateToAppEditor(data.appName); + cy.skipEditorPopover() + cy.get(commonWidgetSelector.shareAppButton).click(); + cy.get(commonWidgetSelector.makePublicAppToggle).uncheck(); + cy.get(commonWidgetSelector.modalCloseButton).click(); + cy.get(commonSelectors.editorPageLogo).click(); + + logout(); + cy.visit(`/applications/${slug}`); + + cy.login(data.email, "password"); + cy.get(commonSelectors.allApplicationLink).verifyVisibleElement( "have.text", - commonText.shareModalElements[elements] + commonText.allApplicationLink ); - } + }); - cy.get(commonWidgetSelector.makePublicAppToggle).should("be.visible"); - cy.get(commonWidgetSelector.appLink).should("be.visible"); - cy.get(commonWidgetSelector.appNameSlugInput).should("be.visible"); - // cy.get(commonWidgetSelector.iframeLink).should("be.visible"); - cy.get(commonWidgetSelector.modalCloseButton).should("be.visible"); + it("Verify app private and public app visibility for the same instance user", () => { + data.firstName = fake.firstName; + data.email = fake.email.toLowerCase(); - cy.clearAndType(commonWidgetSelector.appNameSlugInput, `${slug}`); - cy.get(commonWidgetSelector.modalCloseButton).click(); - cy.forceClickOnCanvas() - cy.dragAndDropWidget("Button", 50, 50); - cy.get(commonSelectors.editorPageLogo).click(); + logout(); + userSignUp(data.firstName, data.email, "Test"); + cy.visit(`/applications/${slug}`); + cy.wait(1000); - logout(); - cy.visit(`/applications/${slug}`); + cy.clearAndType(commonSelectors.workEmailInputField, data.email); + cy.clearAndType(commonSelectors.passwordInputField, "password"); + cy.get(commonSelectors.signInButton).click(); + cy.wait(1000); - cy.get(commonSelectors.loginButton).should("be.visible"); + cy.visit("/"); + cy.wait(2000); + logout(); + cy.appUILogin(); - cy.clearAndType(commonSelectors.workEmailInputField, "dev@tooljet.io"); - cy.clearAndType(commonSelectors.passwordInputField, "password"); - cy.get(commonSelectors.loginButton).click(); + navigateToAppEditor(data.appName); + cy.skipEditorPopover(); + cy.get(commonWidgetSelector.shareAppButton).click(); + cy.get(commonWidgetSelector.makePublicAppToggle).check(); + cy.get(commonWidgetSelector.modalCloseButton).click(); + cy.get(commonSelectors.editorPageLogo).click(); - cy.wait(500); - cy.get('[data-cy="draggable-widget-table1"]').should("be.visible"); - cy.get(commonSelectors.viewerPageLogo).click(); - - navigateToAppEditor(data.appName); - cy.get(commonWidgetSelector.shareAppButton).click(); - cy.get(commonWidgetSelector.makePublicAppToggle).check(); - cy.get(commonWidgetSelector.modalCloseButton).click(); - cy.get(commonSelectors.editorPageLogo).click(); - - logout(); - cy.visit(`/applications/${slug}`); - cy.wait(500); - cy.get('[data-cy="draggable-widget-table1"]').should("be.visible"); - }); - - it("Verify app private and public app visibility for the same workspace user", () => { - addNewUserMW(data.firstName, data.email); - - logout(); - cy.visit(`/applications/${slug}`); - cy.get('[data-cy="draggable-widget-table1"]').should("be.visible"); - - cy.appUILogin(); - navigateToAppEditor(data.appName); - cy.skipEditorPopover() - cy.get(commonWidgetSelector.shareAppButton).click(); - cy.get(commonWidgetSelector.makePublicAppToggle).uncheck(); - cy.get(commonWidgetSelector.modalCloseButton).click(); - cy.get(commonSelectors.editorPageLogo).click(); - - logout(); - cy.visit(`/applications/${slug}`); - - cy.login(data.email, "password"); - cy.get(commonSelectors.allApplicationLink).verifyVisibleElement( - "have.text", - commonText.allApplicationLink - ); - }); - - it("Verify app private and public app visibility for the same instance user", () => { - data.firstName = fake.firstName; - data.email = fake.email.toLowerCase(); - - logout(); - userSignUp(data.firstName, data.email, "Test"); - cy.visit(`/applications/${slug}`); - cy.wait(1000); - - cy.clearAndType(commonSelectors.workEmailInputField, data.email); - cy.clearAndType(commonSelectors.passwordInputField, "password"); - cy.get(commonSelectors.signInButton).click(); - cy.wait(1000); - - cy.visit("/"); - cy.wait(2000); - logout(); - cy.appUILogin(); - - navigateToAppEditor(data.appName); - cy.skipEditorPopover(); - cy.get(commonWidgetSelector.shareAppButton).click(); - cy.get(commonWidgetSelector.makePublicAppToggle).check(); - cy.get(commonWidgetSelector.modalCloseButton).click(); - cy.get(commonSelectors.editorPageLogo).click(); - - logout(); - cy.visit(`/applications/${slug}`); - cy.get('[data-cy="draggable-widget-table1"]').should("be.visible"); - cy.get(commonSelectors.viewerPageLogo).click(); - }); + logout(); + cy.visit(`/applications/${slug}`); + cy.get('[data-cy="draggable-widget-table1"]').should("be.visible"); + cy.get(commonSelectors.viewerPageLogo).click(); + }); + } }); \ No newline at end of file diff --git a/cypress-tests/cypress/e2e/workspace/userPermissions.cy.js b/cypress-tests/cypress/e2e/workspace/userPermissions.cy.js index b6ebcf5416..6911368965 100644 --- a/cypress-tests/cypress/e2e/workspace/userPermissions.cy.js +++ b/cypress-tests/cypress/e2e/workspace/userPermissions.cy.js @@ -23,11 +23,9 @@ describe("User permissions", () => { permissions.reset(); cy.get(commonSelectors.homePageLogo).click(); cy.wait("@homePage"); - cy.createApp(); - cy.renameApp(data.appName); + cy.createApp(data.appName); cy.dragAndDropWidget("Table", 250, 250); cy.get(commonSelectors.editorPageLogo).click(); - cy.reloadAppForTheElement(data.appName); permissions.addNewUserMW(data.firstName, data.email); common.logout(); }); @@ -41,11 +39,7 @@ describe("User permissions", () => { cy.login(data.email, usersText.password); cy.get("body").then(($title) => { if ($title.text().includes(dashboardText.emptyPageDescription)) { - cy.get(commonSelectors.dashboardAppCreateButton).click(); - cy.verifyToastMessage( - commonSelectors.toastMessage, - usersText.createAppPermissionToast - ); + cy.get(commonSelectors.dashboardAppCreateButton).should('be.disabled'); } else { cy.contains(dashboardText.createAppButton).should("not.exist"); } @@ -120,7 +114,21 @@ describe("User permissions", () => { }); it("Should verify the Create and Delete app permission", () => { + data.appName = `${fake.companyName}-App`; + cy.createApp(data.appName); + cy.get(commonSelectors.editorPageLogo).click(); + cy.wait(1000); common.navigateToManageGroups(); + cy.get(groupsSelector.appSearchBox).click(); + cy.get(groupsSelector.searchBoxOptions).contains(data.appName).click(); + cy.get(groupsSelector.selectAddButton).click(); + cy.get("table").contains("td", data.appName); + cy.contains("td", data.appName) + .parent() + .within(() => { + cy.get("td input").first().should("be.checked"); + }); + cy.wait(500) cy.get(groupsSelector.permissionsLink).click(); cy.get(groupsSelector.appsCreateCheck).check(); cy.get(groupsSelector.permissionsLink).click(); @@ -141,11 +149,10 @@ describe("User permissions", () => { common.viewAppCardOptions(data.appName); cy.contains("Delete app").should("not.exist"); - cy.createApp(); - cy.renameApp(data.email); + cy.createApp(data.email); + cy.dragAndDropWidget("Table", 50, 50); cy.get(commonSelectors.editorPageLogo).click(); - cy.reloadAppForTheElement(data.email); common.viewAppCardOptions(data.email); cy.contains("Delete app").should("exist"); cy.get(commonSelectors.appCardOptions(commonText.deleteAppOption)).click(); diff --git a/cypress-tests/cypress/e2e/workspace/workspaceConstants.cy.js b/cypress-tests/cypress/e2e/workspace/workspaceConstants.cy.js index e5c4f3b26b..0d8258136b 100644 --- a/cypress-tests/cypress/e2e/workspace/workspaceConstants.cy.js +++ b/cypress-tests/cypress/e2e/workspace/workspaceConstants.cy.js @@ -279,8 +279,7 @@ describe("Workspace constants", () => { cy.get(commonSelectors.homePageLogo).click(); cy.wait("@homePage"); - cy.createApp(); - cy.renameApp(data.appName); + cy.createApp(data.appName); selectQueryFromLandingPage("runjs", "JavaScript"); addInputOnQueryField("runjs", `return constants.${data.constantsName}`);