You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

ManualProjectCreate.tsx 7.0KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221
  1. /*
  2. * SonarQube
  3. * Copyright (C) 2009-2024 SonarSource SA
  4. * mailto:info AT sonarsource DOT com
  5. *
  6. * This program is free software; you can redistribute it and/or
  7. * modify it under the terms of the GNU Lesser General Public
  8. * License as published by the Free Software Foundation; either
  9. * version 3 of the License, or (at your option) any later version.
  10. *
  11. * This program is distributed in the hope that it will be useful,
  12. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
  14. * Lesser General Public License for more details.
  15. *
  16. * You should have received a copy of the GNU Lesser General Public License
  17. * along with this program; if not, write to the Free Software Foundation,
  18. * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  19. */
  20. import classNames from 'classnames';
  21. import {
  22. ButtonPrimary,
  23. ButtonSecondary,
  24. CloseIcon,
  25. FlagErrorIcon,
  26. FlagMessage,
  27. FlagSuccessIcon,
  28. FormField,
  29. InputField,
  30. InteractiveIcon,
  31. Link,
  32. Note,
  33. Title,
  34. } from 'design-system';
  35. import { isEmpty } from 'lodash';
  36. import * as React from 'react';
  37. import { FormattedMessage, useIntl } from 'react-intl';
  38. import { getValue } from '../../../../api/settings';
  39. import { useDocUrl } from '../../../../helpers/docs';
  40. import { translate } from '../../../../helpers/l10n';
  41. import { GlobalSettingKeys } from '../../../../types/settings';
  42. import { ImportProjectParam } from '../CreateProjectPage';
  43. import ProjectValidation, { ProjectData } from '../components/ProjectValidation';
  44. import { CreateProjectModes } from '../types';
  45. interface Props {
  46. branchesEnabled: boolean;
  47. onProjectSetupDone: (importProjects: ImportProjectParam) => void;
  48. onClose: () => void;
  49. }
  50. interface MainBranchState {
  51. mainBranchName: string;
  52. mainBranchNameError?: boolean;
  53. mainBranchNameTouched: boolean;
  54. }
  55. type ValidState = ProjectData & Required<Pick<ProjectData, 'key' | 'name'>>;
  56. export default function ManualProjectCreate(props: Readonly<Props>) {
  57. const [mainBranch, setMainBranch] = React.useState<MainBranchState>({
  58. mainBranchName: 'main',
  59. mainBranchNameTouched: false,
  60. });
  61. const [project, setProject] = React.useState<ProjectData>({
  62. hasError: false,
  63. key: '',
  64. name: '',
  65. touched: false,
  66. });
  67. const intl = useIntl();
  68. const docUrl = useDocUrl();
  69. React.useEffect(() => {
  70. async function fetchMainBranchName() {
  71. const { value: mainBranchName } = await getValue({ key: GlobalSettingKeys.MainBranchName });
  72. if (mainBranchName !== undefined) {
  73. setMainBranch((prevBranchName) => ({
  74. ...prevBranchName,
  75. mainBranchName,
  76. }));
  77. }
  78. }
  79. fetchMainBranchName();
  80. }, []);
  81. const canSubmit = (
  82. mainBranch: MainBranchState,
  83. projectData: ProjectData,
  84. ): projectData is ValidState => {
  85. const { mainBranchName } = mainBranch;
  86. const { key, name, hasError } = projectData;
  87. return Boolean(!hasError && !isEmpty(key) && !isEmpty(name) && !isEmpty(mainBranchName));
  88. };
  89. const handleFormSubmit = (event: React.FormEvent<HTMLFormElement>) => {
  90. event.preventDefault();
  91. if (canSubmit(mainBranch, project)) {
  92. props.onProjectSetupDone({
  93. creationMode: CreateProjectModes.Manual,
  94. monorepo: false,
  95. projects: [
  96. {
  97. project: project.key,
  98. name: (project.name ?? project.key).trim(),
  99. mainBranch: mainBranchName,
  100. },
  101. ],
  102. });
  103. }
  104. };
  105. const handleBranchNameChange = (mainBranchName: string, fromUI = false) => {
  106. setMainBranch({
  107. mainBranchName,
  108. mainBranchNameError: validateMainBranchName(mainBranchName),
  109. mainBranchNameTouched: fromUI,
  110. });
  111. };
  112. const validateMainBranchName = (mainBranchName: string) => {
  113. if (isEmpty(mainBranchName)) {
  114. return true;
  115. }
  116. return undefined;
  117. };
  118. const { mainBranchName, mainBranchNameError, mainBranchNameTouched } = mainBranch;
  119. const { branchesEnabled } = props;
  120. const mainBranchNameIsValid = mainBranchNameTouched && mainBranchNameError === undefined;
  121. const mainBranchNameIsInvalid = mainBranchNameTouched && mainBranchNameError !== undefined;
  122. return (
  123. <section
  124. aria-label={translate('onboarding.create_project.manual.title')}
  125. className="sw-body-sm"
  126. >
  127. <div className="sw-flex sw-justify-between">
  128. <FormattedMessage
  129. id="onboarding.create_project.manual.step1"
  130. defaultMessage={translate('onboarding.create_project.manual.step1')}
  131. />
  132. <InteractiveIcon
  133. Icon={CloseIcon}
  134. aria-label={intl.formatMessage({ id: 'clear' })}
  135. currentColor
  136. onClick={props.onClose}
  137. size="small"
  138. />
  139. </div>
  140. <Title>{translate('onboarding.create_project.manual.title')}</Title>
  141. {branchesEnabled && (
  142. <FlagMessage className="sw-my-4" variant="info">
  143. {translate('onboarding.create_project.pr_decoration.information')}
  144. </FlagMessage>
  145. )}
  146. <div className="sw-max-w-[50%] sw-mt-2">
  147. <form
  148. id="create-project-manual"
  149. className="sw-flex-col sw-body-sm"
  150. onSubmit={handleFormSubmit}
  151. >
  152. <ProjectValidation onChange={setProject} />
  153. <FormField
  154. htmlFor="main-branch-name"
  155. label={translate('onboarding.create_project.main_branch_name')}
  156. required
  157. >
  158. <div>
  159. <InputField
  160. className={classNames({
  161. 'js__is-invalid': mainBranchNameIsInvalid,
  162. })}
  163. size="large"
  164. id="main-branch-name"
  165. minLength={1}
  166. onChange={(e) => handleBranchNameChange(e.currentTarget.value, true)}
  167. type="text"
  168. value={mainBranchName}
  169. isInvalid={mainBranchNameIsInvalid}
  170. isValid={mainBranchNameIsValid}
  171. required
  172. />
  173. {mainBranchNameIsInvalid && <FlagErrorIcon className="sw-ml-2" />}
  174. {mainBranchNameIsValid && <FlagSuccessIcon className="sw-ml-2" />}
  175. </div>
  176. <Note className="sw-mt-2">
  177. <FormattedMessage
  178. id="onboarding.create_project.main_branch_name.description"
  179. defaultMessage={translate('onboarding.create_project.main_branch_name.description')}
  180. values={{
  181. learn_more: (
  182. <Link to={docUrl('/analyzing-source-code/branches/branch-analysis')}>
  183. {translate('learn_more')}
  184. </Link>
  185. ),
  186. }}
  187. />
  188. </Note>
  189. </FormField>
  190. <ButtonSecondary className="sw-mt-4 sw-mr-4" onClick={props.onClose} type="button">
  191. {intl.formatMessage({ id: 'cancel' })}
  192. </ButtonSecondary>
  193. <ButtonPrimary
  194. className="sw-mt-4"
  195. type="submit"
  196. disabled={!canSubmit(mainBranch, project)}
  197. >
  198. {translate('next')}
  199. </ButtonPrimary>
  200. </form>
  201. </div>
  202. </section>
  203. );
  204. }