From 8586219caf1e5b142caaf1d457498f111b263e1c Mon Sep 17 00:00:00 2001 From: bestony Date: Thu, 16 Jul 2026 07:57:11 +0800 Subject: [PATCH] feat: add project selector on one-click app setup page Show the same Parent project TreeSelect used when creating/editing apps on the one-click configuration page, and pass the selection through to deploy so apps are placed under the chosen project. --- .../variables/OneClickAppConfigPage.tsx | 61 ++++++++++++++++++- .../variables/OneClickDeploymentPage.tsx | 23 +++++++ 2 files changed, 81 insertions(+), 3 deletions(-) diff --git a/src/containers/apps/oneclick/variables/OneClickAppConfigPage.tsx b/src/containers/apps/oneclick/variables/OneClickAppConfigPage.tsx index e4da5866..b4eeadbf 100644 --- a/src/containers/apps/oneclick/variables/OneClickAppConfigPage.tsx +++ b/src/containers/apps/oneclick/variables/OneClickAppConfigPage.tsx @@ -2,8 +2,11 @@ import { Card, Col, Row } from 'antd' import ReactMarkdown from 'react-markdown' import { RouteComponentProps } from 'react-router' import gfm from 'remark-gfm' +import ProjectSelector from '../../../../components/ProjectSelector' import { IOneClickTemplate } from '../../../../models/IOneClickAppModels' +import ProjectDefinition from '../../../../models/ProjectDefinition' import ErrorFactory from '../../../../utils/ErrorFactory' +import { localize } from '../../../../utils/Language' import Toaster from '../../../../utils/Toaster' import Utils from '../../../../utils/Utils' import ApiComponent from '../../../global/ApiComponent' @@ -16,17 +19,21 @@ import OneClickVariablesSection from './OneClickVariablesSection' export const ONE_CLICK_APP_NAME_VAR_NAME = '$$cap_appname' export const ONE_CLICK_ROOT_DOMAIN_VAR_NAME = '$$cap_root_domain' +export const ONE_CLICK_PROJECT_ID_VAR_NAME = '$$cap_project_id' // Query parameter constants for deployment page export const DEPLOYMENT_QUERY_PARAM_TEMPLATE = 'template' export const DEPLOYMENT_QUERY_PARAM_VALUES_ARRAY = 'valuesArray' export const DEPLOYMENT_QUERY_PARAM_APP_NAME = 'appName' +export const DEPLOYMENT_QUERY_PARAM_PROJECT_ID = 'projectId' export default class OneClickAppConfigPage extends ApiComponent< RouteComponentProps, { apiData: IOneClickTemplate | undefined rootDomain: string + projects: ProjectDefinition[] + selectedProjectId: string } > { private isUnmount: boolean = false @@ -36,6 +43,8 @@ export default class OneClickAppConfigPage extends ApiComponent< this.state = { apiData: undefined, rootDomain: '', + projects: [], + selectedProjectId: '', } } @@ -100,17 +109,53 @@ export default class OneClickAppConfigPage extends ApiComponent< apiData = data - return self.apiManager.getCaptainInfo() + return Promise.all([ + self.apiManager.getCaptainInfo(), + self.apiManager.getAllProjects(), + ]) }) - .then(function (captainInfo) { + .then(function ([captainInfo, projectsResponse]) { self.setState({ apiData: apiData, rootDomain: captainInfo.rootDomain, + projects: projectsResponse.projects || [], }) }) .catch(Toaster.createCatcher()) } + renderProjectSelector() { + const self = this + + if ((self.state.projects || []).length === 0) { + return undefined + } + + return ( +
+

{localize('apps.parent_project', 'Parent project')}

+
+ + + { + self.setState({ + selectedProjectId: value, + }) + }} + excludeProjectId={'NONE'} + /> + + +
+ ) + } + render() { const self = this const apiData = this.state.apiData @@ -145,6 +190,7 @@ export default class OneClickAppConfigPage extends ApiComponent<
+ {self.renderProjectSelector()} { @@ -185,8 +237,11 @@ export default class OneClickAppConfigPage extends ApiComponent< const appName = encodeURIComponent( self.props.match.params.appName ) + const projectId = encodeURIComponent( + self.state.selectedProjectId || '' + ) - const deployUrl = `/apps/oneclick/deployment?${DEPLOYMENT_QUERY_PARAM_TEMPLATE}=${templateStr}&${DEPLOYMENT_QUERY_PARAM_VALUES_ARRAY}=${valuesArrayStr}&${DEPLOYMENT_QUERY_PARAM_APP_NAME}=${appName}` + const deployUrl = `/apps/oneclick/deployment?${DEPLOYMENT_QUERY_PARAM_TEMPLATE}=${templateStr}&${DEPLOYMENT_QUERY_PARAM_VALUES_ARRAY}=${valuesArrayStr}&${DEPLOYMENT_QUERY_PARAM_APP_NAME}=${appName}&${DEPLOYMENT_QUERY_PARAM_PROJECT_ID}=${projectId}` self.props.history.push(deployUrl) }} /> diff --git a/src/containers/apps/oneclick/variables/OneClickDeploymentPage.tsx b/src/containers/apps/oneclick/variables/OneClickDeploymentPage.tsx index 8cae9ebd..886738c7 100644 --- a/src/containers/apps/oneclick/variables/OneClickDeploymentPage.tsx +++ b/src/containers/apps/oneclick/variables/OneClickDeploymentPage.tsx @@ -6,8 +6,10 @@ import ApiComponent from '../../../global/ApiComponent' import CenteredSpinner from '../../../global/CenteredSpinner' import { DEPLOYMENT_QUERY_PARAM_APP_NAME, + DEPLOYMENT_QUERY_PARAM_PROJECT_ID, DEPLOYMENT_QUERY_PARAM_TEMPLATE, DEPLOYMENT_QUERY_PARAM_VALUES_ARRAY, + ONE_CLICK_PROJECT_ID_VAR_NAME, } from './OneClickAppConfigPage' import OneClickAppDeployProgress from './OneClickAppDeployProgress' @@ -18,6 +20,7 @@ export default class OneClickDeploymentPage extends ApiComponent< appName: string template?: IOneClickTemplate valuesArray?: Array<{ key: string; value: string }> + projectId?: string } > { private isUnmount: boolean = false @@ -29,6 +32,7 @@ export default class OneClickDeploymentPage extends ApiComponent< appName: '', template: undefined, valuesArray: undefined, + projectId: undefined, } } @@ -45,6 +49,7 @@ export default class OneClickDeploymentPage extends ApiComponent< const templateStr = qs.get(DEPLOYMENT_QUERY_PARAM_TEMPLATE) const valuesArrayStr = qs.get(DEPLOYMENT_QUERY_PARAM_VALUES_ARRAY) const appName = qs.get(DEPLOYMENT_QUERY_PARAM_APP_NAME) || '' + const projectIdFromQuery = qs.get(DEPLOYMENT_QUERY_PARAM_PROJECT_ID) || '' if (!templateStr || !valuesArrayStr || !appName) { Toaster.createCatcher()( @@ -61,10 +66,28 @@ export default class OneClickDeploymentPage extends ApiComponent< value: string }> + // Prefer explicit query param; fall back to reserved values key + const projectIdFromValues = + valuesArray.find((v) => v.key === ONE_CLICK_PROJECT_ID_VAR_NAME) + ?.value || '' + const projectId = projectIdFromQuery || projectIdFromValues || '' + + // Ensure project id is present in values for backends that only read values + if ( + projectId && + !valuesArray.some((v) => v.key === ONE_CLICK_PROJECT_ID_VAR_NAME) + ) { + valuesArray.push({ + key: ONE_CLICK_PROJECT_ID_VAR_NAME, + value: projectId, + }) + } + self.setState({ appName, template, valuesArray, + projectId, }) // Start deployment immediately