diff --git a/frontend/src/HomePage/AppMenu.jsx b/frontend/src/HomePage/AppMenu.jsx index 05281de413..2cd51be364 100644 --- a/frontend/src/HomePage/AppMenu.jsx +++ b/frontend/src/HomePage/AppMenu.jsx @@ -82,13 +82,22 @@ export const AppMenu = function AppMenu({ )} )} - {canUpdateApp && canCreateApp && appType !== 'workflow' && !isModuleApp && ( + {canUpdateApp && canCreateApp && !isModuleApp && ( <> + {appType !== 'workflow' && ( + openAppActionModal('clone-app')} + /> + )} openAppActionModal('clone-app')} + text={ + appType === 'workflow' + ? t('homePage.appCard.exportWorkflow', 'Export workflow') + : t('homePage.appCard.exportApp', 'Export app') + } + onClick={exportApp} /> - )} {canDeleteApp && ( diff --git a/frontend/src/HomePage/BlankPage.jsx b/frontend/src/HomePage/BlankPage.jsx index 631608dfef..050eaf093d 100644 --- a/frontend/src/HomePage/BlankPage.jsx +++ b/frontend/src/HomePage/BlankPage.jsx @@ -147,34 +147,38 @@ export const BlankPage = function BlankPage({ Create new {appType !== 'workflow' ? 'application' : 'workflow'} - {appType !== 'workflow' && ( -
- + + -
- )} +   + {appType !== 'workflow' + ? t('blankPage.importApplication', 'Import an app') + : t('blankPage.importWorkflow', 'Import a workflow')} + + + +
diff --git a/frontend/src/HomePage/HomePage.jsx b/frontend/src/HomePage/HomePage.jsx index deeada63a4..94612f8358 100644 --- a/frontend/src/HomePage/HomePage.jsx +++ b/frontend/src/HomePage/HomePage.jsx @@ -12,7 +12,7 @@ import { } from '@/_services'; import { ConfirmDialog, AppModal } from '@/_components'; import Select from '@/_ui/Select'; -import _, { sample, isEmpty } from 'lodash'; +import _, { sample, isEmpty, capitalize } from 'lodash'; import { Folders } from './Folders'; import { BlankPage } from './BlankPage'; import { toast } from 'react-hot-toast'; @@ -252,7 +252,11 @@ class HomePageComponent extends React.Component { }; getAppType = () => { - return this.props.appType === 'module' ? 'Module' : this.props.appType === 'workflow' ? 'Workflow' : 'App'; + const { appType } = this.props; + if (appType === 'front-end') return 'App'; + if (appType === 'workflow') return 'Workflow'; + if (appType === 'module') return 'Module'; + return 'app'; }; createApp = async (appName) => { @@ -330,6 +334,66 @@ class HomePageComponent extends React.Component { this.setState({ isExportingApp: true, app: app }); }; + exportAppDirectly = async (app) => { + try { + const fetchVersions = await appsService.getVersions(app.id); + const { versions } = fetchVersions; + + const currentEditingVersion = versions?.filter((version) => version?.isCurrentEditingVersion)[0]; + if (!currentEditingVersion) { + toast.error('Could not find current editing version.', { + position: 'top-center', + }); + return; + } + + // Export all TJDB tables used by default + const fetchTables = await appsService.getTables(app.id); + const { tables: allTables } = fetchTables; + + const versionId = currentEditingVersion.id; + const exportTjDb = true; + const exportTables = allTables; + + const appOpts = { + app: [ + { + id: app.id, + search_params: { version_id: versionId }, + }, + ], + }; + + const requestBody = { + ...appOpts, + ...(exportTjDb && { tooljet_database: exportTables }), + organization_id: app.organization_id, + }; + + const data = await appsService.exportResource(requestBody); + + const appName = app.name.replace(/\s+/g, '-').toLowerCase(); + const fileName = `${appName}-export-${new Date().getTime()}`; + const json = JSON.stringify(data, null, 2); + const blob = new Blob([json], { type: 'application/json' }); + const href = URL.createObjectURL(blob); + const link = document.createElement('a'); + link.href = href; + link.download = fileName + '.json'; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + + toast.success('Workflow exported successfully!', { + position: 'top-center', + }); + } catch (error) { + toast.error(`Could not export workflow: ${error?.data?.message || error.message}`, { + position: 'top-center', + }); + } + }; + readAndImport = (event) => { try { const file = event.target.files[0]; @@ -411,7 +475,7 @@ class HomePageComponent extends React.Component { } const data = await appsService.importResource(requestBody); - toast.success('App imported successfully.'); + toast.success(`${capitalize(this.getAppType())} imported successfully.`); this.setState({ isImportingApp: false }); if (!isEmpty(data.imports.app)) { @@ -433,7 +497,7 @@ class HomePageComponent extends React.Component { this.setState({ isImportingApp: false }); if (error.statusCode === 409) return false; - toast.error(error?.error || error?.message || 'App import failed'); + toast.error(error?.error || error?.message || `${capitalize(this.getAppType())} import failed`); } }; @@ -935,6 +999,53 @@ class HomePageComponent extends React.Component { importingGitAppOperations: validationMessage, }); }; + + // Helper functions for workflow limit checks + hasWorkflowLimitReached = () => { + const { workflowInstanceLevelLimit, workflowWorkspaceLevelLimit } = this.state; + + const instanceLimitReached = + workflowInstanceLevelLimit.total === 0 || workflowInstanceLevelLimit.current >= workflowInstanceLevelLimit.total; + const workspaceLimitReached = + workflowWorkspaceLevelLimit.total === 0 || + workflowWorkspaceLevelLimit.current >= workflowWorkspaceLevelLimit.total; + + return instanceLimitReached || workspaceLimitReached; + }; + + hasWorkflowLimitWarning = () => { + const { workflowInstanceLevelLimit, workflowWorkspaceLevelLimit } = this.state; + return this.hasInstanceLimitWarning() || this.hasWorkspaceLimitWarning(); + }; + + hasInstanceLimitWarning = () => { + const { workflowInstanceLevelLimit } = this.state; + const percentage = workflowInstanceLevelLimit.percentage; + + return ( + workflowInstanceLevelLimit.current >= workflowInstanceLevelLimit.total || + (percentage >= 90 && percentage < 100) || + workflowInstanceLevelLimit.current === workflowInstanceLevelLimit.total - 1 + ); + }; + + hasWorkspaceLimitWarning = () => { + const { workflowWorkspaceLevelLimit } = this.state; + const percentage = workflowWorkspaceLevelLimit.percentage; + + return ( + workflowWorkspaceLevelLimit.current >= workflowWorkspaceLevelLimit.total || + (percentage >= 90 && percentage < 100) || + workflowWorkspaceLevelLimit.current === workflowWorkspaceLevelLimit.total - 1 + ); + }; + + getWorkflowLimit = () => { + return this.hasInstanceLimitWarning() + ? this.state.workflowInstanceLevelLimit + : this.state.workflowWorkspaceLevelLimit; + }; + render() { const { apps, @@ -1436,16 +1547,24 @@ class HomePageComponent extends React.Component { 'Create new app' )} - - {this.props.appType !== 'workflow' && this.props.appType !== 'module' && ( + {this.props.appType === 'workflow' ? ( = 100 || (this.props.appType === 'module' && invalidLicense) - } + disabled={this.hasWorkflowLimitReached()} split className="d-inline" data-cy="import-dropdown-menu" /> + ) : ( + this.props.appType !== 'module' && ( + = 100 || (this.props.appType === 'module' && invalidLicense) + } + split + className="d-inline" + data-cy="import-dropdown-menu" + /> + ) )}
@@ -1621,7 +1741,7 @@ class HomePageComponent extends React.Component { canUpdateApp={this.canUpdateApp} deleteApp={this.deleteApp} cloneApp={this.cloneApp} - exportApp={this.exportApp} + exportApp={this.props.appType === 'workflow' ? this.exportAppDirectly : this.exportApp} meta={meta} currentFolder={currentFolder} isLoading={isLoading || !featuresLoaded} diff --git a/frontend/src/modules/common/components/BaseImportAppMenu/BaseImportAppMenu.jsx b/frontend/src/modules/common/components/BaseImportAppMenu/BaseImportAppMenu.jsx index ddf12aacab..624fda5e2c 100644 --- a/frontend/src/modules/common/components/BaseImportAppMenu/BaseImportAppMenu.jsx +++ b/frontend/src/modules/common/components/BaseImportAppMenu/BaseImportAppMenu.jsx @@ -9,19 +9,22 @@ const BaseImportAppMenu = ({ showCloudMenuItems = false, CloudMenuComponent = () => null, darkMode = false, + appType = 'front-end', ...props }) => { const fileInput = React.createRef(); const { t } = useTranslation(); return ( - - {t('homePage.header.chooseFromTemplate', 'Choose from template')} - + {appType !== 'workflow' && ( + + {t('homePage.header.chooseFromTemplate', 'Choose from template')} + + )}