@@ -17,7 +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 { LargeCenteredLayout, PageContentFontWrapper, Spinner } from 'design-system'; | |||
import { debounce } from 'lodash'; | |||
import * as React from 'react'; | |||
import { Helmet } from 'react-helmet-async'; | |||
@@ -32,7 +32,6 @@ import withComponentContext from '../../../app/components/componentContext/withC | |||
import ListFooter from '../../../components/controls/ListFooter'; | |||
import Suggestions from '../../../components/embed-docs-modal/Suggestions'; | |||
import { Location, Router, withRouter } from '../../../components/hoc/withRouter'; | |||
import Spinner from '../../../components/ui/Spinner'; | |||
import { toShortISO8601String } from '../../../helpers/dates'; | |||
import { translate } from '../../../helpers/l10n'; | |||
import { parseAsDate } from '../../../helpers/query'; |
@@ -18,12 +18,11 @@ | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import classNames from 'classnames'; | |||
import { InputSearch, ToggleButton } from 'design-system'; | |||
import { InputSearch, Spinner, ToggleButton } from 'design-system'; | |||
import { isEmpty, omit } from 'lodash'; | |||
import * as React from 'react'; | |||
import { getTree } from '../../../api/components'; | |||
import { Location, Router, withRouter } from '../../../components/hoc/withRouter'; | |||
import Spinner from '../../../components/ui/Spinner'; | |||
import { getBranchLikeQuery } from '../../../helpers/branch-like'; | |||
import { KeyboardKeys } from '../../../helpers/keycodes'; | |||
import { translate } from '../../../helpers/l10n'; |
@@ -26,6 +26,7 @@ import { | |||
LAYOUT_FOOTER_HEIGHT, | |||
LargeCenteredLayout, | |||
PageContentFontWrapper, | |||
Spinner, | |||
ToggleButton, | |||
themeBorder, | |||
themeColor, | |||
@@ -51,7 +52,6 @@ import { Location, Router, withRouter } from '../../../components/hoc/withRouter | |||
import IssueTabViewer from '../../../components/rules/IssueTabViewer'; | |||
import '../../../components/search-navigator.css'; | |||
import { DEFAULT_ISSUES_QUERY } from '../../../components/shared/utils'; | |||
import Spinner from '../../../components/ui/Spinner'; | |||
import { | |||
fillBranchLike, | |||
getBranchLikeQuery, |
@@ -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 { Spinner } from 'design-system'; | |||
import { findLastIndex, keyBy } from 'lodash'; | |||
import * as React from 'react'; | |||
import { getComponentForSourceViewer, getDuplications, getSources } from '../../../api/components'; | |||
@@ -33,7 +34,6 @@ import { | |||
issuesByComponentAndLine, | |||
} from '../../../components/SourceViewer/helpers/indexing'; | |||
import { Alert } from '../../../components/ui/Alert'; | |||
import Spinner from '../../../components/ui/Spinner'; | |||
import { WorkspaceContext } from '../../../components/workspace/context'; | |||
import { getBranchLikeQuery } from '../../../helpers/branch-like'; | |||
import { throwGlobalError } from '../../../helpers/error'; |
@@ -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 { ButtonPrimary, FlagMessage, Title } from 'design-system'; | |||
import { ButtonPrimary, FlagMessage, Spinner, Title } from 'design-system'; | |||
import React, { useState } from 'react'; | |||
import { createPermissionTemplate } from '../../../api/permissions'; | |||
import { Router, withRouter } from '../../../components/hoc/withRouter'; | |||
import Spinner from '../../../components/ui/Spinner'; | |||
import { throwGlobalError } from '../../../helpers/error'; | |||
import { translate } from '../../../helpers/l10n'; | |||
import { useGithubProvisioningEnabledQuery } from '../../../queries/identity-provider/github'; | |||
@@ -60,8 +59,10 @@ function Header(props: Props) { | |||
<header> | |||
<div id="project-permissions-header"> | |||
<div className="sw-flex sw-justify-between"> | |||
<Title>{translate('permission_templates.page')}</Title> | |||
<Spinner loading={!ready} /> | |||
<div className="sw-flex sw-gap-3"> | |||
<Title>{translate('permission_templates.page')}</Title> | |||
<Spinner className="sw-mt-2" loading={!ready} /> | |||
</div> | |||
<ButtonPrimary onClick={() => setCreateModal(true)}>{translate('create')}</ButtonPrimary> | |||
</div> |
@@ -17,13 +17,12 @@ | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import { Checkbox, FlagMessage, SubTitle } from 'design-system'; | |||
import { Checkbox, FlagMessage, Spinner, SubTitle } from 'design-system'; | |||
import * as React from 'react'; | |||
import { | |||
WithNotificationsProps, | |||
withNotifications, | |||
} from '../../../components/hoc/withNotifications'; | |||
import Spinner from '../../../components/ui/Spinner'; | |||
import { hasMessage, translate, translateWithParameters } from '../../../helpers/l10n'; | |||
import { NotificationProjectType } from '../../../types/notifications'; | |||
import { Component } from '../../../types/types'; | |||
@@ -79,7 +78,7 @@ export function ProjectNotifications(props: WithNotificationsProps & Props) { | |||
{translate('notification.dispatcher.information')} | |||
</FlagMessage> | |||
<Spinner loading={loading}> | |||
<Spinner className="sw-mt-6" loading={loading}> | |||
<h3 id="notifications-update-title" className="sw-mt-6"> | |||
{translate('project_information.project_notifications.title')} | |||
</h3> |
@@ -21,6 +21,7 @@ import styled from '@emotion/styled'; | |||
import { | |||
LargeCenteredLayout, | |||
PageContentFontWrapper, | |||
Spinner, | |||
themeBorder, | |||
themeColor, | |||
} from 'design-system'; | |||
@@ -36,7 +37,6 @@ import ScreenPositionHelper from '../../../components/common/ScreenPositionHelpe | |||
import Suggestions from '../../../components/embed-docs-modal/Suggestions'; | |||
import { Location, Router, withRouter } from '../../../components/hoc/withRouter'; | |||
import '../../../components/search-navigator.css'; | |||
import Spinner from '../../../components/ui/Spinner'; | |||
import handleRequiredAuthentication from '../../../helpers/handleRequiredAuthentication'; | |||
import { translate } from '../../../helpers/l10n'; | |||
import { addSideBarClass, removeSideBarClass } from '../../../helpers/pages'; |
@@ -24,9 +24,9 @@ import { | |||
ItemButton, | |||
PopupPlacement, | |||
PopupZLevel, | |||
Spinner, | |||
} from 'design-system'; | |||
import * as React from 'react'; | |||
import Spinner from '../../../components/ui/Spinner'; | |||
import { addGlobalErrorMessage, addGlobalSuccessMessage } from '../../../helpers/globalMessages'; | |||
import { translate } from '../../../helpers/l10n'; | |||
import { openHotspot, probeSonarLintServers } from '../../../helpers/sonarlint'; |
@@ -28,6 +28,7 @@ import { | |||
ItemDivider, | |||
ItemHeader, | |||
PopupZLevel, | |||
Spinner, | |||
} from 'design-system'; | |||
import * as React from 'react'; | |||
import withComponentContext from '../../../app/components/componentContext/withComponentContext'; | |||
@@ -35,7 +36,6 @@ import withCurrentUserContext from '../../../app/components/current-user/withCur | |||
import HelpTooltip from '../../../components/controls/HelpTooltip'; | |||
import Tooltip from '../../../components/controls/Tooltip'; | |||
import Measure from '../../../components/measure/Measure'; | |||
import Spinner from '../../../components/ui/Spinner'; | |||
import { PopupPlacement } from '../../../components/ui/popups'; | |||
import { isBranch } from '../../../helpers/branch-like'; | |||
import { translate } from '../../../helpers/l10n'; |
@@ -19,9 +19,8 @@ | |||
*/ | |||
import { withTheme } from '@emotion/react'; | |||
import styled from '@emotion/styled'; | |||
import { themeColor } from 'design-system'; | |||
import { Spinner, themeColor } from 'design-system'; | |||
import * as React from 'react'; | |||
import Spinner from '../../../components/ui/Spinner'; | |||
import { translate } from '../../../helpers/l10n'; | |||
import { Hotspot } from '../../../types/security-hotspots'; | |||
import { |
@@ -19,13 +19,13 @@ | |||
*/ | |||
import * as React from 'react'; | |||
import withCurrentUserContext from '../../../app/components/current-user/withCurrentUserContext'; | |||
import Spinner from '../../../components/ui/Spinner'; | |||
import { fillBranchLike } from '../../../helpers/branch-like'; | |||
import { Standards } from '../../../types/security'; | |||
import { Hotspot, HotspotStatusOption } from '../../../types/security-hotspots'; | |||
import { Component } from '../../../types/types'; | |||
import { HotspotHeader } from './HotspotHeader'; | |||
import { Spinner } from 'design-system'; | |||
import { CurrentUser } from '../../../types/users'; | |||
import { RuleDescriptionSection } from '../../coding-rules/rule'; | |||
import HotspotReviewHistoryAndComments from './HotspotReviewHistoryAndComments'; |
@@ -18,6 +18,7 @@ | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import classNames from 'classnames'; | |||
import { Spinner } from 'design-system'; | |||
import React, { useCallback, useEffect } from 'react'; | |||
import { FormattedMessage } from 'react-intl'; | |||
import DocLink from '../../../components/common/DocLink'; | |||
@@ -25,7 +26,6 @@ import { ResetButtonLink, SubmitButton } from '../../../components/controls/butt | |||
import NewCodeDefinitionDaysOption from '../../../components/new-code-definition/NewCodeDefinitionDaysOption'; | |||
import NewCodeDefinitionPreviousVersionOption from '../../../components/new-code-definition/NewCodeDefinitionPreviousVersionOption'; | |||
import { NewCodeDefinitionLevels } from '../../../components/new-code-definition/utils'; | |||
import Spinner from '../../../components/ui/Spinner'; | |||
import { translate } from '../../../helpers/l10n'; | |||
import { | |||
getNumberOfDaysDefaultValue, |
@@ -24,6 +24,7 @@ import { | |||
GreySeparator, | |||
InputField, | |||
InputSelect, | |||
Spinner, | |||
Table, | |||
TableRow, | |||
} from 'design-system'; | |||
@@ -32,7 +33,6 @@ import * as React from 'react'; | |||
import { getScannableProjects } from '../../../api/components'; | |||
import withCurrentUserContext from '../../../app/components/current-user/withCurrentUserContext'; | |||
import { LabelValueSelectOption } from '../../../components/controls/Select'; | |||
import Spinner from '../../../components/ui/Spinner'; | |||
import { translate } from '../../../helpers/l10n'; | |||
import { | |||
EXPIRATION_OPTIONS, | |||
@@ -181,14 +181,6 @@ export function TokensForm(props: Readonly<Props>) { | |||
setNewTokenExpiration(newTokenExpiration?.value as TokenExpiration); | |||
}; | |||
const customSpinner = ( | |||
<tr> | |||
<td> | |||
<i className="spinner" /> | |||
</td> | |||
</tr> | |||
); | |||
const tableHeader = ( | |||
<TableRow> | |||
<ContentCell>{translate('name')}</ContentCell> | |||
@@ -299,7 +291,7 @@ export function TokensForm(props: Readonly<Props>) { | |||
header={tableHeader} | |||
noHeaderTopBorder | |||
> | |||
<Spinner customSpinner={customSpinner} loading={!!loading}> | |||
<Spinner loading={loading}> | |||
{tokens && tokens.length <= 0 ? ( | |||
<TableRow> | |||
<ContentCell className="sw-body-lg" colSpan={7}> |
@@ -24,6 +24,7 @@ import { | |||
ContentCell, | |||
DangerButtonSecondary, | |||
FlagWarningIcon, | |||
Spinner, | |||
TableRow, | |||
themeColor, | |||
} from 'design-system'; | |||
@@ -32,7 +33,6 @@ import { FormattedMessage } from 'react-intl'; | |||
import ConfirmButton from '../../../components/controls/ConfirmButton'; | |||
import DateFormatter from '../../../components/intl/DateFormatter'; | |||
import DateFromNow from '../../../components/intl/DateFromNow'; | |||
import Spinner from '../../../components/ui/Spinner'; | |||
import { translate, translateWithParameters } from '../../../helpers/l10n'; | |||
import { useRevokeTokenMutation } from '../../../queries/users'; | |||
import { UserToken } from '../../../types/token'; |
@@ -1,37 +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'; | |||
export interface FacetBoxProps { | |||
className?: string; | |||
children: React.ReactNode; | |||
property: string; | |||
} | |||
export default function FacetBox(props: FacetBoxProps) { | |||
const { children, className, property } = props; | |||
return ( | |||
<div className={classNames('search-navigator-facet-box', className)} data-property={property}> | |||
{children} | |||
</div> | |||
); | |||
} |
@@ -1,137 +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 HelpTooltip from '../../components/controls/HelpTooltip'; | |||
import { Button, ButtonLink } from '../../components/controls/buttons'; | |||
import OpenCloseIcon from '../../components/icons/OpenCloseIcon'; | |||
import { translate, translateWithParameters } from '../../helpers/l10n'; | |||
import Tooltip from '../controls/Tooltip'; | |||
import Spinner from '../ui/Spinner'; | |||
interface Props { | |||
children?: React.ReactNode; | |||
fetching?: boolean; | |||
helper?: string; | |||
disabled?: boolean; | |||
disabledHelper?: string; | |||
name: string; | |||
id: string; | |||
onClear?: () => void; | |||
onClick?: () => void; | |||
open: boolean; | |||
values?: string[]; | |||
} | |||
export default class FacetHeader extends React.PureComponent<Props> { | |||
handleClick = (event: React.SyntheticEvent<HTMLButtonElement>) => { | |||
event.preventDefault(); | |||
event.nativeEvent.preventDefault(); | |||
if (this.props.onClick) { | |||
this.props.onClick(); | |||
} | |||
}; | |||
renderHelper() { | |||
if (!this.props.helper) { | |||
return null; | |||
} | |||
return <HelpTooltip className="spacer-left" overlay={this.props.helper} />; | |||
} | |||
renderValueIndicator() { | |||
const { values } = this.props; | |||
if (!values || !values.length) { | |||
return null; | |||
} | |||
const value = | |||
values.length === 1 ? values[0] : translateWithParameters('x_selected', values.length); | |||
return ( | |||
<span className="badge text-ellipsis" title={value}> | |||
{value} | |||
</span> | |||
); | |||
} | |||
render() { | |||
const { disabled, values, disabledHelper, name, open, children, fetching, id } = this.props; | |||
const showClearButton = values != null && values.length > 0 && this.props.onClear != null; | |||
const header = disabled ? ( | |||
<Tooltip overlay={disabledHelper}> | |||
<ButtonLink className="disabled" aria-disabled aria-label={`${name}, ${disabledHelper}`}> | |||
{name} | |||
</ButtonLink> | |||
</Tooltip> | |||
) : ( | |||
name | |||
); | |||
return ( | |||
<div | |||
className={classNames('search-navigator-facet-header-wrapper display-flex-center', { | |||
'expandable-header': this.props.onClick, | |||
})} | |||
> | |||
{this.props.onClick ? ( | |||
<span className="search-navigator-facet-header display-flex-center"> | |||
<button | |||
className="button-link display-flex-center" | |||
type="button" | |||
onClick={this.handleClick} | |||
aria-expanded={open} | |||
tabIndex={0} | |||
id={id} | |||
> | |||
<OpenCloseIcon className="little-spacer-right" open={open} /> | |||
{header} | |||
</button> | |||
{this.renderHelper()} | |||
</span> | |||
) : ( | |||
<span className="search-navigator-facet-header display-flex-center" id={id}> | |||
{header} | |||
{this.renderHelper()} | |||
</span> | |||
)} | |||
{children} | |||
<span className="search-navigator-facet-header-value spacer-left spacer-right "> | |||
{this.renderValueIndicator()} | |||
</span> | |||
{fetching && ( | |||
<span className="little-spacer-right"> | |||
<Spinner /> | |||
</span> | |||
)} | |||
{showClearButton && ( | |||
<Button | |||
className="search-navigator-facet-header-button button-small button-red" | |||
aria-label={translateWithParameters('clear_x_filter', name)} | |||
onClick={this.props.onClear} | |||
> | |||
{translate('clear')} | |||
</Button> | |||
)} | |||
</div> | |||
); | |||
} | |||
} |
@@ -1,78 +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'; | |||
export interface Props { | |||
active?: boolean; | |||
className?: string; | |||
halfWidth?: boolean; | |||
name: React.ReactNode; | |||
onClick: (x: string, multiple?: boolean) => void; | |||
stat?: React.ReactNode; | |||
/** Textual version of `name` */ | |||
tooltip?: string; | |||
value: string; | |||
} | |||
export default class FacetItem extends React.PureComponent<Props> { | |||
static defaultProps = { | |||
halfWidth: false, | |||
}; | |||
handleClick = (event: React.MouseEvent<HTMLButtonElement>) => { | |||
event.preventDefault(); | |||
this.props.onClick(this.props.value, event.ctrlKey || event.metaKey); | |||
}; | |||
renderValue() { | |||
if (this.props.stat == null) { | |||
return null; | |||
} | |||
return <span className="facet-stat">{this.props.stat}</span>; | |||
} | |||
render() { | |||
const { name, halfWidth, active, value, tooltip } = this.props; | |||
const className = classNames('search-navigator-facet button-link', this.props.className, { | |||
active, | |||
}); | |||
return ( | |||
<span role="listitem" className={classNames({ 'search-navigator-facet-half': halfWidth })}> | |||
<button | |||
aria-checked={active} | |||
className={className} | |||
data-facet={value} | |||
onClick={this.handleClick} | |||
tabIndex={0} | |||
title={tooltip} | |||
role="checkbox" | |||
type="button" | |||
> | |||
<span className="facet-name">{name}</span> | |||
{this.renderValue()} | |||
</button> | |||
</span> | |||
); | |||
} | |||
} |
@@ -1,41 +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 * as React from 'react'; | |||
export type FacetItemsListProps = | |||
| { | |||
children?: React.ReactNode; | |||
labelledby: string; | |||
label?: never; | |||
} | |||
| { | |||
children?: React.ReactNode; | |||
labelledby?: never; | |||
label: string; | |||
}; | |||
export default function FacetItemsList({ children, labelledby, label }: FacetItemsListProps) { | |||
const props = labelledby ? { 'aria-labelledby': labelledby } : { 'aria-label': label }; | |||
return ( | |||
<div className="search-navigator-facet-list" role="list" {...props}> | |||
{children} | |||
</div> | |||
); | |||
} |
@@ -1,448 +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 { sortBy, without } from 'lodash'; | |||
import * as React from 'react'; | |||
import ListFooter from '../../components/controls/ListFooter'; | |||
import SearchBox from '../../components/controls/SearchBox'; | |||
import { Alert } from '../../components/ui/Alert'; | |||
import { translate } from '../../helpers/l10n'; | |||
import { formatMeasure } from '../../helpers/measures'; | |||
import { queriesEqual } from '../../helpers/query'; | |||
import { Dict, Paging, RawQuery } from '../../types/types'; | |||
import FacetBox from './FacetBox'; | |||
import FacetHeader from './FacetHeader'; | |||
import FacetItem from './FacetItem'; | |||
import FacetItemsList from './FacetItemsList'; | |||
import ListStyleFacetFooter from './ListStyleFacetFooter'; | |||
import MultipleSelectionHint from './MultipleSelectionHint'; | |||
interface SearchResponse<S> { | |||
maxResults?: boolean; | |||
results: S[]; | |||
paging?: Paging; | |||
} | |||
export interface Props<S> { | |||
className?: string; | |||
disabled?: boolean; | |||
disabledHelper?: string; | |||
facetHeader: string; | |||
fetching: boolean; | |||
getFacetItemText: (item: string) => string; | |||
getSearchResultKey: (result: S) => string; | |||
getSearchResultText: (result: S) => string; | |||
loadSearchResultCount?: (result: S[]) => Promise<Dict<number>>; | |||
maxInitialItems: number; | |||
maxItems: number; | |||
minSearchLength: number; | |||
onChange: (changes: Dict<string | string[]>) => void; | |||
onClear?: () => void; | |||
onItemClick?: (itemValue: string, multiple: boolean) => void; | |||
onSearch: (query: string, page?: number) => Promise<SearchResponse<S>>; | |||
onToggle: (property: string) => void; | |||
open: boolean; | |||
property: string; | |||
query?: RawQuery; | |||
renderFacetItem: (item: string) => React.ReactNode; | |||
renderSearchResult: (result: S, query: string) => React.ReactNode; | |||
searchPlaceholder: string; | |||
getSortedItems?: () => string[]; | |||
stats: Dict<number> | undefined; | |||
values: string[]; | |||
showMoreAriaLabel?: string; | |||
showLessAriaLabel?: string; | |||
} | |||
interface State<S> { | |||
autoFocus: boolean; | |||
query: string; | |||
searching: boolean; | |||
searchMaxResults?: boolean; | |||
searchPaging?: Paging; | |||
searchResults?: S[]; | |||
searchResultsCounts: Dict<number>; | |||
showFullList: boolean; | |||
} | |||
export default class ListStyleFacet<S> extends React.Component<Props<S>, State<S>> { | |||
mounted = false; | |||
static defaultProps = { | |||
maxInitialItems: 15, | |||
maxItems: 100, | |||
minSearchLength: 2, | |||
}; | |||
state: State<S> = { | |||
autoFocus: false, | |||
query: '', | |||
searching: false, | |||
searchResultsCounts: {}, | |||
showFullList: false, | |||
}; | |||
componentDidMount() { | |||
this.mounted = true; | |||
} | |||
componentDidUpdate(prevProps: Props<S>) { | |||
if (!prevProps.open && this.props.open) { | |||
// focus search field *only* if it was manually open | |||
this.setState({ autoFocus: true }); | |||
} else if ( | |||
(prevProps.open && !this.props.open) || | |||
!queriesEqual(prevProps.query || {}, this.props.query || {}) | |||
) { | |||
// reset state when closing the facet, or when query changes | |||
this.setState({ | |||
query: '', | |||
searchMaxResults: undefined, | |||
searchResults: undefined, | |||
searching: false, | |||
searchResultsCounts: {}, | |||
showFullList: false, | |||
}); | |||
} else if ( | |||
prevProps.stats !== this.props.stats && | |||
Object.keys(this.props.stats || {}).length < this.props.maxInitialItems | |||
) { | |||
// show limited list if `stats` changed and there are less than 15 items | |||
this.setState({ showFullList: false }); | |||
} | |||
} | |||
componentWillUnmount() { | |||
this.mounted = false; | |||
} | |||
handleItemClick = (itemValue: string, multiple: boolean) => { | |||
if (this.props.onItemClick) { | |||
this.props.onItemClick(itemValue, multiple); | |||
} else { | |||
const { values } = this.props; | |||
if (multiple) { | |||
const newValue = sortBy( | |||
values.includes(itemValue) ? without(values, itemValue) : [...values, itemValue], | |||
); | |||
this.props.onChange({ [this.props.property]: newValue }); | |||
} else { | |||
this.props.onChange({ | |||
[this.props.property]: values.includes(itemValue) && values.length < 2 ? [] : [itemValue], | |||
}); | |||
} | |||
} | |||
}; | |||
handleHeaderClick = () => { | |||
this.props.onToggle(this.props.property); | |||
}; | |||
handleClear = () => { | |||
if (this.props.onClear) { | |||
this.props.onClear(); | |||
} else { | |||
this.props.onChange({ [this.props.property]: [] }); | |||
} | |||
}; | |||
stopSearching = () => { | |||
if (this.mounted) { | |||
this.setState({ searching: false }); | |||
} | |||
}; | |||
search = (query: string) => { | |||
if (query.length >= this.props.minSearchLength) { | |||
this.setState({ query, searching: true }); | |||
this.props | |||
.onSearch(query) | |||
.then(this.loadCountsForSearchResults) | |||
.then(({ maxResults, paging, results, stats }) => { | |||
if (this.mounted) { | |||
this.setState((state) => ({ | |||
searching: false, | |||
searchMaxResults: maxResults, | |||
searchResults: results, | |||
searchPaging: paging, | |||
searchResultsCounts: { ...state.searchResultsCounts, ...stats }, | |||
})); | |||
} | |||
}) | |||
.catch(this.stopSearching); | |||
} else { | |||
this.setState({ query, searching: false, searchResults: [] }); | |||
} | |||
}; | |||
searchMore = () => { | |||
const { query, searchPaging, searchResults } = this.state; | |||
if (query && searchResults && searchPaging) { | |||
this.setState({ searching: true }); | |||
this.props | |||
.onSearch(query, searchPaging.pageIndex + 1) | |||
.then(this.loadCountsForSearchResults) | |||
.then(({ paging, results, stats }) => { | |||
if (this.mounted) { | |||
this.setState((state) => ({ | |||
searching: false, | |||
searchResults: [...searchResults, ...results], | |||
searchPaging: paging, | |||
searchResultsCounts: { ...state.searchResultsCounts, ...stats }, | |||
})); | |||
} | |||
}) | |||
.catch(this.stopSearching); | |||
} | |||
}; | |||
loadCountsForSearchResults = (response: SearchResponse<S>) => { | |||
const { loadSearchResultCount = () => Promise.resolve({}) } = this.props; | |||
const resultsToLoad = response.results.filter((result) => { | |||
const key = this.props.getSearchResultKey(result); | |||
return this.getStat(key) === undefined && this.state.searchResultsCounts[key] === undefined; | |||
}); | |||
if (resultsToLoad.length > 0) { | |||
return loadSearchResultCount(resultsToLoad).then((stats) => ({ ...response, stats })); | |||
} else { | |||
return { ...response, stats: {} }; | |||
} | |||
}; | |||
getStat(item: string) { | |||
const { stats } = this.props; | |||
return stats && stats[item] !== undefined ? stats && stats[item] : undefined; | |||
} | |||
getFacetHeaderId = (property: string) => { | |||
return `facet_${property}`; | |||
}; | |||
showFullList = () => { | |||
this.setState({ showFullList: true }); | |||
}; | |||
hideFullList = () => { | |||
this.setState({ showFullList: false }); | |||
}; | |||
renderList() { | |||
const { | |||
maxInitialItems, | |||
maxItems, | |||
property, | |||
stats, | |||
showMoreAriaLabel, | |||
showLessAriaLabel, | |||
values, | |||
} = this.props; | |||
if (!stats) { | |||
return null; | |||
} | |||
const sortedItems = this.props.getSortedItems | |||
? this.props.getSortedItems() | |||
: sortBy( | |||
Object.keys(stats), | |||
(key) => -stats[key], | |||
(key) => this.props.getFacetItemText(key), | |||
); | |||
const limitedList = this.state.showFullList | |||
? sortedItems | |||
: sortedItems.slice(0, maxInitialItems); | |||
// make sure all selected items are displayed | |||
const selectedBelowLimit = this.state.showFullList | |||
? [] | |||
: sortedItems.slice(maxInitialItems).filter((item) => values.includes(item)); | |||
const mightHaveMoreResults = sortedItems.length >= maxItems; | |||
return ( | |||
<> | |||
<FacetItemsList labelledby={this.getFacetHeaderId(property)}> | |||
{limitedList.map((item) => ( | |||
<FacetItem | |||
active={this.props.values.includes(item)} | |||
key={item} | |||
name={this.props.renderFacetItem(item)} | |||
onClick={this.handleItemClick} | |||
stat={formatFacetStat(this.getStat(item))} | |||
tooltip={this.props.getFacetItemText(item)} | |||
value={item} | |||
/> | |||
))} | |||
</FacetItemsList> | |||
{selectedBelowLimit.length > 0 && ( | |||
<> | |||
<div className="note spacer-bottom text-center">⋯</div> | |||
<FacetItemsList labelledby={this.getFacetHeaderId(property)}> | |||
{selectedBelowLimit.map((item) => ( | |||
<FacetItem | |||
active | |||
key={item} | |||
name={this.props.renderFacetItem(item)} | |||
onClick={this.handleItemClick} | |||
stat={formatFacetStat(this.getStat(item))} | |||
tooltip={this.props.getFacetItemText(item)} | |||
value={item} | |||
/> | |||
))} | |||
</FacetItemsList> | |||
</> | |||
)} | |||
<ListStyleFacetFooter | |||
count={limitedList.length + selectedBelowLimit.length} | |||
showLess={this.state.showFullList ? this.hideFullList : undefined} | |||
showMore={this.showFullList} | |||
total={sortedItems.length} | |||
showMoreAriaLabel={showMoreAriaLabel} | |||
showLessAriaLabel={showLessAriaLabel} | |||
/> | |||
{mightHaveMoreResults && this.state.showFullList && ( | |||
<Alert className="spacer-top" variant="warning"> | |||
{translate('facet_might_have_more_results')} | |||
</Alert> | |||
)} | |||
</> | |||
); | |||
} | |||
renderSearch() { | |||
return ( | |||
<SearchBox | |||
autoFocus={this.state.autoFocus} | |||
className="little-spacer-top spacer-bottom" | |||
loading={this.state.searching} | |||
minLength={this.props.minSearchLength} | |||
onChange={this.search} | |||
placeholder={this.props.searchPlaceholder} | |||
value={this.state.query} | |||
/> | |||
); | |||
} | |||
renderSearchResults() { | |||
const { property, showMoreAriaLabel } = this.props; | |||
const { searching, searchMaxResults, searchResults, searchPaging } = this.state; | |||
if (!searching && (!searchResults || !searchResults.length)) { | |||
return <div className="note spacer-bottom">{translate('no_results')}</div>; | |||
} | |||
if (!searchResults) { | |||
// initial search | |||
return null; | |||
} | |||
return ( | |||
<> | |||
<FacetItemsList labelledby={this.getFacetHeaderId(property)}> | |||
{searchResults.map((result) => this.renderSearchResult(result))} | |||
</FacetItemsList> | |||
{searchMaxResults && ( | |||
<Alert className="spacer-top" variant="warning"> | |||
{translate('facet_might_have_more_results')} | |||
</Alert> | |||
)} | |||
{searchPaging && ( | |||
<ListFooter | |||
className="spacer-bottom" | |||
count={searchResults.length} | |||
loadMore={this.searchMore} | |||
ready={!searching} | |||
total={searchPaging.total} | |||
loadMoreAriaLabel={showMoreAriaLabel} | |||
/> | |||
)} | |||
</> | |||
); | |||
} | |||
renderSearchResult(result: S) { | |||
const key = this.props.getSearchResultKey(result); | |||
const active = this.props.values.includes(key); | |||
const stat = this.getStat(key) || this.state.searchResultsCounts[key]; | |||
return ( | |||
<FacetItem | |||
active={active} | |||
key={key} | |||
name={this.props.renderSearchResult(result, this.state.query)} | |||
onClick={this.handleItemClick} | |||
stat={formatFacetStat(stat)} | |||
tooltip={this.props.getSearchResultText(result)} | |||
value={key} | |||
/> | |||
); | |||
} | |||
render() { | |||
const { | |||
className, | |||
disabled, | |||
disabledHelper, | |||
facetHeader, | |||
fetching, | |||
open, | |||
property, | |||
stats = {}, | |||
values: propsValues, | |||
} = this.props; | |||
const { query, searching, searchResults } = this.state; | |||
const values = propsValues.map((item) => this.props.getFacetItemText(item)); | |||
const loadingResults = | |||
query !== '' && searching && (searchResults === undefined || searchResults.length === 0); | |||
const showList = !query || loadingResults; | |||
return ( | |||
<FacetBox | |||
className={classNames(className, { | |||
'search-navigator-facet-box-forbidden': disabled, | |||
})} | |||
property={property} | |||
> | |||
<FacetHeader | |||
fetching={fetching} | |||
name={facetHeader} | |||
disabled={disabled} | |||
id={this.getFacetHeaderId(property)} | |||
disabledHelper={disabledHelper} | |||
onClear={this.handleClear} | |||
onClick={disabled ? undefined : this.handleHeaderClick} | |||
open={open && !disabled} | |||
values={values} | |||
/> | |||
{open && !disabled && ( | |||
<> | |||
{this.renderSearch()} | |||
{showList ? this.renderList() : this.renderSearchResults()} | |||
<MultipleSelectionHint options={Object.keys(stats).length} values={values.length} /> | |||
</> | |||
)} | |||
</FacetBox> | |||
); | |||
} | |||
} | |||
function formatFacetStat(stat: number | undefined) { | |||
return stat && formatMeasure(stat, 'SHORT_INT'); | |||
} |
@@ -1,77 +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 * as React from 'react'; | |||
import { translate, translateWithParameters } from '../../helpers/l10n'; | |||
import { formatMeasure } from '../../helpers/measures'; | |||
import { MetricType } from '../../types/metrics'; | |||
import { ButtonLink } from '../controls/buttons'; | |||
interface Props { | |||
count: number; | |||
showMore: () => void; | |||
showLess?: () => void; | |||
total: number; | |||
showMoreAriaLabel?: string; | |||
showLessAriaLabel?: string; | |||
} | |||
export default class ListStyleFacetFooter extends React.PureComponent<Props> { | |||
handleShowMoreClick = () => { | |||
this.props.showMore(); | |||
}; | |||
handleShowLessClick = () => { | |||
if (this.props.showLess) { | |||
this.props.showLess(); | |||
} | |||
}; | |||
render() { | |||
const { count, total, showMoreAriaLabel, showLessAriaLabel } = this.props; | |||
const hasMore = total > count; | |||
const allShown = Boolean(total && total === count); | |||
return ( | |||
<div className="note spacer-top spacer-bottom text-center"> | |||
{translateWithParameters('x_show', formatMeasure(count, MetricType.Integer))} | |||
{hasMore && ( | |||
<ButtonLink | |||
className="spacer-left" | |||
aria-label={showMoreAriaLabel} | |||
onClick={this.handleShowMoreClick} | |||
> | |||
{translate('show_more')} | |||
</ButtonLink> | |||
)} | |||
{this.props.showLess && allShown && ( | |||
<ButtonLink | |||
className="spacer-left" | |||
aria-label={showLessAriaLabel} | |||
onClick={this.handleShowLessClick} | |||
> | |||
{translate('show_less')} | |||
</ButtonLink> | |||
)} | |||
</div> | |||
); | |||
} | |||
} |
@@ -1,35 +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. | |||
*/ | |||
.multiple-selection-hint { | |||
margin-top: var(--gridSize); | |||
margin-bottom: var(--gridSize); | |||
text-align: center; | |||
} | |||
.multiple-selection-hint-inner { | |||
display: inline-block; | |||
height: var(--controlHeight); | |||
line-height: var(--controlHeight); | |||
border-radius: var(--controlHeight); | |||
background-color: var(--barBorderColor); | |||
text-align: center; | |||
padding: 0 var(--gridSize); | |||
font-size: var(--smallFontSize); | |||
} |
@@ -1,52 +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 * as React from 'react'; | |||
import { translate } from '../../helpers/l10n'; | |||
import './MultipleSelectionHint.css'; | |||
export interface MultipleSelectionHintProps { | |||
options: number; | |||
values: number; | |||
} | |||
const MAX_OPTIONS = 2; | |||
export default function MultipleSelectionHint({ options, values }: MultipleSelectionHintProps) { | |||
// do not render if nothing is selected or there are less than 2 possible options | |||
if (values === 0 || options < MAX_OPTIONS) { | |||
return null; | |||
} | |||
return ( | |||
<div className="multiple-selection-hint"> | |||
<div className="multiple-selection-hint-inner"> | |||
{translate( | |||
isOnMac() | |||
? 'shortcuts.section.global.facets.multiselection.mac' | |||
: 'shortcuts.section.global.facets.multiselection', | |||
)} | |||
</div> | |||
</div> | |||
); | |||
} | |||
function isOnMac() { | |||
return navigator.userAgent.indexOf('Mac OS X') !== -1; | |||
} |
@@ -1,126 +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 { screen } from '@testing-library/react'; | |||
import userEvent from '@testing-library/user-event'; | |||
import * as React from 'react'; | |||
import { renderComponent } from '../../../helpers/testReactTestingUtils'; | |||
import FacetBox, { FacetBoxProps } from '../FacetBox'; | |||
import FacetHeader from '../FacetHeader'; | |||
import FacetItem from '../FacetItem'; | |||
import FacetItemsList from '../FacetItemsList'; | |||
it('should render and function correctly', async () => { | |||
const user = userEvent.setup(); | |||
const onFacetClick = jest.fn(); | |||
renderFacet(undefined, undefined, { onClick: onFacetClick }); | |||
// Start closed. | |||
let facetHeader = screen.getByRole('button', { name: 'foo', expanded: false }); | |||
expect(facetHeader).toBeInTheDocument(); | |||
expect(screen.queryByText('Foo/Bar')).not.toBeInTheDocument(); | |||
// Expand. | |||
await user.click(facetHeader); | |||
facetHeader = screen.getByRole('button', { name: 'foo', expanded: true }); | |||
expect(facetHeader).toBeInTheDocument(); | |||
expect(screen.getByText('Foo/Bar')).toBeInTheDocument(); | |||
// Interact with facets. | |||
const facet1 = screen.getByRole('checkbox', { name: 'Foo/Bar 10' }); | |||
expect(facet1).toHaveClass('active'); | |||
await user.click(facet1); | |||
expect(onFacetClick).toHaveBeenCalledWith('bar', false); | |||
const facet2 = screen.getByRole('checkbox', { name: 'Foo/Baz' }); | |||
expect(facet2).not.toHaveClass('active'); | |||
// Collapse again. | |||
await user.click(facetHeader); | |||
expect(screen.getByRole('button', { name: 'foo', expanded: false })).toBeInTheDocument(); | |||
expect(screen.queryByText('Foo/Bar')).not.toBeInTheDocument(); | |||
}); | |||
it('should correctly render a header with helper text', async () => { | |||
renderFacet(undefined, { helper: 'Help text' }); | |||
await expect(screen.getByRole('img', { description: 'Help text' })).toHaveATooltipWithContent( | |||
'Help text', | |||
); | |||
}); | |||
it('should correctly render a header with value data', async () => { | |||
const user = userEvent.setup(); | |||
renderFacet(undefined, { values: ['value 1'] }); | |||
expect(screen.getByText('value 1')).toBeInTheDocument(); | |||
await user.click(screen.getByRole('button', { name: 'clear_x_filter.foo' })); | |||
expect(screen.queryByText('value 1')).not.toBeInTheDocument(); | |||
}); | |||
it('should correctly render a disabled header', () => { | |||
renderFacet(undefined, { onClick: undefined }); | |||
expect(screen.queryByRole('checkbox', { name: 'foo' })).not.toBeInTheDocument(); | |||
}); | |||
function renderFacet( | |||
facetBoxProps: Partial<FacetBoxProps> = {}, | |||
facetHeaderProps: Partial<FacetHeader['props']> = {}, | |||
facetItemProps: Partial<FacetItem['props']> = {}, | |||
) { | |||
function Facet() { | |||
const [open, setOpen] = React.useState(facetHeaderProps.open ?? false); | |||
const [values, setValues] = React.useState(facetHeaderProps.values ?? undefined); | |||
const property = 'foo'; | |||
const headerId = `facet_${property}`; | |||
return ( | |||
<FacetBox property={property} {...facetBoxProps}> | |||
<FacetHeader | |||
id={headerId} | |||
name="foo" | |||
onClick={() => setOpen(!open)} | |||
onClear={() => setValues(undefined)} | |||
{...{ ...facetHeaderProps, open, values }} | |||
/> | |||
{open && ( | |||
<FacetItemsList labelledby={headerId}> | |||
<FacetItem | |||
active | |||
name="Foo/Bar" | |||
onClick={jest.fn()} | |||
value="bar" | |||
stat={10} | |||
{...facetItemProps} | |||
/> | |||
<FacetItem | |||
active={false} | |||
name="Foo/Baz" | |||
onClick={jest.fn()} | |||
value="baz" | |||
{...facetItemProps} | |||
/> | |||
</FacetItemsList> | |||
)} | |||
</FacetBox> | |||
); | |||
} | |||
return renderComponent(<Facet />); | |||
} |