return postJSON('/api/projects/create', data).catch(throwGlobalError);
}
-export function setupManualProjectCreation(data: {
- name: string;
- project: string;
- mainBranch: string;
- visibility?: Visibility;
-}) {
- return (newCodeDefinitionType?: string, newCodeDefinitionValue?: string) =>
- createProject({
- ...data,
- newCodeDefinitionType,
- newCodeDefinitionValue,
- });
-}
-
export function changeProjectDefaultVisibility(
projectVisibility: Visibility,
): Promise<void | Response> {
loading: boolean;
creatingAlmDefinition?: AlmKeys;
importProjects?: ImportProjectParam;
+ redirectTo: string;
}
const PROJECT_MODE_FOR_ALM_KEY = {
githubSettings: [],
gitlabSettings: [],
loading: true,
+ redirectTo: this.props.location.state?.from || '/projects',
};
componentDidMount() {
githubSettings,
gitlabSettings,
loading,
+ redirectTo,
} = this.state;
const branchSupportEnabled = this.props.hasFeature(Feature.BranchSupport);
<ManualProjectCreate
branchesEnabled={branchSupportEnabled}
onProjectSetupDone={this.handleProjectSetupDone}
+ onClose={() => this.props.router.push({ pathname: redirectTo })}
/>
);
}
render() {
const { location } = this.props;
- const { creatingAlmDefinition, importProjects } = this.state;
+ const { creatingAlmDefinition, importProjects, redirectTo } = this.state;
const mode: CreateProjectModes | undefined = location.query?.mode;
const isProjectSetupDone = location.query?.setncd === 'true';
const gridLayoutStyle = mode ? 'sw-col-start-2 sw-col-span-10' : 'sw-col-span-12';
{this.renderProjectCreation(mode)}
</div>
{importProjects !== undefined && isProjectSetupDone && (
- <NewCodeDefinitionSelection importProjects={importProjects} />
+ <NewCodeDefinitionSelection
+ importProjects={importProjects}
+ onClose={() => this.props.router.push({ pathname: redirectTo })}
+ redirectTo={redirectTo}
+ />
)}
{creatingAlmDefinition && (
*/
import userEvent from '@testing-library/user-event';
import { UserEvent } from '@testing-library/user-event/dist/types/setup/setup';
-import * as React from 'react';
import AlmSettingsServiceMock from '../../../../api/mocks/AlmSettingsServiceMock';
import NewCodeDefinitionServiceMock from '../../../../api/mocks/NewCodeDefinitionServiceMock';
+import { ProjectsServiceMock } from '../../../../api/mocks/ProjectsServiceMock';
import { getNewCodeDefinition } from '../../../../api/newCodeDefinition';
import { mockProject } from '../../../../helpers/mocks/projects';
-import { renderApp } from '../../../../helpers/testReactTestingUtils';
+import { mockAppState, mockCurrentUser } from '../../../../helpers/testMocks';
+import { renderAppRoutes } from '../../../../helpers/testReactTestingUtils';
import { byRole, byText } from '../../../../helpers/testSelector';
import { NewCodeDefinitionType } from '../../../../types/new-code-definition';
-import CreateProjectPage, { CreateProjectPageProps } from '../CreateProjectPage';
+import { Permissions } from '../../../../types/permissions';
+import routes from '../../../projects/routes';
+jest.mock('../../../../api/measures');
+jest.mock('../../../../api/favorites');
jest.mock('../../../../api/alm-settings');
jest.mock('../../../../api/newCodeDefinition');
jest.mock('../../../../api/project-management', () => ({
}));
jest.mock('../../../../api/components', () => ({
...jest.requireActual('../../../../api/components'),
+ searchProjects: jest.fn(),
+ getScannableProjects: jest.fn(),
doesComponentExists: jest
.fn()
.mockImplementation(({ component }) => Promise.resolve(component === 'exists')),
name: /onboarding.create_project.display_name/,
}),
projectNextButton: byRole('button', { name: 'next' }),
+ newCodeDefinitionSection: byRole('region', {
+ name: 'onboarding.create_project.new_code_definition.title',
+ }),
newCodeDefinitionHeader: byText('onboarding.create_x_project.new_code_definition.title1'),
inheritGlobalNcdRadio: byRole('radio', { name: 'new_code_definition.global_setting' }),
projectCreateButton: byRole('button', {
name: 'onboarding.create_project.new_code_definition.create_x_projects1',
}),
+ cancelButton: byRole('button', { name: 'cancel' }),
+ closeButton: byRole('button', { name: 'clear' }),
+ createProjectsButton: byRole('button', { name: 'projects.add' }),
+ createLocalProject: byRole('menuitem', { name: 'my_account.add_project.manual' }),
overrideNcdRadio: byRole('radio', { name: 'new_code_definition.specific_setting' }),
ncdOptionPreviousVersionRadio: byRole('radio', {
name: /new_code_definition.previous_version/,
}),
ncdOptionDaysInputError: byText('new_code_definition.number_days.invalid.1.90'),
projectDashboardText: byText('/dashboard?id=foo'),
+ projectsPageTitle: byRole('heading', { name: 'projects.page' }),
};
async function fillFormAndNext(displayName: string, user: UserEvent) {
let almSettingsHandler: AlmSettingsServiceMock;
let newCodePeriodHandler: NewCodeDefinitionServiceMock;
+let projectHandler: ProjectsServiceMock;
const original = window.location;
});
almSettingsHandler = new AlmSettingsServiceMock();
newCodePeriodHandler = new NewCodeDefinitionServiceMock();
+ projectHandler = new ProjectsServiceMock();
});
beforeEach(() => {
jest.clearAllMocks();
almSettingsHandler.reset();
newCodePeriodHandler.reset();
+ projectHandler.reset();
});
afterAll(() => {
expect(await ui.projectDashboardText.find()).toBeInTheDocument();
});
-function renderCreateProject(props: Partial<CreateProjectPageProps> = {}) {
- renderApp('project/create', <CreateProjectPage {...props} />, {
- navigateTo: 'project/create?mode=manual',
+it('validate the provate key field', async () => {
+ const user = userEvent.setup();
+ renderCreateProject();
+ expect(ui.manualProjectHeader.get()).toBeInTheDocument();
+
+ await user.click(ui.displayNameField.get());
+ await user.keyboard('exists');
+
+ expect(ui.projectNextButton.get()).toBeDisabled();
+ await user.click(ui.projectNextButton.get());
+});
+
+it('should navigate back to the Projects page when clicking cancel or close', async () => {
+ newCodePeriodHandler.setNewCodePeriod({ type: NewCodeDefinitionType.NumberOfDays });
+ const user = userEvent.setup();
+ renderCreateProject();
+
+ await user.click(ui.cancelButton.get());
+ expect(await ui.projectsPageTitle.find()).toBeInTheDocument();
+
+ await user.click(ui.createProjectsButton.get());
+ await user.click(await ui.createLocalProject.find());
+
+ await user.click(ui.closeButton.get());
+ expect(await ui.projectsPageTitle.find()).toBeInTheDocument();
+
+ await user.click(ui.createProjectsButton.get());
+ await user.click(await ui.createLocalProject.find());
+
+ expect(await ui.manualProjectHeader.find()).toBeInTheDocument();
+ await fillFormAndNext('testing', user);
+ expect(ui.newCodeDefinitionHeader.get()).toBeInTheDocument();
+
+ await user.click(await ui.newCodeDefinitionSection.byRole('button', { name: 'clear' }).find());
+ expect(await ui.projectsPageTitle.find()).toBeInTheDocument();
+});
+
+function renderCreateProject() {
+ renderAppRoutes('projects/create?mode=manual', routes, {
+ currentUser: mockCurrentUser({
+ permissions: { global: [Permissions.ProjectCreation] },
+ }),
+ appState: mockAppState({ canAdmin: true }),
});
}
const ui = {
nextButton: byRole('button', { name: 'next' }),
+ cancelButton: byRole('button', { name: 'cancel' }),
+ closeButton: byRole('button', { name: 'clear' }),
};
-jest.mock('../../../../api/project-management', () => ({
- setupManualProjectCreation: jest.fn(),
-}));
-
jest.mock('../../../../api/components', () => ({
doesComponentExists: jest
.fn()
).toHaveValue('test');
});
-function renderManualProjectCreate(props: Partial<ManualProjectCreate['props']> = {}) {
+function renderManualProjectCreate(props: Partial<Parameters<typeof ManualProjectCreate>[0]> = {}) {
renderComponent(
- <ManualProjectCreate branchesEnabled={false} onProjectSetupDone={jest.fn()} {...props} />,
+ <ManualProjectCreate
+ branchesEnabled={false}
+ onProjectSetupDone={jest.fn()}
+ onClose={jest.fn()}
+ {...props}
+ />,
);
}
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import { ButtonPrimary, ButtonSecondary, FlagMessage, Link, Spinner, Title } from 'design-system';
+import {
+ ButtonPrimary,
+ ButtonSecondary,
+ CloseIcon,
+ FlagMessage,
+ InteractiveIcon,
+ Link,
+ Spinner,
+ Title,
+ addGlobalErrorMessage,
+ addGlobalSuccessMessage,
+} from 'design-system';
import { omit } from 'lodash';
import * as React from 'react';
import { useEffect } from 'react';
import { useNavigate, unstable_usePrompt as usePrompt } from 'react-router-dom';
import NewCodeDefinitionSelector from '../../../../components/new-code-definition/NewCodeDefinitionSelector';
import { useDocUrl } from '../../../../helpers/docs';
-import { addGlobalErrorMessage, addGlobalSuccessMessage } from '../../../../helpers/globalMessages';
import { translate } from '../../../../helpers/l10n';
import { getProjectUrl, queryToSearch } from '../../../../helpers/urls';
import {
interface Props {
importProjects: ImportProjectParam;
+ onClose: () => void;
+ redirectTo: string;
}
export default function NewCodeDefinitionSelection(props: Props) {
- const { importProjects } = props;
+ const { importProjects, redirectTo, onClose } = props;
const [selectedDefinition, selectDefinition] = React.useState<NewCodeDefinitiondWithCompliance>();
const [failedImports, setFailedImports] = React.useState<number>(0);
const isMultipleProjects = projectCount > 1;
useEffect(() => {
+ const redirect = (projectCount: number) => {
+ if (projectCount === 1 && data) {
+ if (redirectTo === '/projects') {
+ navigate(getProjectUrl(data.project.key));
+ } else {
+ onClose();
+ }
+ } else {
+ navigate({
+ pathname: '/projects',
+ search: queryToSearch({ sort: '-creation_date' }),
+ });
+ }
+ };
+
if (mutateCount > 0 || isIdle) {
return;
}
}
if (projectCount > failedImports) {
- addGlobalSuccessMessage(
- intl.formatMessage(
- { id: 'onboarding.create_project.success' },
- {
- count: projectCount - failedImports,
- },
- ),
- );
-
- if (projectCount === 1) {
- if (data) {
- navigate(getProjectUrl(data.project.key));
- }
- } else {
- navigate({
- pathname: '/projects',
- search: queryToSearch({ sort: '-creation_date' }),
- });
+ if (redirectTo === '/projects') {
+ addGlobalSuccessMessage(
+ intl.formatMessage(
+ { id: 'onboarding.create_project.success' },
+ {
+ count: projectCount - failedImports,
+ },
+ ),
+ );
+ } else if (data) {
+ addGlobalSuccessMessage(
+ <FormattedMessage
+ defaultMessage={translate('onboarding.create_project.success.admin')}
+ id="onboarding.create_project.success.admin"
+ values={{
+ project_link: <Link to={getProjectUrl(data.project.key)}>{data.project.name}</Link>,
+ }}
+ />,
+ );
}
+ redirect(projectCount);
}
reset();
setFailedImports(0);
- }, [data, projectCount, failedImports, mutateCount, reset, intl, navigate, isIdle]);
+ }, [
+ data,
+ projectCount,
+ failedImports,
+ mutateCount,
+ reset,
+ intl,
+ navigate,
+ isIdle,
+ redirectTo,
+ onClose,
+ ]);
React.useEffect(() => {
if (isImporting) {
};
return (
- <div id="project-ncd-selection" className="sw-body-sm">
+ <section
+ aria-label={translate('onboarding.create_project.new_code_definition.title')}
+ id="project-ncd-selection"
+ className="sw-body-sm"
+ >
+ <div className="sw-flex sw-justify-between">
+ <FormattedMessage
+ id="onboarding.create_project.manual.step2"
+ defaultMessage={translate('onboarding.create_project.manual.step2')}
+ />
+ <InteractiveIcon
+ Icon={CloseIcon}
+ aria-label={intl.formatMessage({ id: 'clear' })}
+ currentColor
+ onClick={onClose}
+ size="small"
+ />
+ </div>
<Title>
<FormattedMessage
defaultMessage={translate('onboarding.create_x_project.new_code_definition.title')}
</FlagMessage>
)}
</div>
- </div>
+ </section>
);
}
import classNames from 'classnames';
import {
ButtonPrimary,
+ ButtonSecondary,
+ CloseIcon,
FlagErrorIcon,
FlagMessage,
FlagSuccessIcon,
FormField,
InputField,
+ InteractiveIcon,
Link,
Note,
+ TextError,
Title,
} from 'design-system';
import { debounce, isEmpty } from 'lodash';
import * as React from 'react';
-import { FormattedMessage } from 'react-intl';
+import { FormattedMessage, useIntl } from 'react-intl';
import { doesComponentExists } from '../../../../api/components';
import { getValue } from '../../../../api/settings';
import { useDocUrl } from '../../../../helpers/docs';
interface Props {
branchesEnabled: boolean;
onProjectSetupDone: (importProjects: ImportProjectParam) => void;
+ onClose: () => void;
}
interface State {
projectNameError?: boolean;
projectNameTouched: boolean;
projectKey: string;
- projectKeyError?: boolean;
+ projectKeyError?: 'DUPLICATE_KEY' | 'WRONG_FORMAT';
projectKeyTouched: boolean;
validatingProjectKey: boolean;
mainBranchName: string;
type ValidState = State & Required<Pick<State, 'projectKey' | 'projectName'>>;
-export default class ManualProjectCreate extends React.PureComponent<Props, State> {
- mounted = false;
+export default function ManualProjectCreate(props: Readonly<Props>) {
+ const [project, setProject] = React.useState<State>({
+ projectKey: '',
+ projectName: '',
+ projectKeyTouched: false,
+ projectNameTouched: false,
+ mainBranchName: 'main',
+ mainBranchNameTouched: false,
+ validatingProjectKey: false,
+ });
+ const intl = useIntl();
+ const docUrl = useDocUrl();
- constructor(props: Props) {
- super(props);
- this.state = {
- projectKey: '',
- projectName: '',
- projectKeyTouched: false,
- projectNameTouched: false,
- mainBranchName: 'main',
- mainBranchNameTouched: false,
- validatingProjectKey: false,
- };
- this.checkFreeKey = debounce(this.checkFreeKey, DEBOUNCE_DELAY);
- }
+ const checkFreeKey = React.useCallback(
+ debounce((key: string) => {
+ setProject((prevProject) => ({ ...prevProject, validatingProjectKey: true }));
- componentDidMount() {
- this.mounted = true;
- this.fetchMainBranchName();
- }
+ doesComponentExists({ component: key })
+ .then((alreadyExist) => {
+ setProject((prevProject) => {
+ if (key === prevProject.projectKey) {
+ return {
+ ...prevProject,
+ projectKeyError: alreadyExist ? 'DUPLICATE_KEY' : undefined,
+ validatingProjectKey: false,
+ };
+ }
+ return prevProject;
+ });
+ })
+ .catch(() => {
+ setProject((prevProject) => {
+ if (key === prevProject.projectKey) {
+ return {
+ ...prevProject,
+ projectKeyError: undefined,
+ validatingProjectKey: false,
+ };
+ }
+ return prevProject;
+ });
+ });
+ }, DEBOUNCE_DELAY),
+ [],
+ );
- componentWillUnmount() {
- this.mounted = false;
- }
+ const handleProjectKeyChange = React.useCallback(
+ (projectKey: string, fromUI = false) => {
+ const projectKeyError = validateKey(projectKey);
- fetchMainBranchName = async () => {
- const mainBranchName = await getValue({ key: GlobalSettingKeys.MainBranchName });
+ setProject((prevProject) => ({
+ ...prevProject,
+ projectKey,
+ projectKeyError,
+ projectKeyTouched: fromUI,
+ }));
- if (this.mounted && mainBranchName.value !== undefined) {
- this.setState({ mainBranchName: mainBranchName.value });
+ if (projectKeyError === undefined) {
+ checkFreeKey(projectKey);
+ }
+ },
+ [checkFreeKey],
+ );
+
+ React.useEffect(() => {
+ async function fetchMainBranchName() {
+ const { value: mainBranchName } = await getValue({ key: GlobalSettingKeys.MainBranchName });
+
+ if (mainBranchName !== undefined) {
+ setProject((prevProject) => ({
+ ...prevProject,
+ mainBranchName,
+ }));
+ }
}
- };
- checkFreeKey = (key: string) => {
- this.setState({ validatingProjectKey: true });
+ fetchMainBranchName();
+ }, []);
- doesComponentExists({ component: key })
- .then((alreadyExist) => {
- if (this.mounted && key === this.state.projectKey) {
- this.setState({
- projectKeyError: alreadyExist ? true : undefined,
- validatingProjectKey: false,
- });
- }
- })
- .catch(() => {
- if (this.mounted && key === this.state.projectKey) {
- this.setState({ projectKeyError: undefined, validatingProjectKey: false });
- }
- });
- };
+ React.useEffect(() => {
+ if (!project.projectKeyTouched) {
+ const sanitizedProjectKey = project.projectName
+ .trim()
+ .replace(PROJECT_KEY_INVALID_CHARACTERS, '-');
- canSubmit(state: State): state is ValidState {
+ handleProjectKeyChange(sanitizedProjectKey);
+ }
+ }, [project.projectName, project.projectKeyTouched, handleProjectKeyChange]);
+
+ const canSubmit = (state: State): state is ValidState => {
const { projectKey, projectKeyError, projectName, projectNameError, mainBranchName } = state;
return Boolean(
projectKeyError === undefined &&
!isEmpty(projectName) &&
!isEmpty(mainBranchName),
);
- }
+ };
- handleFormSubmit = (event: React.FormEvent<HTMLFormElement>) => {
+ const handleFormSubmit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
- const { projectKey, projectName, mainBranchName } = this.state;
- if (this.canSubmit(this.state)) {
- this.props.onProjectSetupDone({
+ const { projectKey, projectName, mainBranchName } = project;
+ if (canSubmit(project)) {
+ props.onProjectSetupDone({
creationMode: CreateProjectModes.Manual,
projects: [
{
}
};
- handleProjectKeyChange = (projectKey: string, fromUI = false) => {
- const projectKeyError = this.validateKey(projectKey);
-
- this.setState({
- projectKey,
- projectKeyError,
- projectKeyTouched: fromUI,
+ const handleProjectNameChange = (projectName: string, fromUI = false) => {
+ setProject({
+ ...project,
+ projectName,
+ projectNameError: validateName(projectName),
+ projectNameTouched: fromUI,
});
-
- if (projectKeyError === undefined) {
- this.checkFreeKey(projectKey);
- }
};
- handleProjectNameChange = (projectName: string, fromUI = false) => {
- this.setState(
- {
- projectName,
- projectNameError: this.validateName(projectName),
- projectNameTouched: fromUI,
- },
- () => {
- if (!this.state.projectKeyTouched) {
- const sanitizedProjectKey = this.state.projectName
- .trim()
- .replace(PROJECT_KEY_INVALID_CHARACTERS, '-');
- this.handleProjectKeyChange(sanitizedProjectKey);
- }
- },
- );
- };
-
- handleBranchNameChange = (mainBranchName: string, fromUI = false) => {
- this.setState({
+ const handleBranchNameChange = (mainBranchName: string, fromUI = false) => {
+ setProject({
+ ...project,
mainBranchName,
- mainBranchNameError: this.validateMainBranchName(mainBranchName),
+ mainBranchNameError: validateMainBranchName(mainBranchName),
mainBranchNameTouched: fromUI,
});
};
- validateKey = (projectKey: string) => {
+ const validateKey = (projectKey: string) => {
const result = validateProjectKey(projectKey);
- return result === ProjectKeyValidationResult.Valid ? undefined : true;
+ if (result !== ProjectKeyValidationResult.Valid) {
+ return 'WRONG_FORMAT';
+ }
+ return undefined;
};
- validateName = (projectName: string) => {
+ const validateName = (projectName: string) => {
if (isEmpty(projectName)) {
return true;
}
return undefined;
};
- validateMainBranchName = (mainBranchName: string) => {
+ const validateMainBranchName = (mainBranchName: string) => {
if (isEmpty(mainBranchName)) {
return true;
}
return undefined;
};
- render() {
- const {
- projectKey,
- projectKeyError,
- projectKeyTouched,
- projectName,
- projectNameError,
- projectNameTouched,
- validatingProjectKey,
- mainBranchName,
- mainBranchNameError,
- mainBranchNameTouched,
- } = this.state;
- const { branchesEnabled } = this.props;
+ const {
+ projectKey,
+ projectKeyError,
+ projectKeyTouched,
+ projectName,
+ projectNameError,
+ projectNameTouched,
+ validatingProjectKey,
+ mainBranchName,
+ mainBranchNameError,
+ mainBranchNameTouched,
+ } = project;
+ const { branchesEnabled } = props;
- const touched = Boolean(projectKeyTouched || projectNameTouched);
- const projectNameIsInvalid = projectNameTouched && projectNameError !== undefined;
- const projectNameIsValid = projectNameTouched && projectNameError === undefined;
- const projectKeyIsInvalid = touched && projectKeyError !== undefined;
- const projectKeyIsValid = touched && !validatingProjectKey && projectKeyError === undefined;
- const mainBranchNameIsValid = mainBranchNameTouched && mainBranchNameError === undefined;
- const mainBranchNameIsInvalid = mainBranchNameTouched && mainBranchNameError !== undefined;
+ const touched = Boolean(projectKeyTouched || projectNameTouched);
+ const projectNameIsInvalid = projectNameTouched && projectNameError !== undefined;
+ const projectNameIsValid = projectNameTouched && projectNameError === undefined;
+ const projectKeyIsInvalid = touched && projectKeyError !== undefined;
+ const projectKeyIsValid = touched && !validatingProjectKey && projectKeyError === undefined;
+ const mainBranchNameIsValid = mainBranchNameTouched && mainBranchNameError === undefined;
+ const mainBranchNameIsInvalid = mainBranchNameTouched && mainBranchNameError !== undefined;
- return (
- <div className="sw-max-w-[50%]">
- <Title>{translate('onboarding.create_project.manual.title')}</Title>
- {branchesEnabled && (
- <FlagMessage className="sw-my-4" variant="info">
- {translate('onboarding.create_project.pr_decoration.information')}
- </FlagMessage>
- )}
+ return (
+ <section
+ aria-label={translate('onboarding.create_project.manual.title')}
+ className="sw-body-sm"
+ >
+ <div className="sw-flex sw-justify-between">
+ <FormattedMessage
+ id="onboarding.create_project.manual.step1"
+ defaultMessage={translate('onboarding.create_project.manual.step1')}
+ />
+ <InteractiveIcon
+ Icon={CloseIcon}
+ aria-label={intl.formatMessage({ id: 'clear' })}
+ currentColor
+ onClick={props.onClose}
+ size="small"
+ />
+ </div>
+ <Title>{translate('onboarding.create_project.manual.title')}</Title>
+ {branchesEnabled && (
+ <FlagMessage className="sw-my-4" variant="info">
+ {translate('onboarding.create_project.pr_decoration.information')}
+ </FlagMessage>
+ )}
+ <div className="sw-max-w-[50%] sw-mt-2">
<form
id="create-project-manual"
className="sw-flex-col sw-body-sm"
- onSubmit={this.handleFormSubmit}
+ onSubmit={handleFormSubmit}
>
<FormField
htmlFor="project-name"
id="project-name"
maxLength={PROJECT_NAME_MAX_LEN}
minLength={1}
- onChange={(e) => this.handleProjectNameChange(e.currentTarget.value, true)}
+ onChange={(e) => handleProjectNameChange(e.currentTarget.value, true)}
type="text"
value={projectName}
autoFocus
size="large"
id="project-key"
minLength={1}
- onChange={(e) => this.handleProjectKeyChange(e.currentTarget.value, true)}
+ onChange={(e) => handleProjectKeyChange(e.currentTarget.value, true)}
type="text"
value={projectKey}
isInvalid={projectKeyIsInvalid}
{projectKeyIsInvalid && <FlagErrorIcon className="sw-ml-2" />}
{projectKeyIsValid && <FlagSuccessIcon className="sw-ml-2" />}
</div>
- <Note className="sw-mt-2">
- {translate('onboarding.create_project.project_key.description')}
+ <Note className="sw-flex-col sw-mt-2">
+ {projectKeyError === 'DUPLICATE_KEY' && (
+ <TextError
+ text={translate('onboarding.create_project.project_key.duplicate_key')}
+ />
+ )}
+ {!isEmpty(projectKey) && projectKeyError === 'WRONG_FORMAT' && (
+ <TextError text={translate('onboarding.create_project.project_key.wrong_format')} />
+ )}
+ <p>{translate('onboarding.create_project.project_key.description')}</p>
</Note>
</FormField>
size="large"
id="main-branch-name"
minLength={1}
- onChange={(e) => this.handleBranchNameChange(e.currentTarget.value, true)}
+ onChange={(e) => handleBranchNameChange(e.currentTarget.value, true)}
type="text"
value={mainBranchName}
isInvalid={mainBranchNameIsInvalid}
{mainBranchNameIsValid && <FlagSuccessIcon className="sw-ml-2" />}
</div>
<Note className="sw-mt-2">
- <FormattedMessageWithDocLink />
+ <FormattedMessage
+ id="onboarding.create_project.main_branch_name.description"
+ defaultMessage={translate('onboarding.create_project.main_branch_name.description')}
+ values={{
+ learn_more: (
+ <Link to={docUrl('/analyzing-source-code/branches/branch-analysis')}>
+ {translate('learn_more')}
+ </Link>
+ ),
+ }}
+ />
</Note>
</FormField>
- <ButtonPrimary
- type="submit"
- className="sw-mt-4 sw-mb-4"
- disabled={!this.canSubmit(this.state)}
- >
+ <ButtonSecondary className="sw-mt-4 sw-mr-4" onClick={props.onClose} type="button">
+ {intl.formatMessage({ id: 'cancel' })}
+ </ButtonSecondary>
+ <ButtonPrimary className="sw-mt-4" type="submit" disabled={!canSubmit(project)}>
{translate('next')}
</ButtonPrimary>
</form>
</div>
- );
- }
-}
-
-function FormattedMessageWithDocLink() {
- const docUrl = useDocUrl();
-
- return (
- <FormattedMessage
- id="onboarding.create_project.main_branch_name.description"
- defaultMessage={translate('onboarding.create_project.main_branch_name.description')}
- values={{
- learn_more: (
- <Link to={docUrl('/analyzing-source-code/branches/branch-analysis')}>
- {translate('learn_more')}
- </Link>
- ),
- }}
- />
+ </section>
);
}
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2024 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-import * as React from 'react';
-import { FormattedMessage } from 'react-intl';
-import { createProject } from '../../api/project-management';
-import { getValue } from '../../api/settings';
-import Link from '../../components/common/Link';
-import VisibilitySelector from '../../components/common/VisibilitySelector';
-import Modal from '../../components/controls/Modal';
-import { ResetButtonLink, SubmitButton } from '../../components/controls/buttons';
-import { Alert } from '../../components/ui/Alert';
-import MandatoryFieldMarker from '../../components/ui/MandatoryFieldMarker';
-import MandatoryFieldsExplanation from '../../components/ui/MandatoryFieldsExplanation';
-import { translate } from '../../helpers/l10n';
-import { getProjectUrl } from '../../helpers/urls';
-import { Visibility } from '../../types/component';
-import { GlobalSettingKeys } from '../../types/settings';
-
-interface Props {
- defaultProjectVisibility?: Visibility;
- onClose: () => void;
- onProjectCreated: () => void;
-}
-
-interface State {
- createdProject?: { key: string; name: string };
- key: string;
- loading: boolean;
- name: string;
- visibility?: Visibility;
- // add index declaration to be able to do `this.setState({ [name]: value });`
- [x: string]: any;
- mainBranchName: string;
-}
-
-export default class CreateProjectForm extends React.PureComponent<Props, State> {
- closeButton?: HTMLElement | null;
- mounted = false;
-
- constructor(props: Props) {
- super(props);
- this.state = {
- key: '',
- loading: false,
- name: '',
- visibility: props.defaultProjectVisibility,
- mainBranchName: 'main',
- };
- }
-
- componentDidMount() {
- this.mounted = true;
- this.fetchMainBranchName();
- }
-
- componentDidUpdate() {
- // wrap with `setTimeout` because of https://github.com/reactjs/react-modal/issues/338
- setTimeout(() => {
- if (this.closeButton) {
- this.closeButton.focus();
- }
- }, 0);
- }
-
- componentWillUnmount() {
- this.mounted = false;
- }
-
- fetchMainBranchName = async () => {
- const mainBranchName = await getValue({ key: GlobalSettingKeys.MainBranchName });
-
- if (this.mounted && mainBranchName.value !== undefined) {
- this.setState({ mainBranchName: mainBranchName.value });
- }
- };
-
- handleInputChange = (event: React.SyntheticEvent<HTMLInputElement>) => {
- const { name, value } = event.currentTarget;
- this.setState({ [name]: value });
- };
-
- handleVisibilityChange = (visibility: Visibility) => {
- this.setState({ visibility });
- };
-
- handleFormSubmit = (event: React.SyntheticEvent<HTMLFormElement>) => {
- event.preventDefault();
- const { name, key, mainBranchName, visibility } = this.state;
-
- const data = {
- name,
- project: key,
- mainBranch: mainBranchName,
- visibility,
- };
-
- this.setState({ loading: true });
- createProject(data).then(
- (response) => {
- if (this.mounted) {
- this.setState({ createdProject: response.project, loading: false });
- this.props.onProjectCreated();
- }
- },
- () => {
- if (this.mounted) {
- this.setState({ loading: false });
- }
- },
- );
- };
-
- render() {
- const { defaultProjectVisibility } = this.props;
- const { createdProject } = this.state;
- const header = translate('qualifiers.create.TRK');
-
- return (
- <Modal contentLabel={header} onRequestClose={this.props.onClose}>
- {createdProject ? (
- <div>
- <header className="modal-head">
- <h2>{header}</h2>
- </header>
-
- <div className="modal-body">
- <Alert variant="success">
- <FormattedMessage
- defaultMessage={translate(
- 'projects_management.project_has_been_successfully_created',
- )}
- id="projects_management.project_has_been_successfully_created"
- values={{
- project: (
- <Link to={getProjectUrl(createdProject.key)}>{createdProject.name}</Link>
- ),
- }}
- />
- </Alert>
- </div>
-
- <footer className="modal-foot">
- <ResetButtonLink
- id="create-project-close"
- innerRef={(node) => (this.closeButton = node)}
- onClick={this.props.onClose}
- >
- {translate('close')}
- </ResetButtonLink>
- </footer>
- </div>
- ) : (
- <form id="create-project-form" onSubmit={this.handleFormSubmit}>
- <header className="modal-head">
- <h2>{header}</h2>
- </header>
-
- <div className="modal-body">
- <MandatoryFieldsExplanation className="modal-field" />
- <div className="modal-field">
- <label htmlFor="create-project-name">
- {translate('onboarding.create_project.display_name')}
- <MandatoryFieldMarker />
- </label>
- <input
- autoFocus
- id="create-project-name"
- maxLength={2000}
- name="name"
- onChange={this.handleInputChange}
- required
- type="text"
- value={this.state.name}
- />
- </div>
- <div className="modal-field">
- <label htmlFor="create-project-key">
- {translate('onboarding.create_project.project_key')}
- <MandatoryFieldMarker />
- </label>
- <input
- id="create-project-key"
- maxLength={400}
- name="key"
- onChange={this.handleInputChange}
- required
- type="text"
- value={this.state.key}
- />
- </div>
- <div className="modal-field">
- <label htmlFor="create-project-main-branch-name">
- {translate('onboarding.create_project.main_branch_name')}
- <MandatoryFieldMarker />
- </label>
- <input
- id="create-project-main-branch-name"
- maxLength={400}
- name="mainBranchName"
- onChange={this.handleInputChange}
- required
- type="text"
- value={this.state.mainBranchName}
- />
- </div>
- <div className="modal-field">
- <label>{translate('visibility')}</label>
- <VisibilitySelector
- canTurnToPrivate={defaultProjectVisibility !== undefined}
- className="little-spacer-top"
- onChange={this.handleVisibilityChange}
- visibility={this.state.visibility}
- />
- </div>
- </div>
-
- <footer className="modal-foot">
- {this.state.loading && <i className="spinner spacer-right" />}
- <SubmitButton disabled={this.state.loading} id="create-project-submit">
- {translate('create')}
- </SubmitButton>
- <ResetButtonLink id="create-project-cancel" onClick={this.props.onClose}>
- {translate('cancel')}
- </ResetButtonLink>
- </footer>
- </form>
- )}
- </Modal>
- );
- }
-}
*/
import * as React from 'react';
import { useState } from 'react';
+import { useLocation, useNavigate } from 'react-router-dom';
import { Button, EditButton } from '../../components/controls/buttons';
import { translate } from '../../helpers/l10n';
import { Visibility } from '../../types/component';
export interface Props {
defaultProjectVisibility?: Visibility;
hasProvisionPermission?: boolean;
- onProjectCreate: () => void;
onChangeDefaultProjectVisibility: (visibility: Visibility) => void;
}
export default function Header(props: Readonly<Props>) {
const [visibilityForm, setVisibilityForm] = useState(false);
+ const navigate = useNavigate();
+ const location = useLocation();
const { defaultProjectVisibility, hasProvisionPermission } = props;
</span>
{hasProvisionPermission && (
- <Button id="create-project" onClick={props.onProjectCreate}>
+ <Button
+ id="create-project"
+ onClick={() =>
+ navigate('/projects/create?mode=manual', {
+ state: { from: location.pathname },
+ })
+ }
+ >
{translate('qualifiers.create.TRK')}
</Button>
)}
import { Permissions } from '../../types/permissions';
import { SettingsKey } from '../../types/settings';
import { LoggedInUser } from '../../types/users';
-import CreateProjectForm from './CreateProjectForm';
import Header from './Header';
import Projects from './Projects';
import Search from './Search';
interface State {
analyzedBefore?: Date;
- createProjectForm: boolean;
defaultProjectVisibility?: Visibility;
page: number;
projects: Project[];
constructor(props: Props) {
super(props);
this.state = {
- createProjectForm: false,
ready: false,
projects: [],
provisioned: false,
this.setState({ selection: [] });
};
- openCreateProjectForm = () => {
- this.setState({ createProjectForm: true });
- };
-
- closeCreateProjectForm = () => {
- this.setState({ createProjectForm: false });
- };
-
render() {
const { currentUser } = this.props;
const { defaultProjectVisibility } = this.state;
defaultProjectVisibility={defaultProjectVisibility}
hasProvisionPermission={hasGlobalPermission(currentUser, Permissions.ProjectCreation)}
onChangeDefaultProjectVisibility={this.handleDefaultProjectVisibilityChange}
- onProjectCreate={this.openCreateProjectForm}
/>
<Search
ready={this.state.ready}
total={this.state.total}
/>
-
- {this.state.createProjectForm && (
- <CreateProjectForm
- defaultProjectVisibility={defaultProjectVisibility}
- onClose={this.closeCreateProjectForm}
- onProjectCreated={this.requestProjects}
- />
- )}
</main>
);
}
createProject: byRole('button', {
name: 'qualifiers.create.TRK',
}),
-
+ manualProjectHeader: byText('onboarding.create_project.manual.title'),
+ displayNameField: byRole('textbox', {
+ name: /onboarding.create_project.display_name/,
+ }),
+ projectNextButton: byRole('button', { name: 'next' }),
+ newCodeDefinitionHeader: byText('onboarding.create_x_project.new_code_definition.title1'),
+ projectCreateButton: byRole('button', {
+ name: 'onboarding.create_project.new_code_definition.create_x_projects1',
+ }),
visibilityFilter: byRole('combobox', { name: 'projects_management.filter_by_visibility' }),
qualifierFilter: byRole('combobox', { name: 'projects_management.filter_by_component' }),
analysisDateFilter: byPlaceholderText('last_analysis_before'),
});
it('should create project', async () => {
- settingsHandler.set(GlobalSettingKeys.MainBranchName, 'main');
const user = userEvent.setup();
+ settingsHandler.set(GlobalSettingKeys.MainBranchName, 'main');
renderProjectManagementApp({}, { permissions: { global: [Permissions.ProjectCreation] } });
await waitFor(() => expect(ui.row.getAll()).toHaveLength(5));
- await user.click(await ui.createProject.find());
- expect(ui.createDialog.get()).toBeInTheDocument();
- expect(ui.createDialog.by(ui.privateVisibility).get()).not.toBeChecked();
- await user.click(ui.createDialog.by(ui.privateVisibility).get());
- expect(ui.createDialog.by(ui.privateVisibility).get()).not.toBeChecked();
- await user.click(ui.createDialog.by(ui.cancel).get());
-
- expect(await ui.defaultVisibility.find()).toBeInTheDocument();
- expect(ui.defaultVisibility.get()).toHaveTextContent('—');
- await user.click(ui.editDefaultVisibility.get());
- expect(await ui.changeDefaultVisibilityDialog.find()).toBeInTheDocument();
- expect(ui.defaultVisibilityWarning.get()).not.toHaveTextContent('.github');
- await user.click(ui.changeDefaultVisibilityDialog.by(ui.visibilityPublicRadio).get());
- await user.click(ui.changeDefaultVisibilityDialog.by(ui.submitDefaultVisibilityChange).get());
- expect(ui.changeDefaultVisibilityDialog.query()).not.toBeInTheDocument();
- expect(ui.defaultVisibility.get()).toHaveTextContent('visibility.public');
-
- await user.click(await ui.createProject.find());
- expect(ui.createDialog.get()).toBeInTheDocument();
- await user.click(ui.createDialog.by(ui.privateVisibility).get());
- expect(ui.createDialog.by(ui.privateVisibility).get()).toBeChecked();
- await user.type(ui.createDialog.by(ui.displayNameInput).get(), 'a Test');
- await user.type(ui.createDialog.by(ui.projectKeyInput).get(), 'test');
- expect(ui.createDialog.by(ui.mainBranchNameInput).get()).toHaveValue('main');
- await user.click(ui.createDialog.by(ui.create).get());
- expect(ui.createDialog.by(ui.successMsg).get()).toBeInTheDocument();
- await user.click(ui.createDialog.by(ui.close).get());
- expect(ui.row.getAll()).toHaveLength(6);
- expect(ui.row.getAll()[1]).toHaveTextContent('qualifier.TRKa Testvisibility.privatetest—');
+
+ await user.click(ui.createProject.get());
+
+ expect(byText('/projects/create?mode=manual').get()).toBeInTheDocument();
});
it('should edit permissions of single project', async () => {
onboarding.project_analysis.description=We initialized your project on SonarQube, now it's up to you to launch analyses!
onboarding.project_analysis.guide_to_integrate_pipelines=follow the guide to integrating with Pipelines
+onboarding.create_project.manual.step1=1 of 2
+onboarding.create_project.manual.step2=2 of 2
onboarding.create_project.manual.title=Create a local project
onboarding.create_project.select_method=How do you want to create your project?
onboarding.create_project.select_method.manually=Are you just testing or have an advanced use-case? Create a local project.
onboarding.create_project.alm_not_configured=Contact your admin to set up the global configuration allowing you to import project from this DevOps Platform
onboarding.create_project.check_alm_supported=Checking if available
onboarding.create_project.project_key=Project key
+onboarding.create_project.project_key.duplicate_key=The project key name is already taken.
+onboarding.create_project.project_key.wrong_format=The provided value doesn't match the expected format.
onboarding.create_project.project_key.description=The project key is a unique identifier for your project. It may contain up to 400 characters. Allowed characters are alphanumeric, '-' (dash), '_' (underscore), '.' (period) and ':' (colon), with at least one non-digit.
onboarding.create_project.project_key.error.empty=You must provide at least one character.
onboarding.create_project.project_key.error.too_long=The provided key is too long.
onboarding.create_project.new_code_definition.title=Set up project for Clean as You Code
onboarding.create_x_project.new_code_definition.title=Set up {count, plural, one {project} other {# projects}} for Clean as You Code
+onboarding.create_project.new_code_definition.title=Set up project for Clean as You Code
onboarding.create_project.new_code_definition.description=The new code definition sets which part of your code will be considered new code. This helps you focus attention on the most recent changes to your project, enabling you to follow the Clean as You Code methodology. Learn more: {link}
onboarding.create_project.new_code_definition.description.link=Defining New Code
onboarding.create_project.new_code_definition.create_x_projects=Create {count, plural, one {project} other {# projects}}
onboarding.create_projects.new_code_definition.change_info=You can change this setting for each project individually at any time in the project administration settings.
onboarding.create_project.success=Your {count, plural, one {project has} other {# projects have}} been created.
+onboarding.create_project.success.admin=Project {project_link} has been successfully created.
onboarding.create_project.failure=Import of {count, plural, one {# project} other {# projects}} failed.
onboarding.token.header=Provide a token