Browse Source

SONAR-21422 Migrating projects management page to adopt new UI

tags/10.4.0.87286
Revanshu Paliwal 5 months ago
parent
commit
c7d0bb440b
19 changed files with 364 additions and 797 deletions
  1. 1
    1
      server/sonar-web/design-system/src/components/input/InputSearch.tsx
  2. 1
    0
      server/sonar-web/src/main/js/app/components/GlobalContainer.tsx
  3. 1
    0
      server/sonar-web/src/main/js/app/index.ts
  4. 16
    20
      server/sonar-web/src/main/js/apps/permissions/project/components/ApplyTemplate.tsx
  5. 62
    53
      server/sonar-web/src/main/js/apps/projectsManagement/BulkApplyTemplateModal.tsx
  6. 45
    45
      server/sonar-web/src/main/js/apps/projectsManagement/ChangeDefaultVisibilityForm.tsx
  7. 22
    23
      server/sonar-web/src/main/js/apps/projectsManagement/DeleteModal.tsx
  8. 33
    31
      server/sonar-web/src/main/js/apps/projectsManagement/Header.tsx
  9. 50
    46
      server/sonar-web/src/main/js/apps/projectsManagement/ProjectManagementApp.tsx
  10. 0
    43
      server/sonar-web/src/main/js/apps/projectsManagement/ProjectRow.css
  11. 20
    33
      server/sonar-web/src/main/js/apps/projectsManagement/ProjectRow.tsx
  12. 15
    22
      server/sonar-web/src/main/js/apps/projectsManagement/ProjectRowActions.tsx
  13. 29
    28
      server/sonar-web/src/main/js/apps/projectsManagement/Projects.tsx
  14. 19
    18
      server/sonar-web/src/main/js/apps/projectsManagement/RestoreAccessModal.tsx
  15. 44
    38
      server/sonar-web/src/main/js/apps/projectsManagement/Search.tsx
  16. 6
    6
      server/sonar-web/src/main/js/apps/projectsManagement/__tests__/ProjectManagementApp-it.tsx
  17. 0
    87
      server/sonar-web/src/main/js/components/controls/DateInput.css
  18. 0
    195
      server/sonar-web/src/main/js/components/controls/DateInput.tsx
  19. 0
    108
      server/sonar-web/src/main/js/components/controls/DateRangeInput.tsx

+ 1
- 1
server/sonar-web/design-system/src/components/input/InputSearch.tsx View File

@@ -75,7 +75,6 @@ export function InputSearch(props: PropsWithChildren<Props>) {
value: parentValue,
searchInputAriaLabel,
} = props;

const intl = useIntl();
const input = useRef<null | HTMLElement>(null);
const [value, setValue] = useState(parentValue ?? '');
@@ -84,6 +83,7 @@ export function InputSearch(props: PropsWithChildren<Props>) {
() =>
debounce((val: string) => {
onChange(val);
setDirty(false);
}, DEBOUNCE_DELAY),
[onChange],
);

+ 1
- 0
server/sonar-web/src/main/js/app/components/GlobalContainer.tsx View File

@@ -85,6 +85,7 @@ const TEMP_PAGELIST_WITH_NEW_BACKGROUND_WHITE = [
'/admin/settings/encryption',
'/admin/extension/license/support',
'/admin/audit',
'/admin/projects_management',
];

export default function GlobalContainer() {

+ 1
- 0
server/sonar-web/src/main/js/app/index.ts View File

@@ -23,6 +23,7 @@
import 'core-js/stable';
/* */
import axios from 'axios';
import 'react-day-picker/dist/style.css';
import { getAvailableFeatures } from '../api/features';
import { getGlobalNavigation } from '../api/navigation';
import { getCurrentUser } from '../api/users';

+ 16
- 20
server/sonar-web/src/main/js/apps/permissions/project/components/ApplyTemplate.tsx View File

@@ -27,7 +27,6 @@ import {
} from 'design-system';
import * as React from 'react';
import { applyTemplateToProject, getPermissionTemplates } from '../../../../api/permissions';
import MandatoryFieldsExplanation from '../../../../components/ui/MandatoryFieldsExplanation';
import { translate, translateWithParameters } from '../../../../helpers/l10n';
import { PermissionTemplate } from '../../../../types/types';

@@ -136,25 +135,22 @@ export default class ApplyTemplate extends React.PureComponent<Props, State> {
)}

{!this.state.done && !this.state.loading && (
<>
<MandatoryFieldsExplanation className="sw-mb-4" />
<FormField
label={translate('template')}
required
htmlFor="project-permissions-template-input"
>
{this.state.permissionTemplates && (
<InputSelect
size="full"
id="project-permissions-template"
inputId="project-permissions-template-input"
onChange={this.handlePermissionTemplateChange}
options={options}
value={options.filter((o) => o.value === this.state.permissionTemplate)}
/>
)}
</FormField>
</>
<FormField
label={translate('template')}
required
htmlFor="project-permissions-template-input"
>
{this.state.permissionTemplates && (
<InputSelect
size="full"
id="project-permissions-template"
inputId="project-permissions-template-input"
onChange={this.handlePermissionTemplateChange}
options={options}
value={options.filter((o) => o.value === this.state.permissionTemplate)}
/>
)}
</FormField>
)}
</div>
</form>

+ 62
- 53
server/sonar-web/src/main/js/apps/projectsManagement/BulkApplyTemplateModal.tsx View File

@@ -17,14 +17,18 @@
* 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,
FlagMessage,
FormField,
InputSelect,
LabelValueSelectOption,
Modal,
Spinner,
} from 'design-system';
import * as React from 'react';
import { bulkApplyTemplate, getPermissionTemplates } from '../../api/permissions';
import { Project } from '../../api/project-management';
import Modal from '../../components/controls/Modal';
import Select from '../../components/controls/Select';
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 { toISO8601WithOffsetString } from '../../helpers/dates';
import { addGlobalErrorMessageFromAPI } from '../../helpers/globalMessages';
@@ -49,6 +53,8 @@ interface State {
submitting: boolean;
}

const FORM_ID = 'bulk-apply-template-form';

export default class BulkApplyTemplateModal extends React.PureComponent<Props, State> {
mounted = false;
state: State = { done: false, loading: true, submitting: false };
@@ -83,7 +89,8 @@ export default class BulkApplyTemplateModal extends React.PureComponent<Props, S
);
}

handleConfirmClick = () => {
handleConfirmClick = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
const { analyzedBefore } = this.props;
const { permissionTemplate } = this.state;
if (permissionTemplate) {
@@ -118,7 +125,7 @@ export default class BulkApplyTemplateModal extends React.PureComponent<Props, S
}
};

handlePermissionTemplateChange = ({ value }: { value: string }) => {
handlePermissionTemplateChange = ({ value }: LabelValueSelectOption<string>) => {
this.setState({ permissionTemplate: value });
};

@@ -132,15 +139,15 @@ export default class BulkApplyTemplateModal extends React.PureComponent<Props, S

if (isSelectionOnlyManaged) {
return (
<Alert variant="error">
<FlagMessage variant="error" className="sw-my-2">
{translate(
'permission_templates.bulk_apply_permission_template.apply_to_only_github_projects',
)}
</Alert>
</FlagMessage>
);
} else if (isSelectionOnlyLocal) {
return (
<Alert variant="warning">
<FlagMessage variant="warning" className="sw-my-2">
{this.props.selection.length
? translateWithParameters(
'permission_templates.bulk_apply_permission_template.apply_to_selected',
@@ -150,11 +157,11 @@ export default class BulkApplyTemplateModal extends React.PureComponent<Props, S
'permission_templates.bulk_apply_permission_template.apply_to_all',
this.props.total,
)}
</Alert>
</FlagMessage>
);
}
return (
<Alert variant="warning">
<FlagMessage variant="warning" className="sw-my-2">
{translateWithParameters(
'permission_templates.bulk_apply_permission_template.apply_to_selected',
localProjects.length,
@@ -164,7 +171,7 @@ export default class BulkApplyTemplateModal extends React.PureComponent<Props, S
'permission_templates.bulk_apply_permission_template.apply_to_github_projects',
managedProjects.length,
)}
</Alert>
</FlagMessage>
);
};

@@ -174,20 +181,17 @@ export default class BulkApplyTemplateModal extends React.PureComponent<Props, S
? this.state.permissionTemplates.map((t) => ({ label: t.name, value: t.id }))
: [];
return (
<div className="modal-field">
<label htmlFor="bulk-apply-template-input">
{translate('template')}
<MandatoryFieldMarker />
</label>
<Select
<FormField htmlFor="bulk-apply-template-input" label={translate('template')} required>
<InputSelect
id="bulk-apply-template"
inputId="bulk-apply-template-input"
isDisabled={this.state.submitting || isSelectionOnlyManaged}
onChange={this.handlePermissionTemplateChange}
options={options}
value={options.find((option) => option.value === this.state.permissionTemplate)}
size="auto"
/>
</div>
</FormField>
);
};

@@ -196,44 +200,49 @@ export default class BulkApplyTemplateModal extends React.PureComponent<Props, S
const header = translate('permission_templates.bulk_apply_permission_template');

const isSelectionOnlyManaged = this.props.selection.every((s) => s.managed === true);
const body = (
<form id={FORM_ID} onSubmit={this.handleConfirmClick}>
{done && (
<FlagMessage variant="success">
{translate('projects_role.apply_template.success')}
</FlagMessage>
)}

return (
<Modal contentLabel={header} onRequestClose={this.props.onClose} size="small">
<header className="modal-head">
<h2>{header}</h2>
</header>

<div className="modal-body">
{done && (
<Alert variant="success">{translate('projects_role.apply_template.success')}</Alert>
)}

{loading && <i className="spinner" />}

{!loading && !done && permissionTemplates && (
<>
<MandatoryFieldsExplanation className="spacer-bottom" />
{this.renderWarning()}
{this.renderSelect(isSelectionOnlyManaged)}
</>
)}
</div>
<Spinner loading={loading} />

<footer className="modal-foot">
{submitting && <i className="spinner spacer-right" />}
{!loading && !done && permissionTemplates && (
<SubmitButton
{!loading && !done && permissionTemplates && (
<>
<MandatoryFieldsExplanation className="sw-mb-2" />
{this.renderWarning()}
{this.renderSelect(isSelectionOnlyManaged)}
</>
)}
</form>
);
return (
<Modal
isScrollable={false}
isOverflowVisible
headerTitle={header}
onClose={this.props.onClose}
loading={submitting}
body={body}
primaryButton={
!loading &&
!done &&
permissionTemplates && (
<ButtonPrimary
autoFocus
disabled={submitting || isSelectionOnlyManaged}
onClick={this.handleConfirmClick}
form={FORM_ID}
type="submit"
>
{translate('apply')}
</SubmitButton>
)}
<ResetButtonLink onClick={this.props.onClose}>
{done ? translate('close') : translate('cancel')}
</ResetButtonLink>
</footer>
</Modal>
</ButtonPrimary>
)
}
secondaryButtonLabel={done ? translate('close') : translate('cancel')}
/>
);
}
}

+ 45
- 45
server/sonar-web/src/main/js/apps/projectsManagement/ChangeDefaultVisibilityForm.tsx View File

@@ -17,11 +17,8 @@
* 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, FlagMessage, Modal, RadioButton, TextSubdued } from 'design-system';
import React, { useState } from 'react';
import Modal from '../../components/controls/Modal';
import Radio from '../../components/controls/Radio';
import { Button, ResetButtonLink } from '../../components/controls/buttons';
import { Alert } from '../../components/ui/Alert';
import { translate } from '../../helpers/l10n';
import { useGithubProvisioningEnabledQuery } from '../../queries/identity-provider/github';
import { Visibility } from '../../types/component';
@@ -32,11 +29,14 @@ export interface Props {
onConfirm: (visiblity: Visibility) => void;
}

const FORM_ID = 'change-default-visibility-form';

export default function ChangeDefaultVisibilityForm(props: Props) {
const [visibility, setVisibility] = useState(props.defaultVisibility);
const { data: githubProbivisioningEnabled } = useGithubProvisioningEnabledQuery();

const handleConfirmClick = () => {
const handleConfirmClick = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
props.onConfirm(visibility);
props.onClose();
};
@@ -47,47 +47,47 @@ export default function ChangeDefaultVisibilityForm(props: Props) {

const header = translate('settings.projects.change_visibility_form.header');

return (
<Modal contentLabel={header} onRequestClose={props.onClose}>
<header className="modal-head">
<h2>{header}</h2>
</header>

<div className="modal-body">
{Object.values(Visibility).map((visibilityValue) => (
<div className="big-spacer-bottom" key={visibilityValue}>
<Radio
value={visibilityValue}
checked={visibility === visibilityValue}
onCheck={handleVisibilityChange}
>
<div>
{translate('visibility', visibilityValue)}
<p className="text-muted spacer-top">
{translate('visibility', visibilityValue, 'description.short')}
</p>
</div>
</Radio>
</div>
))}

<Alert variant="warning">
{translate(
`settings.projects.change_visibility_form.warning${
githubProbivisioningEnabled ? '.github' : ''
}`,
)}
</Alert>
</div>
const body = (
<form id={FORM_ID} onSubmit={handleConfirmClick}>
{Object.values(Visibility).map((visibilityValue) => (
<div className="sw-mb-4" key={visibilityValue}>
<RadioButton
value={visibilityValue}
checked={visibility === visibilityValue}
onCheck={handleVisibilityChange}
>
<div>
{translate('visibility', visibilityValue)}
<TextSubdued as="p" className="sw-mt-2">
{translate('visibility', visibilityValue, 'description.short')}
</TextSubdued>
</div>
</RadioButton>
</div>
))}
<FlagMessage variant="warning">
{translate(
`settings.projects.change_visibility_form.warning${
githubProbivisioningEnabled ? '.github' : ''
}`,
)}
</FlagMessage>
</form>
);

<footer className="modal-foot">
<Button className="js-confirm" type="submit" onClick={handleConfirmClick}>
return (
<Modal
isScrollable={false}
isOverflowVisible
headerTitle={header}
onClose={props.onClose}
body={body}
primaryButton={
<ButtonPrimary form={FORM_ID} autoFocus type="submit">
{translate('settings.projects.change_visibility_form.submit')}
</Button>
<ResetButtonLink className="js-modal-close" onClick={props.onClose}>
{translate('cancel')}
</ResetButtonLink>
</footer>
</Modal>
</ButtonPrimary>
}
secondaryButtonLabel={translate('cancel')}
/>
);
}

+ 22
- 23
server/sonar-web/src/main/js/apps/projectsManagement/DeleteModal.tsx View File

@@ -17,11 +17,9 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import { DangerButtonPrimary, FlagMessage, Modal } from 'design-system';
import * as React from 'react';
import { Project, bulkDeleteProjects } from '../../api/project-management';
import Modal from '../../components/controls/Modal';
import { ResetButtonLink, SubmitButton } from '../../components/controls/buttons';
import { Alert } from '../../components/ui/Alert';
import { toISO8601WithOffsetString } from '../../helpers/dates';
import { translate, translateWithParameters } from '../../helpers/l10n';

@@ -80,42 +78,43 @@ export default class DeleteModal extends React.PureComponent<Props, State> {
};

renderWarning = () => (
<Alert variant="warning">
<FlagMessage variant="warning">
{this.props.selection.length
? translateWithParameters(
'projects_management.delete_selected_warning',
this.props.selection.length,
)
: translateWithParameters('projects_management.delete_all_warning', this.props.total)}
</Alert>
</FlagMessage>
);

render() {
const header = translate('qualifiers.delete', this.props.qualifier);

return (
<Modal contentLabel={header} onRequestClose={this.props.onClose}>
<header className="modal-head">
<h2>{header}</h2>
</header>
<div className="modal-body">
{this.renderWarning()}
{translate('qualifiers.delete_confirm', this.props.qualifier)}
</div>
<footer className="modal-foot">
{this.state.loading && <i className="spinner spacer-right" />}
<SubmitButton
className="button-red"
<Modal
headerTitle={header}
onClose={this.props.onClose}
body={
<>
{this.renderWarning()}
<p className="sw-mt-2">
{translate('qualifiers.delete_confirm', this.props.qualifier)}
</p>
</>
}
primaryButton={
<DangerButtonPrimary
autoFocus
disabled={this.state.loading}
onClick={this.handleConfirmClick}
type="submit"
>
{translate('delete')}
</SubmitButton>
<ResetButtonLink onClick={this.props.onClose}>{translate('cancel')}</ResetButtonLink>
</footer>
</Modal>
</DangerButtonPrimary>
}
secondaryButtonLabel={translate('cancel')}
/>
);
}
}

+ 33
- 31
server/sonar-web/src/main/js/apps/projectsManagement/Header.tsx View File

@@ -17,10 +17,10 @@
* 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, InteractiveIcon, PencilIcon, Title } from 'design-system';
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';
import ChangeDefaultVisibilityForm from './ChangeDefaultVisibilityForm';
@@ -39,39 +39,41 @@ export default function Header(props: Readonly<Props>) {
const { defaultProjectVisibility, hasProvisionPermission } = props;

return (
<header className="page-header">
<h1 className="page-title">{translate('projects_management')}</h1>
<header className="sw-mb-5">
<div className="sw-flex sw-items-center sw-justify-between">
<Title className="sw-m-0">{translate('projects_management')}</Title>
<div className="sw-flex sw-items-center it__page-actions">
<div className="sw-mr-2">
<span className="sw-mr-1">
{translate('settings.projects.default_visibility_of_new_projects')}{' '}
<strong className="sw-body-sm-highlight">
{defaultProjectVisibility ? translate('visibility', defaultProjectVisibility) : '—'}
</strong>
</span>
<InteractiveIcon
className="it__change-visibility"
Icon={PencilIcon}
onClick={() => setVisibilityForm(true)}
aria-label={translate('settings.projects.change_visibility_form.label')}
/>
</div>

<div className="page-actions">
<span className="big-spacer-right">
<span className="text-middle">
{translate('settings.projects.default_visibility_of_new_projects')}{' '}
<strong>
{defaultProjectVisibility ? translate('visibility', defaultProjectVisibility) : '—'}
</strong>
</span>
<EditButton
className="js-change-visibility spacer-left button-small"
onClick={() => setVisibilityForm(true)}
aria-label={translate('settings.projects.change_visibility_form.label')}
/>
</span>

{hasProvisionPermission && (
<Button
id="create-project"
onClick={() =>
navigate('/projects/create?mode=manual', {
state: { from: location.pathname },
})
}
>
{translate('qualifiers.create.TRK')}
</Button>
)}
{hasProvisionPermission && (
<ButtonPrimary
id="create-project"
onClick={() =>
navigate('/projects/create?mode=manual', {
state: { from: location.pathname },
})
}
>
{translate('qualifiers.create.TRK')}
</ButtonPrimary>
)}
</div>
</div>

<p className="page-description">{translate('projects_management.page.description')}</p>
<p className="sw-mt-4">{translate('projects_management.page.description')}</p>

{visibilityForm && (
<ChangeDefaultVisibilityForm

+ 50
- 46
server/sonar-web/src/main/js/apps/projectsManagement/ProjectManagementApp.tsx View File

@@ -17,6 +17,7 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import { LargeCenteredLayout, PageContentFontWrapper } from 'design-system';
import { debounce, uniq } from 'lodash';
import * as React from 'react';
import { Helmet } from 'react-helmet-async';
@@ -202,52 +203,55 @@ class ProjectManagementApp extends React.PureComponent<Props, State> {
const { currentUser } = this.props;
const { defaultProjectVisibility } = this.state;
return (
<main className="page page-limited" id="projects-management-page">
<Suggestions suggestions="projects_management" />
<Helmet defer={false} title={translate('projects_management')} />

<Header
defaultProjectVisibility={defaultProjectVisibility}
hasProvisionPermission={hasGlobalPermission(currentUser, Permissions.ProjectCreation)}
onChangeDefaultProjectVisibility={this.handleDefaultProjectVisibilityChange}
/>

<Search
analyzedBefore={this.state.analyzedBefore}
onAllDeselected={this.onAllDeselected}
onAllSelected={this.onAllSelected}
onDateChanged={this.handleDateChanged}
onDeleteProjects={this.requestProjects}
onProvisionedChanged={this.onProvisionedChanged}
onQualifierChanged={this.onQualifierChanged}
onSearch={this.onSearch}
onVisibilityChanged={this.onVisibilityChanged}
projects={this.state.projects}
provisioned={this.state.provisioned}
qualifiers={this.state.qualifiers}
query={this.state.query}
ready={this.state.ready}
selection={this.state.selection}
total={this.state.total}
visibility={this.state.visibility}
/>

<Projects
currentUser={this.props.currentUser}
onProjectDeselected={this.onProjectDeselected}
onProjectSelected={this.onProjectSelected}
projects={this.state.projects}
ready={this.state.ready}
selection={this.state.selection}
/>

<ListFooter
count={this.state.projects.length}
loadMore={this.loadMore}
ready={this.state.ready}
total={this.state.total}
/>
</main>
<LargeCenteredLayout as="main" id="projects-management-page">
<PageContentFontWrapper className="sw-body-sm sw-my-8">
<Suggestions suggestions="projects_management" />
<Helmet defer={false} title={translate('projects_management')} />

<Header
defaultProjectVisibility={defaultProjectVisibility}
hasProvisionPermission={hasGlobalPermission(currentUser, Permissions.ProjectCreation)}
onChangeDefaultProjectVisibility={this.handleDefaultProjectVisibilityChange}
/>

<Search
analyzedBefore={this.state.analyzedBefore}
onAllDeselected={this.onAllDeselected}
onAllSelected={this.onAllSelected}
onDateChanged={this.handleDateChanged}
onDeleteProjects={this.requestProjects}
onProvisionedChanged={this.onProvisionedChanged}
onQualifierChanged={this.onQualifierChanged}
onSearch={this.onSearch}
onVisibilityChanged={this.onVisibilityChanged}
projects={this.state.projects}
provisioned={this.state.provisioned}
qualifiers={this.state.qualifiers}
query={this.state.query}
ready={this.state.ready}
selection={this.state.selection}
total={this.state.total}
visibility={this.state.visibility}
/>

<Projects
currentUser={this.props.currentUser}
onProjectDeselected={this.onProjectDeselected}
onProjectSelected={this.onProjectSelected}
projects={this.state.projects}
ready={this.state.ready}
selection={this.state.selection}
/>

<ListFooter
count={this.state.projects.length}
loadMore={this.loadMore}
ready={this.state.ready}
total={this.state.total}
useMIUIButtons
/>
</PageContentFontWrapper>
</LargeCenteredLayout>
);
}
}

+ 0
- 43
server/sonar-web/src/main/js/apps/projectsManagement/ProjectRow.css View File

@@ -1,43 +0,0 @@
/*
* 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.
*/
.project-row-text-cell {
max-width: 20em;
}

.projects-management-search {
display: flex;
gap: 20px;
padding: 8px 10px;
flex-wrap: wrap;
}

.projects-management-search > * {
display: flex;
white-space: nowrap;
}

.projects-management-search > *:not(.bulk-actions) {
flex-direction: column;
justify-content: center;
}

.projects-management-search > .bulk-actions {
justify-content: end;
}

+ 20
- 33
server/sonar-web/src/main/js/apps/projectsManagement/ProjectRow.tsx View File

@@ -17,20 +17,17 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import { ActionCell, Badge, Checkbox, ContentCell, HoverLink, Note, TableRow } from 'design-system';
import * as React from 'react';
import { Project } from '../../api/project-management';
import Link from '../../components/common/Link';
import PrivacyBadgeContainer from '../../components/common/PrivacyBadgeContainer';
import Checkbox from '../../components/controls/Checkbox';
import Tooltip from '../../components/controls/Tooltip';
import QualifierIcon from '../../components/icons/QualifierIcon';
import DateFormatter from '../../components/intl/DateFormatter';
import { translate, translateWithParameters } from '../../helpers/l10n';
import { getComponentOverviewUrl } from '../../helpers/urls';
import { useGithubProvisioningEnabledQuery } from '../../queries/identity-provider/github';
import { ComponentQualifier } from '../../types/component';
import { LoggedInUser } from '../../types/users';
import './ProjectRow.css';
import ProjectRowActions from './ProjectRowActions';

interface Props {
@@ -49,52 +46,42 @@ export default function ProjectRow(props: Props) {
};

return (
<tr data-project-key={project.key}>
<td className="thin">
<TableRow data-project-key={project.key}>
<ContentCell>
<Checkbox
label={translateWithParameters('projects_management.select_project', project.name)}
checked={selected}
onCheck={handleProjectCheck}
/>
</td>

<td className="nowrap hide-overflow project-row-text-cell">
<Link
className="link-no-underline"
to={getComponentOverviewUrl(project.key, project.qualifier)}
>
<QualifierIcon className="little-spacer-right" qualifier={project.qualifier} />

</ContentCell>
<ContentCell className="it__project-row-text-cell">
<HoverLink to={getComponentOverviewUrl(project.key, project.qualifier)}>
<Tooltip overlay={project.name} placement="left">
<span>{project.name}</span>
</Tooltip>
</Link>
</HoverLink>
{project.qualifier === ComponentQualifier.Project &&
githubProvisioningEnabled &&
!project.managed && <span className="badge sw-ml-1">{translate('local')}</span>}
</td>

<td className="thin nowrap">
!project.managed && <Badge className="sw-ml-1">{translate('local')}</Badge>}
</ContentCell>
<ContentCell>
<PrivacyBadgeContainer qualifier={project.qualifier} visibility={project.visibility} />
</td>

<td className="nowrap hide-overflow project-row-text-cell">
</ContentCell>
<ContentCell className="it__project-row-text-cell">
<Tooltip overlay={project.key} placement="left">
<span className="note">{project.key}</span>
<Note>{project.key}</Note>
</Tooltip>
</td>

<td className="thin nowrap text-right">
</ContentCell>
<ContentCell>
{project.lastAnalysisDate ? (
<DateFormatter date={project.lastAnalysisDate} />
) : (
<span className="note">—</span>
<Note>—</Note>
)}
</td>

<td className="thin nowrap">
</ContentCell>
<ActionCell>
<ProjectRowActions currentUser={currentUser} project={project} />
</td>
</tr>
</ActionCell>
</TableRow>
);
}

+ 15
- 22
server/sonar-web/src/main/js/apps/projectsManagement/ProjectRowActions.tsx View File

@@ -17,11 +17,10 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import { ActionsDropdown, ItemButton, ItemLink, PopupZLevel, Spinner } from 'design-system';
import React, { useState } from 'react';
import { getComponentNavigation } from '../../api/navigation';
import { Project } from '../../api/project-management';
import ActionsDropdown, { ActionsDropdownItem } from '../../components/controls/ActionsDropdown';
import Spinner from '../../components/ui/Spinner';
import { throwGlobalError } from '../../helpers/error';
import { translate, translateWithParameters } from '../../helpers/l10n';
import { getComponentPermissionsUrl } from '../../helpers/urls';
@@ -71,43 +70,37 @@ export default function ProjectRowActions({ currentUser, project }: Props) {
return (
<>
<ActionsDropdown
label={translateWithParameters('projects_management.show_actions_for_x', project.name)}
id="project-management-action-dropdown"
toggleClassName="it__user-actions-toggle"
onOpen={handleDropdownOpen}
allowResizing
ariaLabel={translateWithParameters('projects_management.show_actions_for_x', project.name)}
zLevel={PopupZLevel.Global}
>
{loading ? (
<ActionsDropdownItem>
<Spinner />
</ActionsDropdownItem>
) : (
<Spinner loading={loading} className="sw-flex sw-ml-3">
<>
{hasAccess === true && (
<ActionsDropdownItem
className="js-edit-permissions"
to={getComponentPermissionsUrl(project.key)}
>
<ItemLink to={getComponentPermissionsUrl(project.key)}>
{translate(project.managed ? 'show_permissions' : 'edit_permissions')}
</ActionsDropdownItem>
</ItemLink>
)}

{hasAccess === false &&
(!project.managed || currentUser.local || !githubProvisioningEnabled) && (
<ActionsDropdownItem
className="js-restore-access"
<ItemButton
className="it__restore-access"
onClick={() => setRestoreAccessModal(true)}
>
{translate('global_permissions.restore_access')}
</ActionsDropdownItem>
</ItemButton>
)}
</>
)}
</Spinner>

{!project.managed && (
<ActionsDropdownItem
className="js-apply-template"
onClick={() => setApplyTemplateModal(true)}
>
<ItemButton className="it__apply-template" onClick={() => setApplyTemplateModal(true)}>
{translate('projects_role.apply_template')}
</ActionsDropdownItem>
</ItemButton>
)}
</ActionsDropdown>


+ 29
- 28
server/sonar-web/src/main/js/apps/projectsManagement/Projects.tsx View File

@@ -17,7 +17,9 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/

import classNames from 'classnames';
import { ActionCell, ContentCell, Table, TableRow } from 'design-system';
import * as React from 'react';
import { Project } from '../../api/project-management';
import { translate } from '../../helpers/l10n';
@@ -44,34 +46,33 @@ export default function Projects(props: Readonly<Props>) {
}
};

const header = (
<TableRow>
<ContentCell>&nbsp;</ContentCell>
<ContentCell>{translate('name')}</ContentCell>
<ContentCell>{translate('visibility')}</ContentCell>
<ContentCell>{translate('key')}</ContentCell>
<ContentCell>{translate('last_analysis')}</ContentCell>
<ActionCell>{translate('actions')}</ActionCell>
</TableRow>
);

return (
<div className="boxed-group boxed-group-inner">
<table
className={classNames('data', 'zebra', { 'new-loading': !ready })}
id="projects-management-page-projects"
>
<thead>
<tr>
<th />
<th>{translate('name')}</th>
<th />
<th>{translate('key')}</th>
<th className="thin nowrap text-right">{translate('last_analysis')}</th>
<th />
</tr>
</thead>
<tbody>
{projects.map((project) => (
<ProjectRow
currentUser={currentUser}
key={project.key}
onProjectCheck={onProjectCheck}
project={project}
selected={selection.some((s) => s.key === project.key)}
/>
))}
</tbody>
</table>
</div>
<Table
columnCount={6}
header={header}
id="projects-management-page-projects"
className={classNames({ 'sw-opacity-50 sw-transition sw-duration-75 sw-ease-in': !ready })}
>
{projects.map((project) => (
<ProjectRow
currentUser={currentUser}
key={project.key}
onProjectCheck={onProjectCheck}
project={project}
selected={selection.some((s) => s.key === project.key)}
/>
))}
</Table>
);
}

+ 19
- 18
server/sonar-web/src/main/js/apps/projectsManagement/RestoreAccessModal.tsx View File

@@ -17,12 +17,11 @@
* 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, Modal } from 'design-system';
import * as React from 'react';
import { FormattedMessage } from 'react-intl';
import { grantPermissionToUser } from '../../api/permissions';
import { Project } from '../../api/project-management';
import Modal from '../../components/controls/Modal';
import { ResetButtonLink, SubmitButton } from '../../components/controls/buttons';
import { translate } from '../../helpers/l10n';
import { LoggedInUser } from '../../types/users';

@@ -37,6 +36,8 @@ interface State {
loading: boolean;
}

const FORM_ID = 'restore-access-form';

export default class RestoreAccessModal extends React.PureComponent<Props, State> {
mounted = false;
state: State = { loading: false };
@@ -70,16 +71,16 @@ export default class RestoreAccessModal extends React.PureComponent<Props, State
});

render() {
const { loading } = this.state;
const header = translate('global_permissions.restore_access');

return (
<Modal contentLabel={header} onRequestClose={this.props.onClose}>
<form onSubmit={this.handleFormSubmit}>
<header className="modal-head">
<h2>{header}</h2>
</header>

<div className="modal-body">
<Modal
headerTitle={header}
onClose={this.props.onClose}
loading={loading}
body={
<form id={FORM_ID} onSubmit={this.handleFormSubmit}>
<FormattedMessage
defaultMessage={translate('global_permissions.restore_access.message')}
id="global_permissions.restore_access.message"
@@ -88,15 +89,15 @@ export default class RestoreAccessModal extends React.PureComponent<Props, State
administer: <strong>{translate('projects_role.admin')}</strong>,
}}
/>
</div>
<footer className="modal-foot">
{this.state.loading && <i className="spinner spacer-right" />}
<SubmitButton disabled={this.state.loading}>{translate('restore')}</SubmitButton>
<ResetButtonLink onClick={this.props.onClose}>{translate('cancel')}</ResetButtonLink>
</footer>
</form>
</Modal>
</form>
}
primaryButton={
<ButtonPrimary autoFocus disabled={loading} form={FORM_ID} type="submit">
{translate('restore')}
</ButtonPrimary>
}
secondaryButtonLabel={translate('cancel')}
/>
);
}
}

+ 44
- 38
server/sonar-web/src/main/js/apps/projectsManagement/Search.tsx View File

@@ -17,18 +17,24 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/

import {
ButtonSecondary,
Checkbox,
DangerButtonPrimary,
DatePicker,
HelperHintIcon,
InputSearch,
InputSelect,
Spinner,
} from 'design-system';
import { sortBy } from 'lodash';
import * as React from 'react';
import { components, OptionProps, SingleValueProps } from 'react-select';
import { OptionProps, SingleValueProps, components } from 'react-select';
import { Project } from '../../api/project-management';
import withAppStateContext from '../../app/components/app-state/withAppStateContext';
import { Button } from '../../components/controls/buttons';
import Checkbox from '../../components/controls/Checkbox';
import DateInput from '../../components/controls/DateInput';
import HelpTooltip from '../../components/controls/HelpTooltip';
import SearchBox from '../../components/controls/SearchBox';
import Select, { LabelValueSelectOption } from '../../components/controls/Select';
import QualifierIcon from '../../components/icons/QualifierIcon';
import { LabelValueSelectOption } from '../../components/controls/Select';
import { translate } from '../../helpers/l10n';
import { AppState } from '../../types/appstate';
import { Visibility } from '../../types/component';
@@ -129,6 +135,7 @@ class Search extends React.PureComponent<Props, State> {
const checked = isAllChecked || thirdState;
return (
<Checkbox
className="it__projects-selection"
checked={checked}
id="projects-selection"
onCheck={this.onCheck}
@@ -138,12 +145,7 @@ class Search extends React.PureComponent<Props, State> {
);
};

renderQualifierOption = (option: LabelValueSelectOption) => (
<div className="display-flex-center">
<QualifierIcon className="little-spacer-right" qualifier={option.value} />
{option.label}
</div>
);
renderQualifierOption = (option: LabelValueSelectOption) => <div>{option.label}</div>;

renderQualifierFilter = () => {
const options = this.getQualifierOptions();
@@ -151,8 +153,8 @@ class Search extends React.PureComponent<Props, State> {
return null;
}
return (
<Select
className="input-medium it__project-qualifier-select"
<InputSelect
className="it__project-qualifier-select"
isDisabled={!this.props.ready}
name="projects-qualifier"
onChange={this.handleQualifierChange}
@@ -175,8 +177,7 @@ class Search extends React.PureComponent<Props, State> {
{ value: Visibility.Private, label: translate('visibility.private') },
];
return (
<Select
className="input-small"
<InputSelect
isDisabled={!this.props.ready}
name="projects-visibility"
onChange={this.handleVisibilityChange}
@@ -190,64 +191,69 @@ class Search extends React.PureComponent<Props, State> {

renderTypeFilter = () =>
this.props.qualifiers === 'TRK' ? (
<div>
<div className="sw-flex sw-items-center">
<Checkbox
checked={this.props.provisioned}
className="link-checkbox-control"
id="projects-provisioned"
onCheck={this.props.onProvisionedChanged}
>
<span className="text-middle little-spacer-left">
{translate('provisioning.only_provisioned')}
</span>
<span className="sw-ml-1">{translate('provisioning.only_provisioned')}</span>
<HelpTooltip
className="spacer-left"
className="sw-ml-2"
overlay={translate('provisioning.only_provisioned.tooltip')}
/>
>
<HelperHintIcon />
</HelpTooltip>
</Checkbox>
</div>
) : null;

renderDateFilter = () => {
return (
<DateInput
inputClassName="input-medium"
<DatePicker
clearButtonLabel={translate('clear')}
name="analyzed-before"
onChange={this.props.onDateChanged}
placeholder={translate('last_analysis_before')}
value={this.props.analyzedBefore}
showClearButton
alignRight
size="auto"
/>
);
};

render() {
return (
<div className="big-spacer-bottom">
<div className="projects-management-search">
<div>{this.props.ready ? this.renderCheckbox() : <i className="spinner" />}</div>
<div className="sw-mb-4">
<div className="sw-flex sw-justify-start sw-items-center sw-flex-wrap sw-gap-2 sw-p-2">
<Spinner loading={!this.props.ready} className="sw-ml-2">
{this.renderCheckbox()}
</Spinner>
{this.renderQualifierFilter()}
{this.renderDateFilter()}
{this.renderVisibilityFilter()}
{this.renderTypeFilter()}
<div className="flex-grow">
<SearchBox
<div className="sw-flex-grow">
<InputSearch
minLength={3}
onChange={this.props.onSearch}
placeholder={translate('search.search_by_name_or_key')}
value={this.props.query}
size="auto"
/>
</div>
<div className="bulk-actions">
<Button
className="js-bulk-apply-permission-template"
<div>
<ButtonSecondary
className="it__bulk-apply-permission-template"
disabled={this.props.selection.length === 0}
onClick={this.handleBulkApplyTemplateClick}
>
{translate('permission_templates.bulk_apply_permission_template')}
</Button>
</ButtonSecondary>
{this.props.qualifiers === 'TRK' && (
<Button
className="js-delete spacer-left button-red"
<DangerButtonPrimary
className="sw-ml-2"
disabled={this.props.selection.length === 0}
onClick={this.handleDeleteClick}
title={
@@ -257,7 +263,7 @@ class Search extends React.PureComponent<Props, State> {
}
>
{translate('delete')}
</Button>
</DangerButtonPrimary>
)}
</div>
</div>

+ 6
- 6
server/sonar-web/src/main/js/apps/projectsManagement/__tests__/ProjectManagementApp-it.tsx View File

@@ -75,10 +75,10 @@ const ui = {
}),
projectActions: (projectName: string) =>
byRole('button', { name: `projects_management.show_actions_for_x.${projectName}` }),
editPermissions: byRole('link', { name: 'edit_permissions' }),
showPermissions: byRole('link', { name: 'show_permissions' }),
applyPermissionTemplate: byRole('button', { name: 'projects_role.apply_template' }),
restoreAccess: byRole('button', { name: 'global_permissions.restore_access' }),
editPermissions: byRole('menuitem', { name: 'edit_permissions' }),
showPermissions: byRole('menuitem', { name: 'show_permissions' }),
applyPermissionTemplate: byRole('menuitem', { name: 'projects_role.apply_template' }),
restoreAccess: byRole('menuitem', { name: 'global_permissions.restore_access' }),
editPermissionsPage: byText('/project_roles?id=project1'),

apply: byRole('button', { name: 'apply' }),
@@ -114,7 +114,7 @@ const ui = {
qualifierFilter: byRole('combobox', { name: 'projects_management.filter_by_component' }),
analysisDateFilter: byPlaceholderText('last_analysis_before'),
provisionedFilter: byRole('checkbox', {
name: 'provisioning.only_provisioned help',
name: 'provisioning.only_provisioned',
}),
searchFilter: byRole('searchbox', { name: 'search.search_by_name_or_key' }),

@@ -291,7 +291,7 @@ describe('Bulk permission templates', () => {
.get(),
).toBeInTheDocument();
await selectEvent.select(
ui.bulkApplyDialog.by(ui.selectTemplate('field_required')).get(),
ui.bulkApplyDialog.by(ui.selectTemplate('required')).get(),
'Permission Template 2',
);
await user.click(ui.bulkApplyDialog.by(ui.apply).get());

+ 0
- 87
server/sonar-web/src/main/js/components/controls/DateInput.css View File

@@ -1,87 +0,0 @@
/*
* 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.
*/
.rdp {
--rdp-cell-size: 30px;
--rdp-caption-font-size: 13px;
/* Ensures the month/year dropdowns do not move on click, but rdp outline is not shown */
--rdp-outline: 2px solid transparent;
--rdp-outline-selected: 2px solid transparent;
}

.rdp-day_selected {
background-color: var(--blue);
}

.rdp-day_selected:hover {
background-color: var(--blue);
}

.date-input-control {
position: relative;
display: inline-block;
cursor: pointer;
}

.date-input-control-input {
width: 130px;
padding-left: var(--controlHeight) !important;
cursor: pointer;
}

.date-input-control-input.is-filled {
padding-right: 16px !important;
}

.date-input-control-icon {
position: absolute;
top: 4px;
left: 4px;
}

.date-input-control-icon path {
fill: var(--neutral600);
opacity: 0.9;
}

.date-input-control-input:focus + .date-input-control-icon path {
fill: var(--info500);
}

.date-input-control-reset {
position: absolute;
top: 4px;
right: 4px;
border: none;
}

.date-input-calendar {
position: absolute;
z-index: var(--dropdownMenuZIndex);
top: 100%;
left: 0;
border: 1px solid var(--barBorderColor);
background-color: #fff;
box-shadow: var(--defaultShadow);
}

.date-input-calendar.align-right {
left: initial;
right: 0;
}

+ 0
- 195
server/sonar-web/src/main/js/components/controls/DateInput.tsx View File

@@ -1,195 +0,0 @@
/*
* 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 classNames from 'classnames';
import * as React from 'react';
import { ActiveModifiers, DayPicker, Matcher } from 'react-day-picker';
import 'react-day-picker/dist/style.css';
import { injectIntl, WrappedComponentProps } from 'react-intl';
import { ClearButton } from '../../components/controls/buttons';
import OutsideClickHandler from '../../components/controls/OutsideClickHandler';
import CalendarIcon from '../../components/icons/CalendarIcon';
import { getShortWeekDayName, translate } from '../../helpers/l10n';
import './DateInput.css';
import EscKeydownHandler from './EscKeydownHandler';
import FocusOutHandler from './FocusOutHandler';

// When no minDate is given, year dropdown will show year options up to PAST_MAX_YEARS in the past
const YEARS_TO_DISPLAY = 10;

interface Props {
alignRight?: boolean;
className?: string;
currentMonth?: Date;
highlightFrom?: Date;
highlightTo?: Date;
inputClassName?: string;
maxDate?: Date;
minDate?: Date;
name?: string;
id?: string;
onChange: (date: Date | undefined) => void;
placeholder: string;
value?: Date;
}

interface State {
currentMonth: Date;
open: boolean;
lastHovered?: Date;
}

export default class DateInput extends React.PureComponent<Props, State> {
input?: HTMLInputElement | null;

constructor(props: Props) {
super(props);

this.state = { currentMonth: props.value || props.currentMonth || new Date(), open: false };
}

focus = () => {
if (this.input) {
this.input.focus();
}
this.openCalendar();
};

handleResetClick = () => {
this.closeCalendar();
this.props.onChange(undefined);
};

openCalendar = () => {
this.setState({
currentMonth: this.props.value || this.props.currentMonth || new Date(),
lastHovered: undefined,
open: true,
});
};

closeCalendar = () => {
this.setState({ open: false });
};

handleDayClick = (day: Date, modifiers: ActiveModifiers) => {
if (!modifiers.disabled) {
this.closeCalendar();
this.props.onChange(day);
}
};

handleDayMouseEnter = (day: Date, modifiers: ActiveModifiers) => {
this.setState({ lastHovered: modifiers.disabled ? undefined : day });
};

render() {
const {
alignRight,
highlightFrom,
highlightTo,
minDate,
maxDate = new Date(),
value: selectedDay,
name,
className,
inputClassName,
id,
placeholder,
} = this.props;
const { lastHovered, currentMonth, open } = this.state;

// Infer start and end dropdown year from min/max dates, if set
const fromYear = minDate ? minDate.getFullYear() : new Date().getFullYear() - YEARS_TO_DISPLAY;
const toYear = maxDate ? maxDate.getFullYear() : new Date().getFullYear() + 1;

let highlighted: Matcher = false;
const lastHoveredOrValue = lastHovered || selectedDay;
if (highlightFrom && lastHoveredOrValue) {
highlighted = { from: highlightFrom, to: lastHoveredOrValue };
}
if (highlightTo && lastHoveredOrValue) {
highlighted = { from: lastHoveredOrValue, to: highlightTo };
}

return (
<FocusOutHandler onFocusOut={this.closeCalendar}>
<OutsideClickHandler onClickOutside={this.closeCalendar}>
<EscKeydownHandler onKeydown={this.closeCalendar}>
<span className={classNames('date-input-control', className)}>
<InputWrapper
className={classNames('date-input-control-input', inputClassName, {
'is-filled': selectedDay !== undefined,
})}
id={id}
innerRef={(node: HTMLInputElement | null) => (this.input = node)}
name={name}
onFocus={this.openCalendar}
placeholder={placeholder}
readOnly
type="text"
value={selectedDay}
/>
<CalendarIcon className="date-input-control-icon" fill="" />
{selectedDay !== undefined && (
<ClearButton
aria-label={translate('reset_date')}
className="button-tiny date-input-control-reset"
iconProps={{ size: 12 }}
onClick={this.handleResetClick}
/>
)}
{open && (
<div className={classNames('date-input-calendar', { 'align-right': alignRight })}>
<DayPicker
mode="default"
captionLayout="dropdown-buttons"
fromYear={fromYear}
toYear={toYear}
disabled={{ after: maxDate, before: minDate }}
weekStartsOn={1}
formatters={{
formatWeekdayName: (date) => getShortWeekDayName(date.getDay()),
}}
modifiers={{ highlighted }}
modifiersClassNames={{ highlighted: 'highlighted' }}
month={currentMonth}
onMonthChange={(currentMonth) => this.setState({ currentMonth })}
selected={selectedDay}
onDayClick={this.handleDayClick}
onDayMouseEnter={this.handleDayMouseEnter}
/>
</div>
)}
</span>
</EscKeydownHandler>
</OutsideClickHandler>
</FocusOutHandler>
);
}
}

type InputWrapperProps = Omit<React.InputHTMLAttributes<HTMLInputElement>, 'value'> &
WrappedComponentProps & { innerRef: React.Ref<HTMLInputElement>; value: Date | undefined };

const InputWrapper = injectIntl(({ innerRef, intl, value, ...other }: InputWrapperProps) => {
const formattedValue =
value && intl.formatDate(value, { year: 'numeric', month: 'short', day: 'numeric' });
return <input {...other} ref={innerRef} value={formattedValue || ''} />;
});

+ 0
- 108
server/sonar-web/src/main/js/components/controls/DateRangeInput.tsx View File

@@ -1,108 +0,0 @@
/*
* 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 classNames from 'classnames';
import { max, min } from 'date-fns';
import * as React from 'react';
import { translate } from '../../helpers/l10n';
import DateInput from './DateInput';

type DateRange = { from?: Date; to?: Date };

interface Props {
className?: string;
maxDate?: Date;
minDate?: Date;
onChange: (date: DateRange) => void;
value?: DateRange;
alignEndDateCalandarRight?: boolean;
}

export default class DateRangeInput extends React.PureComponent<Props> {
toDateInput?: DateInput | null;

get from() {
return this.props.value && this.props.value.from;
}

get to() {
return this.props.value && this.props.value.to;
}

handleFromChange = (from: Date | undefined) => {
this.props.onChange({ from, to: this.to });

// use `setTimeout` to work around the immediate closing of the `toDateInput`
setTimeout(() => {
if (from && !this.to && this.toDateInput) {
this.toDateInput.focus();
}
});
};

handleToChange = (to: Date | undefined) => {
this.props.onChange({ from: this.from, to });
};

render() {
const { alignEndDateCalandarRight, minDate, maxDate } = this.props;

return (
<div className={classNames('display-flex-end', this.props.className)}>
<div className="display-flex-column">
<label className="text-bold little-spacer-bottom" htmlFor="date-from">
{translate('start_date')}
</label>
<DateInput
currentMonth={this.to}
data-test="from"
id="date-from"
highlightTo={this.to}
minDate={minDate}
maxDate={maxDate && this.to ? min([maxDate, this.to]) : maxDate || this.to}
onChange={this.handleFromChange}
placeholder={translate('start_date')}
value={this.from}
/>
</div>
<span className="note little-spacer-left little-spacer-right little-spacer-bottom">
{translate('to_')}
</span>
<div className="display-flex-column">
<label className="text-bold little-spacer-bottom" htmlFor="date-to">
{translate('end_date')}
</label>
<DateInput
alignRight={alignEndDateCalandarRight}
currentMonth={this.from}
data-test="to"
id="date-to"
highlightFrom={this.from}
minDate={minDate && this.from ? max([minDate, this.from]) : minDate || this.from}
maxDate={maxDate}
onChange={this.handleToChange}
placeholder={translate('end_date')}
ref={(element) => (this.toDateInput = element)}
value={this.to}
/>
</div>
</div>
);
}
}

Loading…
Cancel
Save