diff --git a/frontend/src/Editor/AppVersionsManager/AppVersionsManager.jsx b/frontend/src/Editor/AppVersionsManager/AppVersionsManager.jsx index 6c71dc6a43..f43445cec5 100644 --- a/frontend/src/Editor/AppVersionsManager/AppVersionsManager.jsx +++ b/frontend/src/Editor/AppVersionsManager/AppVersionsManager.jsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from 'react'; +import React, { useState } from 'react'; import cx from 'classnames'; import { appVersionService } from '@/_services'; import { CustomSelect } from './CustomSelect'; @@ -6,6 +6,7 @@ import { toast } from 'react-hot-toast'; import { shallow } from 'zustand/shallow'; import { useAppVersionStore } from '@/_stores/appVersionStore'; import { useEditorStore } from '@/_stores/editorStore'; +import { useEnvironmentsAndVersionsStore } from '@/_stores/environmentsAndVersionsStore'; const appVersionLoadingStatus = Object.freeze({ loading: 'loading', @@ -20,18 +21,56 @@ export const AppVersionsManager = function ({ isEditable = true, isViewer, }) { - const [appVersionStatus, setGetAppVersionStatus] = useState(appVersionLoadingStatus.loading); + const { initializedEnvironmentDropdown, versionsPromotedToEnvironment, lazyLoadAppVersions, appVersionsLazyLoaded } = + useEnvironmentsAndVersionsStore( + (state) => ({ + appVersionsLazyLoaded: state.appVersionsLazyLoaded, + initializedEnvironmentDropdown: state.initializedEnvironmentDropdown, + versionsPromotedToEnvironment: state.versionsPromotedToEnvironment, + lazyLoadAppVersions: state.actions.lazyLoadAppVersions, + }), + shallow + ); + + if (initializedEnvironmentDropdown) { + return ( + + ); + } else { + return <>; + } +}; + +const RenderComponent = ({ + appId, + isEditable, + isViewer, + setAppDefinitionFromVersion, + onVersionDelete, + versionsPromotedToEnvironment, + lazyLoadAppVersions, + appVersionsLazyLoaded, +}) => { + const [appVersionStatus, setGetAppVersionStatus] = useState(appVersionLoadingStatus.loaded); const [deleteVersion, setDeleteVersion] = useState({ versionId: '', versionName: '', showModal: false, }); + const [forceMenuOpen, setForceMenuOpen] = useState(false); - const { releasedVersionId, editingVersion, appVersions, setAppVersions } = useAppVersionStore( + const { releasedVersionId, editingVersion } = useAppVersionStore( (state) => ({ editingVersion: state.editingVersion, - appVersions: state.appVersions, - setAppVersions: state.actions?.setAppVersions, releasedVersionId: state.releasedVersionId, }), shallow @@ -43,16 +82,6 @@ export const AppVersionsManager = function ({ shallow ); - useEffect(() => { - if (appVersions && appVersions.length > 0) { - setGetAppVersionStatus(appVersionLoadingStatus.loaded); - } - - return () => { - setGetAppVersionStatus(appVersionLoadingStatus.loading); - }; - }, [appVersions]); - const darkMode = localStorage.getItem('darkMode') === 'true'; const selectVersion = (id) => { @@ -93,13 +122,12 @@ export const AppVersionsManager = function ({ }) .finally(() => { appVersionService.getAll(appId, true).then((data) => { - setAppVersions(data.versions); onVersionDelete(); }); }); }; - const options = appVersions.map((appVersion) => ({ + const options = versionsPromotedToEnvironment.map((appVersion) => ({ value: appVersion.id, isReleasedVersion: appVersion.id === releasedVersionId, appVersionName: appVersion.name, @@ -139,10 +167,17 @@ export const AppVersionsManager = function ({ ), })); + const onMenuOpen = async () => { + if (!appVersionsLazyLoaded) { + setGetAppVersionStatus(appVersionLoadingStatus.loading); + await lazyLoadAppVersions(appId); + setGetAppVersionStatus(appVersionLoadingStatus.loaded); + } + setForceMenuOpen(!forceMenuOpen); + }; + const customSelectProps = { appId, - appVersions, - setAppVersions, setAppDefinitionFromVersion, editingVersion, setDeleteVersion, @@ -175,6 +210,9 @@ export const AppVersionsManager = function ({ {...customSelectProps} className={` ${darkMode && 'dark-theme'}`} isEditable={isEditable} + onMenuOpen={onMenuOpen} + onMenuClose={() => setForceMenuOpen(false)} + menuIsOpen={forceMenuOpen} /> diff --git a/frontend/src/Editor/Editor.jsx b/frontend/src/Editor/Editor.jsx index 39a9cadb74..2a21c2af09 100644 --- a/frontend/src/Editor/Editor.jsx +++ b/frontend/src/Editor/Editor.jsx @@ -707,8 +707,6 @@ const EditorComponent = (props) => { useAppVersionStore.getState().actions.updateReleasedVersionId(data.current_version_id); } - const appVersions = await appEnvironmentService.getVersionsByEnvironment(data?.id); - setAppVersions(appVersions.appVersions); const currentOrgId = data?.organization_id || data?.organizationId; updateState({ diff --git a/frontend/src/Editor/Header/RightTopHeaderButtons/EnvironmentManager/EnvironmentsManager.jsx b/frontend/src/Editor/Header/RightTopHeaderButtons/EnvironmentManager/EnvironmentsManager.jsx new file mode 100644 index 0000000000..e8a116e077 --- /dev/null +++ b/frontend/src/Editor/Header/RightTopHeaderButtons/EnvironmentManager/EnvironmentsManager.jsx @@ -0,0 +1,27 @@ +import { useEnvironmentsAndVersionsActions } from '@/_stores/environmentsAndVersionsStore'; +import React, { useEffect } from 'react'; +import { shallow } from 'zustand/shallow'; +import { useAppVersionStore } from '@/_stores/appVersionStore'; + +const EnvironmentManager = () => { + const { editingVersionId } = useAppVersionStore( + (state) => ({ + editingVersionId: state?.editingVersion?.id, + }), + shallow + ); + const { init, setEnvironmentDropdownStatus } = useEnvironmentsAndVersionsActions(); + useEffect(() => { + initComponent(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + const initComponent = async () => { + await init(editingVersionId); + setEnvironmentDropdownStatus(true); + }; + + return
; +}; + +export default EnvironmentManager; diff --git a/frontend/src/Editor/Header/RightTopHeaderButtons/EnvironmentManager/index.js b/frontend/src/Editor/Header/RightTopHeaderButtons/EnvironmentManager/index.js new file mode 100644 index 0000000000..34f253383a --- /dev/null +++ b/frontend/src/Editor/Header/RightTopHeaderButtons/EnvironmentManager/index.js @@ -0,0 +1,2 @@ +import EnvironmentManager from './EnvironmentsManager'; +export default EnvironmentManager; diff --git a/frontend/src/Editor/ManageAppUsers.jsx b/frontend/src/Editor/Header/RightTopHeaderButtons/ManageAppUsers.jsx similarity index 100% rename from frontend/src/Editor/ManageAppUsers.jsx rename to frontend/src/Editor/Header/RightTopHeaderButtons/ManageAppUsers.jsx diff --git a/frontend/src/Editor/Header/RightTopHeaderButtons/PromoteVersionButton.jsx b/frontend/src/Editor/Header/RightTopHeaderButtons/PromoteVersionButton.jsx new file mode 100644 index 0000000000..57201589d4 --- /dev/null +++ b/frontend/src/Editor/Header/RightTopHeaderButtons/PromoteVersionButton.jsx @@ -0,0 +1,7 @@ +import React from 'react'; + +const PromoteVersionButton = () => { + return <>; +}; + +export default PromoteVersionButton; diff --git a/frontend/src/Editor/ReleaseVersionButton.jsx b/frontend/src/Editor/Header/RightTopHeaderButtons/ReleaseVersionButton.jsx similarity index 88% rename from frontend/src/Editor/ReleaseVersionButton.jsx rename to frontend/src/Editor/Header/RightTopHeaderButtons/ReleaseVersionButton.jsx index 04973bbd03..3c72c32a89 100644 --- a/frontend/src/Editor/ReleaseVersionButton.jsx +++ b/frontend/src/Editor/Header/RightTopHeaderButtons/ReleaseVersionButton.jsx @@ -8,7 +8,7 @@ import { ConfirmDialog } from '@/_components/ConfirmDialog'; import { shallow } from 'zustand/shallow'; import { ButtonSolid } from '@/_ui/AppButton/AppButton'; -export const ReleaseVersionButton = function DeployVersionButton({ appId, appName, fetchApp, onVersionRelease }) { +export const ReleaseVersionButton = function DeployVersionButton({ onVersionRelease }) { const [isReleasing, setIsReleasing] = useState(false); const { isVersionReleased, editingVersion } = useAppVersionStore( (state) => ({ @@ -24,17 +24,15 @@ export const ReleaseVersionButton = function DeployVersionButton({ appId, appNam setShowPageDeletionConfirmation(false); setIsReleasing(true); + const { id: versionToBeReleased, name, app_id } = editingVersion; + appsService - .saveApp(appId, { - name: appName, - current_version_id: editingVersion.id, - }) + .releaseVersion(app_id, versionToBeReleased) .then(() => { - toast(`Version ${editingVersion.name} released`, { + toast(`Version ${name} released`, { icon: '🚀', }); - fetchApp && fetchApp(); - onVersionRelease(editingVersion.id); + onVersionRelease(versionToBeReleased); setIsReleasing(false); }) .catch((_error) => { diff --git a/frontend/src/Editor/Header/RightTopHeaderButtons/RightTopHeaderButtons.jsx b/frontend/src/Editor/Header/RightTopHeaderButtons/RightTopHeaderButtons.jsx new file mode 100644 index 0000000000..95c399f194 --- /dev/null +++ b/frontend/src/Editor/Header/RightTopHeaderButtons/RightTopHeaderButtons.jsx @@ -0,0 +1,101 @@ +import React, { useEffect } from 'react'; +import { ReleaseVersionButton } from './ReleaseVersionButton'; +import { Link } from 'react-router-dom'; +import { useAppInfo, useAppDataActions } from '@/_stores/appDataStore'; +import { ManageAppUsers } from './ManageAppUsers'; +import { useAppVersionStore } from '@/_stores/appVersionStore'; +import { shallow } from 'zustand/shallow'; +import queryString from 'query-string'; +import { isEmpty } from 'lodash'; +import { useCurrentStateStore } from '@/_stores/currentStateStore'; +import SolidIcon from '@/_ui/Icon/SolidIcons'; +import { useEnvironmentsAndVersionsStore } from '@/_stores/environmentsAndVersionsStore'; +import PromoteVersionButton from './PromoteVersionButton'; + +const RightTopHeaderButtons = ({ onVersionRelease }) => { + return ( +
+
+ + +
+
+ ); +}; + +const PreviewAndShareIcons = () => { + const { appId, app, slug, isPublic, appVersionPreviewLink, currentVersionId } = useAppInfo(); + const { setAppPreviewLink } = useAppDataActions(); + const { isVersionReleased, editingVersion } = useAppVersionStore( + (state) => ({ + isVersionReleased: state.isVersionReleased, + editingVersion: state.editingVersion, + }), + shallow + ); + const { pageHandle } = useCurrentStateStore( + (state) => ({ + pageHandle: state?.page?.handle, + }), + shallow + ); + const darkMode = localStorage.getItem('darkMode') === 'true'; + + useEffect(() => { + const previewQuery = queryString.stringify({ version: editingVersion.name }); + const appVersionPreviewLink = editingVersion.id + ? `/applications/${slug || appId}/${pageHandle}${!isEmpty(previewQuery) ? `?${previewQuery}` : ''}` + : ''; + setAppPreviewLink(appVersionPreviewLink); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [slug, currentVersionId, editingVersion]); + + return ( +
+
+ {appId && ( + + )} +
+
+ + + +
+
+ ); +}; + +const PromoteAndReleaseButton = ({ onVersionRelease }) => { + const { shouldRenderPromoteButton, shouldRenderReleaseButton } = useEnvironmentsAndVersionsStore( + (state) => ({ + shouldRenderPromoteButton: state.shouldRenderPromoteButton, + shouldRenderReleaseButton: state.shouldRenderReleaseButton, + }), + shallow + ); + + return ( +
+ {shouldRenderPromoteButton && } + {shouldRenderReleaseButton && } +
+ ); +}; + +export default RightTopHeaderButtons; diff --git a/frontend/src/Editor/Header/RightTopHeaderButtons/index.js b/frontend/src/Editor/Header/RightTopHeaderButtons/index.js new file mode 100644 index 0000000000..5d3d19228f --- /dev/null +++ b/frontend/src/Editor/Header/RightTopHeaderButtons/index.js @@ -0,0 +1,2 @@ +import RightTopHeaderButtons from './RightTopHeaderButtons'; +export default RightTopHeaderButtons; diff --git a/frontend/src/Editor/Header/index.js b/frontend/src/Editor/Header/index.js index 1bc6eda0bb..51696634b4 100644 --- a/frontend/src/Editor/Header/index.js +++ b/frontend/src/Editor/Header/index.js @@ -1,12 +1,8 @@ import React, { useEffect } from 'react'; -import { Link } from 'react-router-dom'; -import AppLogo from '@/_components/AppLogo'; import EditAppName from './EditAppName'; import HeaderActions from './HeaderActions'; import RealtimeAvatars from '../RealtimeAvatars'; import { AppVersionsManager } from '@/Editor/AppVersionsManager/AppVersionsManager'; -import { ManageAppUsers } from '../ManageAppUsers'; -import { ReleaseVersionButton } from '../ReleaseVersionButton'; import cx from 'classnames'; import config from 'config'; // eslint-disable-next-line import/no-unresolved @@ -16,10 +12,11 @@ import { useCurrentStateStore } from '@/_stores/currentStateStore'; import { shallow } from 'zustand/shallow'; import { useAppDataActions, useAppInfo, useCurrentUser } from '@/_stores/appDataStore'; import SolidIcon from '@/_ui/Icon/SolidIcons'; -import { redirectToDashboard } from '@/_helpers/routes'; import queryString from 'query-string'; import { isEmpty } from 'lodash'; import LogoNavDropdown from '@/_components/LogoNavDropdown'; +import RightTopHeaderButtons from './RightTopHeaderButtons'; +import EnvironmentManager from './RightTopHeaderButtons/EnvironmentManager'; export default function EditorHeader({ M, @@ -31,15 +28,13 @@ export default function EditorHeader({ onNameChanged, setAppDefinitionFromVersion, onVersionRelease, - saveEditingVersion, onVersionDelete, slug, darkMode, - isSocketOpen, }) { const currentUser = useCurrentUser(); - const { isSaving, appId, appName, app, isPublic, appVersionPreviewLink, currentVersionId } = useAppInfo(); + const { isSaving, appId, appName, isPublic, currentVersionId } = useAppInfo(); const { setAppPreviewLink } = useAppDataActions(); const { isVersionReleased, editingVersion } = useAppVersionStore( (state) => ({ @@ -148,6 +143,8 @@ export default function EditorHeader({
+ + {editingVersion && ( )} -
-
-
-
- {appId && ( - - )} -
-
- - - -
-
- - {isSocketOpen && ( -
- -
- )} -
-
+ diff --git a/frontend/src/_services/app_environment.service.js b/frontend/src/_services/app_environment.service.js index 8e237c2db1..411ef368ca 100644 --- a/frontend/src/_services/app_environment.service.js +++ b/frontend/src/_services/app_environment.service.js @@ -5,6 +5,7 @@ import queryString from 'query-string'; export const appEnvironmentService = { getAllEnvironments, getVersionsByEnvironment, + init, }; function getAllEnvironments() { @@ -29,3 +30,9 @@ function getVersionsByEnvironment(appId, environmentId /* not needed for CE */) requestOptions ).then(handleResponse); } + +function init(editing_version_id = null) { + const requestOptions = { method: 'GET', headers: authHeader(), credentials: 'include' }; + const query = queryString.stringify({ editing_version_id }); + return fetch(`${config.apiUrl}/app-environments/init?${query}`, requestOptions).then(handleResponse); +} diff --git a/frontend/src/_services/apps.service.js b/frontend/src/_services/apps.service.js index 6af8a7c70c..f1f816cfd3 100644 --- a/frontend/src/_services/apps.service.js +++ b/frontend/src/_services/apps.service.js @@ -25,6 +25,7 @@ export const appsService = { getAppUsers, getVersions, getTables, + releaseVersion, }; function validateReleasedApp(slug) { @@ -204,3 +205,14 @@ function getTables(id) { const requestOptions = { method: 'GET', headers: authHeader(), credentials: 'include' }; return fetch(`${config.apiUrl}/apps/${id}/tables`, requestOptions).then(handleResponse); } + +function releaseVersion(appId, versionToBeReleased) { + const requestOptions = { + method: 'PUT', + headers: authHeader(), + body: JSON.stringify({ versionToBeReleased }), + credentials: 'include', + }; + + return fetch(`${config.apiUrl}/v2/apps/${appId}/release`, requestOptions).then(handleResponse); +} diff --git a/frontend/src/_stores/environmentsAndVersionsStore.js b/frontend/src/_stores/environmentsAndVersionsStore.js new file mode 100644 index 0000000000..80c9cba59c --- /dev/null +++ b/frontend/src/_stores/environmentsAndVersionsStore.js @@ -0,0 +1,60 @@ +import create from 'zustand'; +import { zustandDevTools } from './utils'; +import { appEnvironmentService } from '../_services/app_environment.service'; + +const initialState = { + selectedVersion: null, + selectedEnvironment: null, + appVersionEnvironment: null, + versionsPromotedToEnvironment: [], + environments: [], + shouldRenderPromoteButton: false, + shouldRenderReleaseButton: false, + initializedEnvironmentDropdown: false, + initializedVersionsDropdown: false, + environmentsLazyLoaded: false, + appVersionsLazyLoaded: false, +}; + +export const useEnvironmentsAndVersionsStore = create( + zustandDevTools( + (set, get) => ({ + ...initialState, + actions: { + init: async (editingVersionId) => { + try { + const response = await appEnvironmentService.init(editingVersionId); + set((state) => ({ + ...state, + selectedEnvironment: response.editorEnvironment, + selectedVersion: response.editorVersion, + appVersionEnvironment: response.appVersionEnvironment, + shouldRenderPromoteButton: response.shouldRenderPromoteButton, + shouldRenderReleaseButton: response.shouldRenderReleaseButton, + environments: [response.editorEnvironment], + versionsPromotedToEnvironment: [response.editorVersion], + })); + } catch (error) { + console.error('Error while initializing the environment dropdown', error); + } + }, + setEnvironmentDropdownStatus: (state) => set({ initializedEnvironmentDropdown: state }), + lazyLoadAppVersions: async (appId) => { + try { + const response = await appEnvironmentService.getVersionsByEnvironment(appId, get().selectedEnvironment.id); + set((state) => ({ + ...state, + versionsPromotedToEnvironment: response.appVersions, + appVersionsLazyLoaded: true, + })); + } catch (error) { + console.error('Error while getting the versions', error); + } + }, + }, + }), + { name: 'App Version Manager Store' } + ) +); + +export const useEnvironmentsAndVersionsActions = () => useEnvironmentsAndVersionsStore((state) => state.actions); diff --git a/server/src/controllers/app_environments.controller.ts b/server/src/controllers/app_environments.controller.ts index 1b7e411904..50f8dbc674 100644 --- a/server/src/controllers/app_environments.controller.ts +++ b/server/src/controllers/app_environments.controller.ts @@ -1,4 +1,4 @@ -import { Controller, Get, Query, UseGuards } from '@nestjs/common'; +import { Controller, Get, Param, Query, UseGuards } from '@nestjs/common'; import { decamelizeKeys } from 'humps'; import { JwtAuthGuard } from '../modules/auth/jwt-auth.guard'; import { ForbiddenException } from '@nestjs/common'; @@ -17,6 +17,16 @@ export class AppEnvironmentsController { private orgEnvironmentVariablesAbilityFactory: OrgEnvironmentVariablesAbilityFactory ) {} + @UseGuards(JwtAuthGuard) + @Get('init') + async init(@User() user, @Query('editing_version_id') editingVersionId: string) { + /* + init is a method in the AppEnvironmentService class that is used to initialize the app environment mananger. + Should not use for any other purpose. + */ + return await this.appEnvironmentServices.init(editingVersionId); + } + @UseGuards(JwtAuthGuard) @Get() async index(@User() user, @Query('app_id') appId: string) { @@ -39,9 +49,13 @@ export class AppEnvironmentsController { } @UseGuards(JwtAuthGuard) - @Get('versions') - async getVersions(@User() user, @Query('app_id') appId: string) { - const appVersions = await this.appEnvironmentServices.getVersionsByEnvironment(user?.organizationId, appId); + @Get(':id/versions') + async getVersionsByEnvironment(@User() user, @Param('id') environmentId: string, @Query('app_id') appId: string) { + const appVersions = await this.appEnvironmentServices.getVersionsByEnvironment( + user?.organizationId, + appId, + environmentId + ); return { appVersions }; } } diff --git a/server/src/controllers/apps.controller.v2.ts b/server/src/controllers/apps.controller.v2.ts index 45944e7210..3acf8768c0 100644 --- a/server/src/controllers/apps.controller.v2.ts +++ b/server/src/controllers/apps.controller.v2.ts @@ -32,6 +32,7 @@ import { PageService } from '@services/page.service'; import { EventsService } from '@services/events_handler.service'; import { AppVersionUpdateDto } from '@dto/app-version-update.dto'; import { CreateEventHandlerDto, UpdateEventHandlerDto } from '@dto/event-handler.dto'; +import { VersionReleaseDto } from '@dto/version-release.dto'; @Controller({ path: 'apps', @@ -499,4 +500,20 @@ export class AppsControllerV2 { return await this.eventService.deleteEvent(eventId, versionId); } + + @UseGuards(JwtAuthGuard) + @UseInterceptors(ValidAppInterceptor) + @Put(':id/release') + async releaseVersion( + @User() user, + @Param('id') id, + @AppDecorator() app: App, + @Body() versionReleaseDto: VersionReleaseDto + ) { + const ability = await this.appsAbilityFactory.appsActions(user, app.id); + if (!ability.can('updateParams', app)) { + throw new ForbiddenException('You do not have permissions to perform this action'); + } + return await this.appsService.releaseVersion(app.id, versionReleaseDto); + } } diff --git a/server/src/dto/version-release.dto.ts b/server/src/dto/version-release.dto.ts new file mode 100644 index 0000000000..f191290a14 --- /dev/null +++ b/server/src/dto/version-release.dto.ts @@ -0,0 +1,10 @@ +import { IsNotEmpty, IsUUID } from 'class-validator'; +import { Transform } from 'class-transformer'; +import { sanitizeInput } from 'src/helpers/utils.helper'; + +export class VersionReleaseDto { + @IsNotEmpty() + @IsUUID() + @Transform(({ value }) => sanitizeInput(value)) + versionToBeReleased: string; +} diff --git a/server/src/services/app_environments.service.ts b/server/src/services/app_environments.service.ts index 461f486e1c..635dddc7d2 100644 --- a/server/src/services/app_environments.service.ts +++ b/server/src/services/app_environments.service.ts @@ -7,8 +7,43 @@ import { OrganizationConstant } from 'src/entities/organization_constants.entity import { EntityManager, FindOneOptions, In, DeleteResult } from 'typeorm'; import { AppVersion } from 'src/entities/app_version.entity'; +export interface AppEnvironmentResponse { + editorVersion: Partial; + editorEnvironment: AppEnvironment; + appVersionEnvironment: AppEnvironment; + shouldRenderPromoteButton: boolean; + shouldRenderReleaseButton: boolean; +} + @Injectable() export class AppEnvironmentService { + async init(editingVersionId: string, manager?: EntityManager): Promise { + return await dbTransactionWrap(async (manager: EntityManager) => { + const editorVersion = await manager.findOne(AppVersion, { + select: ['id', 'name', 'currentEnvironmentId'], + where: { id: editingVersionId }, + }); + const editorEnvironment = await manager.findOne(AppEnvironment, { id: editorVersion.currentEnvironmentId }); + const { shouldRenderPromoteButton, shouldRenderReleaseButton } = + this.calculateButtonVisibility(editorEnvironment); + const response: AppEnvironmentResponse = { + editorVersion, + editorEnvironment, + appVersionEnvironment: editorEnvironment, + shouldRenderPromoteButton, + shouldRenderReleaseButton, + }; + return response; + }, manager); + } + + calculateButtonVisibility(appVersionEnvironment: AppEnvironment) { + /* Further conditions can handle from here */ + const shouldRenderPromoteButton = false; + const shouldRenderReleaseButton = true; + return { shouldRenderPromoteButton, shouldRenderReleaseButton }; + } + async get( organizationId: string, id?: string, diff --git a/server/src/services/apps.service.ts b/server/src/services/apps.service.ts index e98474dd4f..fa28f609bd 100644 --- a/server/src/services/apps.service.ts +++ b/server/src/services/apps.service.ts @@ -28,6 +28,7 @@ import { Layout } from 'src/entities/layout.entity'; import { Component } from 'src/entities/component.entity'; import { EventHandler } from 'src/entities/event_handler.entity'; +import { VersionReleaseDto } from '@dto/version-release.dto'; const uuid = require('uuid'); @Injectable() @@ -1044,4 +1045,25 @@ export class AppsService { }); }); } + + async releaseVersion(appId: string, versionReleaseDto: VersionReleaseDto, manager?: EntityManager) { + return await dbTransactionWrap(async (manager: EntityManager) => { + const { versionToBeReleased } = versionReleaseDto; + //check if the app version is eligible for release + const currentEnvironment: AppEnvironment = await manager + .createQueryBuilder(AppEnvironment, 'app_environments') + .select(['app_environments.id', 'app_environments.isDefault']) + .innerJoinAndSelect('app_versions', 'app_versions', 'app_versions.current_environment_id = app_environments.id') + .where('app_versions.id = :versionToBeReleased', { + versionToBeReleased, + }) + .getOne(); + + if (!currentEnvironment?.isDefault) { + throw new BadRequestException('You can only release when the version is promoted to production'); + } + + return await manager.update(App, appId, { currentVersionId: versionToBeReleased }); + }, manager); + } }