Browse Source

SONAR-21656 Drop old page.css classes and associated

tags/10.5.0.89998
Grégoire Aubert 2 months ago
parent
commit
238317635b
31 changed files with 106 additions and 1001 deletions
  1. 0
    5
      server/sonar-web/design-system/src/components/Badge.tsx
  2. 3
    2
      server/sonar-web/src/main/js/app/components/FormattingHelp.tsx
  3. 29
    24
      server/sonar-web/src/main/js/app/components/GlobalContainer.tsx
  4. 4
    4
      server/sonar-web/src/main/js/app/components/PluginRiskConsent.tsx
  5. 2
    2
      server/sonar-web/src/main/js/app/components/ResetPassword.tsx
  6. 2
    2
      server/sonar-web/src/main/js/app/components/SimpleContainer.tsx
  7. 2
    2
      server/sonar-web/src/main/js/app/components/SimpleSessionsContainer.tsx
  8. 0
    268
      server/sonar-web/src/main/js/app/styles/components/page.css
  9. 0
    1
      server/sonar-web/src/main/js/app/styles/sonar.ts
  10. 0
    2
      server/sonar-web/src/main/js/app/theme.js
  11. 44
    50
      server/sonar-web/src/main/js/apps/background-tasks/components/BackgroundTasksApp.tsx
  12. 1
    1
      server/sonar-web/src/main/js/apps/change-admin-password/ChangeAdminPasswordAppRenderer.tsx
  13. 0
    10
      server/sonar-web/src/main/js/apps/coding-rules/components/CodingRulesApp.tsx
  14. 1
    1
      server/sonar-web/src/main/js/apps/component-measures/components/ComponentMeasuresApp.tsx
  15. 0
    11
      server/sonar-web/src/main/js/apps/issues/components/IssuesApp.tsx
  16. 0
    61
      server/sonar-web/src/main/js/apps/issues/styles.css
  17. 0
    4
      server/sonar-web/src/main/js/apps/projects/components/AllProjects.tsx
  18. 0
    165
      server/sonar-web/src/main/js/apps/projects/styles.css
  19. 4
    20
      server/sonar-web/src/main/js/apps/quality-gates/components/App.tsx
  20. 10
    12
      server/sonar-web/src/main/js/apps/quality-gates/components/Details.tsx
  21. 0
    1
      server/sonar-web/src/main/js/apps/security-hotspots/SecurityHotspotsApp.tsx
  22. 0
    1
      server/sonar-web/src/main/js/apps/security-hotspots/SecurityHotspotsAppRenderer.tsx
  23. 0
    5
      server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotList.tsx
  24. 0
    62
      server/sonar-web/src/main/js/apps/security-hotspots/styles.css
  25. 0
    10
      server/sonar-web/src/main/js/apps/settings/components/SettingsApp.tsx
  26. 1
    169
      server/sonar-web/src/main/js/apps/settings/styles.css
  27. 0
    5
      server/sonar-web/src/main/js/apps/web-api/__tests__/WebApi-it.tsx
  28. 0
    3
      server/sonar-web/src/main/js/apps/web-api/components/WebApiApp.tsx
  29. 3
    7
      server/sonar-web/src/main/js/components/common/VisibilitySelector.tsx
  30. 0
    47
      server/sonar-web/src/main/js/helpers/__tests__/pages-test.ts
  31. 0
    44
      server/sonar-web/src/main/js/helpers/pages.ts

+ 0
- 5
server/sonar-web/design-system/src/components/Badge.tsx View File

@@ -74,11 +74,6 @@ const StyledBadge = styled.span<{
&:empty {
${tw`sw-hidden`}
}

.page-actions & {
${tw`sw-my-1`};
${tw`sw-mx-0`};
}
`;

const StyledCounter = styled.span<{

+ 3
- 2
server/sonar-web/src/main/js/app/components/FormattingHelp.tsx View File

@@ -17,13 +17,14 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import { CenteredLayout } from 'design-system';
import * as React from 'react';
import { Helmet } from 'react-helmet-async';
import { translate } from '../../helpers/l10n';

export default function FormattingHelp() {
return (
<div className="page page-limited">
<CenteredLayout className="sw-py-6 sw-h-screen">
<Helmet defer={false} title={translate('formatting.page')} />
<h2 className="spacer-bottom">Formatting Syntax</h2>
<table className="width-100 data zebra">
@@ -147,6 +148,6 @@ export default function FormattingHelp() {
</tr>
</tbody>
</table>
</div>
</CenteredLayout>
);
}

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

@@ -82,38 +82,36 @@ export default function GlobalContainer() {
<SuggestionsProvider>
<A11yProvider>
<A11ySkipLinks />
<div className="global-container">
<GlobalContainerWrapper>
<GlobalBackground
secondary={PAGES_WITH_SECONDARY_BACKGROUND.includes(location.pathname)}
className="sw-box-border sw-flex-[1_0_auto]"
id="container"
>
<div className="page-container">
<Workspace>
<IndexationContextProvider>
<LanguagesContextProvider>
<MetricsContextProvider>
<div className="sw-sticky sw-top-0 sw-z-global-navbar">
<SystemAnnouncement />
<IndexationNotification />
<NCDAutoUpdateMessage />
<UpdateNotification dismissable />
<GlobalNav location={location} />
{/* The following is the portal anchor point for the component nav
* See ComponentContainer.tsx
*/}
<div id="component-nav-portal" />
</div>
<Outlet />
</MetricsContextProvider>
</LanguagesContextProvider>
</IndexationContextProvider>
</Workspace>
</div>
<Workspace>
<IndexationContextProvider>
<LanguagesContextProvider>
<MetricsContextProvider>
<div className="sw-sticky sw-top-0 sw-z-global-navbar">
<SystemAnnouncement />
<IndexationNotification />
<NCDAutoUpdateMessage />
<UpdateNotification dismissable />
<GlobalNav location={location} />
{/* The following is the portal anchor point for the component nav
* See ComponentContainer.tsx
*/}
<div id="component-nav-portal" />
</div>
<Outlet />
</MetricsContextProvider>
</LanguagesContextProvider>
</IndexationContextProvider>
</Workspace>
<PromotionNotification />
</GlobalBackground>
<GlobalFooter />
</div>
</GlobalContainerWrapper>
<StartupModal />
</A11yProvider>
</SuggestionsProvider>
@@ -121,6 +119,13 @@ export default function GlobalContainer() {
);
}

const GlobalContainerWrapper = styled.div`
display: flex;
flex-direction: column;
height: 100%;
min-height: 100vh;
`;

const GlobalBackground = styled.div<{ secondary: boolean }>`
background-color: ${({ secondary }) =>
themeColor(secondary ? 'backgroundSecondary' : 'backgroundPrimary')};

+ 4
- 4
server/sonar-web/src/main/js/app/components/PluginRiskConsent.tsx View File

@@ -18,7 +18,7 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/

import { ButtonPrimary, Card, Title } from 'design-system';
import { ButtonPrimary, Card, CenteredLayout, Title } from 'design-system';
import * as React from 'react';
import { Helmet } from 'react-helmet-async';
import { setSimpleSettingValue } from '../../api/settings';
@@ -67,11 +67,11 @@ export function PluginRiskConsent(props: Readonly<PluginRiskConsentProps>) {
};

return (
<>
<CenteredLayout className="sw-h-screen sw-pt-[10vh]">
<Helmet defer={false} title={translate('plugin_risk_consent.page')} />

<Card
className="sw-body-md sw-min-w-[500px] sw-mx-auto sw-mt-[10vh] sw-w-[40%] sw-text-center"
className="sw-body-md sw-min-w-[500px] sw-mx-auto sw-w-[40%] sw-text-center"
data-testid="plugin-risk-consent-page"
>
<Title className="sw-mb-4">{translate('plugin_risk_consent.title')}</Title>
@@ -84,7 +84,7 @@ export function PluginRiskConsent(props: Readonly<PluginRiskConsentProps>) {
{translate('plugin_risk_consent.action')}
</ButtonPrimary>
</Card>
</>
</CenteredLayout>
);
}


+ 2
- 2
server/sonar-web/src/main/js/app/components/ResetPassword.tsx View File

@@ -38,10 +38,10 @@ export interface ResetPasswordProps {

export function ResetPassword({ currentUser }: Readonly<ResetPasswordProps>) {
return (
<LargeCenteredLayout>
<LargeCenteredLayout className="sw-h-screen sw-pt-10">
<PageContentFontWrapper className="sw-body-sm">
<Helmet defer={false} title={translate('my_account.reset_password.page')} />
<div className="sw-flex sw-justify-center sw-mt-10">
<div className="sw-flex sw-justify-center">
<div>
<Title>{translate('my_account.reset_password')}</Title>
<FlagMessage variant="warning" className="sw-mb-4">

+ 2
- 2
server/sonar-web/src/main/js/app/components/SimpleContainer.tsx View File

@@ -28,8 +28,8 @@ import MainSonarQubeBar from './nav/global/MainSonarQubeBar';
*/
export default function SimpleContainer({ children }: { children?: React.ReactNode }) {
return (
<div className="global-container new-background">
<div className="page-wrapper" id="container">
<div className="sw-flex sw-flex-col sw-h-full sw-min-h-[100vh]">
<div className="sw-box-border sw-flex-auto" id="container">
<MainSonarQubeBar />
{children !== undefined ? children : <Outlet />}
</div>

+ 2
- 2
server/sonar-web/src/main/js/app/components/SimpleSessionsContainer.tsx View File

@@ -27,8 +27,8 @@ export default function SimpleSessionsContainer() {
<>
<PageTracker />

<div className="global-container">
<div className="page-wrapper new-background" id="container">
<div className="sw-flex sw-flex-col sw-h-full sw-min-h-[100vh]">
<div className="sw-box-border sw-flex-auto" id="container">
<Outlet />
</div>
<GlobalFooter hideLoggedInInfo />

+ 0
- 268
server/sonar-web/src/main/js/app/styles/components/page.css View File

@@ -1,268 +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.
*/
.white-page {
background-color: #fff !important;
}

.global-container {
display: flex;
flex-direction: column;
height: 100%;
min-height: 100vh;
}

.page {
z-index: var(--normalZIndex);
padding: 10px 20px;
}

.page:before,
.page:after {
display: table;
content: '';
line-height: 0;
}

.page:after {
clear: both;
}

.page-limited {
max-width: 1280px;
margin-left: auto;
margin-right: auto;
padding-top: 20px;
padding-bottom: 20px;
}

.page-container {
min-width: var(--minPageWidth);
}

.page-wrapper {
box-sizing: border-box;
flex: 1 0 auto;
}

.page-header {
position: relative;
margin-bottom: 20px;
}

.page-header:before,
.page-header:after {
display: table;
content: '';
line-height: 0;
}

.page-header:after {
clear: both;
}

.page-title {
float: left;
margin-bottom: 0;
line-height: var(--controlHeight);
}

.page-actions {
float: right;
margin-bottom: 10px;
margin-left: 10px;
line-height: var(--controlHeight);
text-align: right;
}

.page-actions .badge {
margin: 3px 0;
}

.page-description {
float: left;
clear: left;
max-width: 800px;
line-height: 1.5;
margin-top: 6px;
}

.page-with-sidebar {
display: flex;
}

.page-main {
flex-grow: 1;
}

.page-sidebar {
width: 30%;
min-width: 300px;
flex-shrink: 0;
padding-left: 40px;
box-sizing: border-box;
}

.page-sidebar-fixed {
min-width: 300px;
flex-shrink: 0;
padding-left: 40px;
box-sizing: border-box;
width: 300px;
}

.page-sidebar-sticky {
width: 320px !important;
padding-right: 0;
}

.page-limited .page-sidebar-sticky {
margin: -20px 0 -20px -20px;
padding-right: 0 !important;
}

.page-limited .page-sidebar-sticky .page-sidebar-sticky-inner {
padding: 20px 0;
}

.page-sidebar-sticky .page-sidebar-sticky-inner {
position: fixed;
z-index: 10;
top: 30px;
bottom: 0;
left: 0;
overflow: auto;
width: calc(50vw - 640px + 280px + 3px);
border-right: 1px solid var(--barBorderColor);
box-sizing: border-box;
background: var(--barBackgroundColor);
}

@media (max-width: 1335px) {
.page-sidebar-sticky .page-sidebar-sticky-inner {
width: 310px;
}
}

.layout-page {
display: flex;
align-items: stretch;
width: 100%;
flex-grow: 1;
}

.layout-page-filters {
width: 260px;
padding: 20px;
}

.layout-page-main {
flex-grow: 1;
min-width: 740px;
padding: 20px;
z-index: var(--pageMainZIndex);
}

.layout-page-main-inner {
position: relative;
z-index: var(--normalZIndex);
min-width: 740px;
max-width: 980px;
}

.layout-page-side-outer {
width: calc(50vw - 370px);
flex-grow: 0;
flex-shrink: 0;
background-color: var(--barBackgroundColor);
}

.layout-page-side {
position: fixed;
z-index: var(--pageSideZIndex);
top: 30px;
bottom: 0;
left: 0;
width: calc(50vw - 370px);
border-right: 1px solid var(--barBorderColor);
overflow-y: auto;
overflow-x: hidden;
background-color: var(--barBackgroundColor);
}

.layout-page-side-inner {
width: 300px;
margin-left: calc(50vw - 670px);
background-color: var(--barBackgroundColor);
}

.layout-page-header-panel,
.layout-page-header-panel-inner {
height: 56px;
box-sizing: border-box;
}

.layout-page-header-panel {
margin-top: -20px;
}

.layout-page-header-panel-inner {
position: fixed;
z-index: 30;
line-height: var(--controlHeight);
padding-top: 16px;
padding-bottom: 16px;
border-bottom: 1px solid var(--barBorderColor);
background-color: var(--barBackgroundColor);
}

.layout-page-main-header {
position: relative;
z-index: var(--aboveNormalZIndex);
margin-bottom: 20px;
}

.layout-page-main-header .component-name {
line-height: var(--controlHeight);
}

.layout-page-main-header-inner {
left: calc(50vw - 370px + 1px);
right: 0;
padding-left: 20px;
padding-right: 20px;
}

@media (max-width: 1320px) {
.layout-page-side-outer {
width: 300px;
}

.layout-page-side {
width: 300px;
}

.layout-page-side-inner {
margin-left: 0;
}

.layout-page-main-header-inner {
left: 301px;
}
}

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

@@ -24,7 +24,6 @@ import '../../../../../public/fonts/Inter/inter.css';
import '../../../../../public/fonts/Ubuntu/Ubuntu.css';

import './components/global-loading.css';
import './components/page.css';
import './init/base.css';
import './init/misc.css';
import './print.css';

+ 0
- 2
server/sonar-web/src/main/js/app/theme.js View File

@@ -218,8 +218,6 @@ module.exports = {

globalNavContentHeight: `${4 * grid}px`,

maxPageWidth: '1320px',
minPageWidth: '1080px',
pagePadding: '20px',
},


+ 44
- 50
server/sonar-web/src/main/js/apps/background-tasks/components/BackgroundTasksApp.tsx View File

@@ -17,7 +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 { LargeCenteredLayout, PageContentFontWrapper, Spinner } from 'design-system';
import { Spinner } from '@sonarsource/echoes-react';
import { LargeCenteredLayout, PageContentFontWrapper } from 'design-system';
import { debounce } from 'lodash';
import * as React from 'react';
import { Helmet } from 'react-helmet-async';
@@ -215,15 +216,6 @@ export class BackgroundTasksApp extends React.PureComponent<Props, State> {
const { component, location } = this.props;
const { loading, pagination, types, tasks } = this.state;

if (!types) {
return (
<div className="page page-limited">
<Helmet defer={false} title={translate('background_tasks.page')} />
<Spinner />
</div>
);
}

const status = location.query.status || DEFAULT_FILTERS.status;
const taskType = location.query.taskType || DEFAULT_FILTERS.taskType;
const currents = location.query.currents || DEFAULT_FILTERS.currents;
@@ -233,48 +225,50 @@ export class BackgroundTasksApp extends React.PureComponent<Props, State> {

return (
<LargeCenteredLayout id="background-tasks">
<PageContentFontWrapper className="sw-my-8 sw-body-sm">
<PageContentFontWrapper className="sw-my-4 sw-body-sm">
<Suggestions suggestions="background_tasks" />
<Helmet defer={false} title={translate('background_tasks.page')} />
<Header component={component} />

<Stats
component={component}
failingCount={this.state.failingCount}
onCancelAllPending={this.handleCancelAllPending}
onShowFailing={this.handleShowFailing}
pendingCount={this.state.pendingCount}
pendingTime={this.state.pendingTime}
/>

<Search
component={component}
currents={currents}
loading={loading}
maxExecutedAt={maxExecutedAt}
minSubmittedAt={minSubmittedAt}
onFilterUpdate={this.handleFilterUpdate}
onReload={this.loadTasksDebounced}
query={query}
status={status}
taskType={taskType}
types={types}
/>

<Tasks
component={component}
onCancelTask={this.handleCancelTask}
onFilterTask={this.handleFilterTask}
tasks={tasks}
/>

<ListFooter
count={tasks.length}
loadMore={this.loadMoreTasks}
loading={loading}
pageSize={pagination.pageSize}
total={pagination.total}
/>
<Spinner isLoading={!types}>
<Header component={component} />

<Stats
component={component}
failingCount={this.state.failingCount}
onCancelAllPending={this.handleCancelAllPending}
onShowFailing={this.handleShowFailing}
pendingCount={this.state.pendingCount}
pendingTime={this.state.pendingTime}
/>

<Search
component={component}
currents={currents}
loading={loading}
maxExecutedAt={maxExecutedAt}
minSubmittedAt={minSubmittedAt}
onFilterUpdate={this.handleFilterUpdate}
onReload={this.loadTasksDebounced}
query={query}
status={status}
taskType={taskType}
types={types ?? []}
/>

<Tasks
component={component}
onCancelTask={this.handleCancelTask}
onFilterTask={this.handleFilterTask}
tasks={tasks}
/>

<ListFooter
count={tasks.length}
loadMore={this.loadMoreTasks}
loading={loading}
pageSize={pagination.pageSize}
total={pagination.total}
/>
</Spinner>
</PageContentFontWrapper>
</LargeCenteredLayout>
);

+ 1
- 1
server/sonar-web/src/main/js/apps/change-admin-password/ChangeAdminPasswordAppRenderer.tsx View File

@@ -74,7 +74,7 @@ export default function ChangeAdminPasswordAppRenderer(
}

return (
<CenteredLayout>
<CenteredLayout className="sw-h-screen">
<Helmet defer={false} title={translate('users.change_admin_password.page')} />

<PageContentFontWrapper className="sw-body-sm sw-flex sw-flex-col sw-items-center sw-justify-center">

+ 0
- 10
server/sonar-web/src/main/js/apps/coding-rules/components/CodingRulesApp.tsx View File

@@ -42,12 +42,6 @@ import '../../../components/search-navigator.css';
import { isInput, isShortcut } from '../../../helpers/keyboardEventHelpers';
import { KeyboardKeys } from '../../../helpers/keycodes';
import { translate, translateWithParameters } from '../../../helpers/l10n';
import {
addSideBarClass,
addWhitePageClass,
removeSideBarClass,
removeWhitePageClass,
} from '../../../helpers/pages';
import { SecurityStandard } from '../../../types/security';
import { SettingsKey } from '../../../types/settings';
import { Dict, Paging, RawQuery, Rule, RuleActivation } from '../../../types/types';
@@ -135,8 +129,6 @@ export class CodingRulesApp extends React.PureComponent<Props, State> {

componentDidMount() {
this.mounted = true;
addWhitePageClass();
addSideBarClass();
this.attachShortcuts();
this.fetchInitialData();
}
@@ -154,8 +146,6 @@ export class CodingRulesApp extends React.PureComponent<Props, State> {

componentWillUnmount() {
this.mounted = false;
removeWhitePageClass();
removeSideBarClass();
this.detachShortcuts();
}


+ 1
- 1
server/sonar-web/src/main/js/apps/component-measures/components/ComponentMeasuresApp.tsx View File

@@ -280,7 +280,7 @@ class ComponentMeasuresApp extends React.PureComponent<Props, State> {
<Suggestions suggestions="component_measures" />
<Helmet defer={false} title={translate('layout.measures')} />
<PageContentFontWrapper className="sw-body-sm">
<Spinner className="my-10 sw-flex sw-content-center" isLoading={this.state.loading} />
<Spinner isLoading={this.state.loading} />

{measures.length > 0 ? (
<div className="sw-grid sw-grid-cols-12 sw-w-full">

+ 0
- 11
server/sonar-web/src/main/js/apps/issues/components/IssuesApp.tsx View File

@@ -63,12 +63,6 @@ import { parseIssueFromResponse } from '../../../helpers/issues';
import { isInput, isShortcut } from '../../../helpers/keyboardEventHelpers';
import { KeyboardKeys } from '../../../helpers/keycodes';
import { translate, translateWithParameters } from '../../../helpers/l10n';
import {
addSideBarClass,
addWhitePageClass,
removeSideBarClass,
removeWhitePageClass,
} from '../../../helpers/pages';
import { serializeDate } from '../../../helpers/query';
import { withBranchLikes } from '../../../queries/branch';
import { BranchLike } from '../../../types/branch-like';
@@ -225,8 +219,6 @@ export class App extends React.PureComponent<Props, State> {
return;
}

addWhitePageClass();
addSideBarClass();
this.attachShortcuts();

if (!this.props.isFetchingBranch) {
@@ -272,9 +264,6 @@ export class App extends React.PureComponent<Props, State> {
componentWillUnmount() {
this.detachShortcuts();
this.mounted = false;

removeWhitePageClass();
removeSideBarClass();
}

attachShortcuts() {

+ 0
- 61
server/sonar-web/src/main/js/apps/issues/styles.css View File

@@ -17,33 +17,6 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
.not-all-issue-warning.open-issue-list {
background-color: var(--barBackgroundColor);
box-sizing: border-box;
display: inline-block;
padding: 16px 16px 0;
position: sticky;
top: 0;
z-index: 1000;
}

.issues .issue-list {
/* no math, just a good guess */
min-width: 640px;
width: 800px;
}

.issues .issue a:focus,
.issues .issue button:focus {
box-shadow: none;
}

@media (max-width: 1320px) {
.issues .issue-list {
width: calc(60vw - 40px);
}
}

.issue-location {
display: inline-block;
vertical-align: top;
@@ -52,37 +25,3 @@
background-color: var(--issueBgColor);
transition: background-color 0.3s ease;
}

.issues-workspace-list-component {
padding: 15px 0 6px;
}

.issues-workspace-list-item + .issues-workspace-list-item {
margin-top: 5px;
}

li:first-child .issues-workspace-list-component {
padding-top: 0;
}

.issues-predefined-periods {
display: flex;
}

.issues-predefined-periods .search-navigator-facet {
width: auto;
margin-right: calc(var(--gridSize) / 2);
}

.bulk-change-radio-button {
margin: 0 calc(-1 * var(--gridSize) / 2);
padding: 0 calc(var(--gridSize) / 2);
}

.bulk-change-radio-button:hover {
background-color: var(--barBackgroundColor);
}

.layout-page-main.open-issue {
padding-top: 0;
}

+ 0
- 4
server/sonar-web/src/main/js/apps/projects/components/AllProjects.tsx View File

@@ -41,7 +41,6 @@ import { Location, Router, withRouter } from '../../../components/hoc/withRouter
import '../../../components/search-navigator.css';
import handleRequiredAuthentication from '../../../helpers/handleRequiredAuthentication';
import { translate } from '../../../helpers/l10n';
import { addSideBarClass, removeSideBarClass } from '../../../helpers/pages';
import { get, save } from '../../../helpers/storage';
import { isDefined } from '../../../helpers/types';
import { AppState } from '../../../types/appstate';
@@ -94,8 +93,6 @@ export class AllProjects extends React.PureComponent<Props, State> {
}

this.handleQueryChange();

addSideBarClass();
}

componentDidUpdate(prevProps: Props) {
@@ -106,7 +103,6 @@ export class AllProjects extends React.PureComponent<Props, State> {

componentWillUnmount() {
this.mounted = false;
removeSideBarClass();
}

fetchMoreProjects = () => {

+ 0
- 165
server/sonar-web/src/main/js/apps/projects/styles.css View File

@@ -17,176 +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.
*/
.projects-page .layout-page-header-panel-inner,
.projects-page .layout-page-header-panel {
height: 98px;
line-height: normal;
}

.projects-topbar-item + .projects-topbar-item {
padding-left: 24px;
}

.projects-topbar-item.is-last {
margin-left: auto;
padding-left: 32px;
}

.projects-topbar-item-search {
position: relative;
flex: 1;
height: var(--controlHeight);
}

.projects-header-row {
padding-top: 2px;
}

.projects-list .page-actions {
margin-bottom: 0;
}

.project-card-name {
font-weight: 600;
}

.projects-leak-sorting-option.is-focused {
background-color: var(--leakSecondaryColor);
}

.projects-facet-list {
padding-left: 10px;
padding-right: 10px;
}

.projects-facets-header {
margin-bottom: 10px;
padding: 10px 0;
border-bottom: 1px solid var(--barBorderColor);
}

.projects-facets-reset {
float: right;
}

.projects-facet-bar {
display: inline-block;
width: 60px;
margin-left: 8px;
}

.projects-facet-bar-inner {
min-width: 1px;
height: 10px;
background-color: var(--gray60);
transition: width 0.3s ease;
}

.projects-empty-list {
padding: calc(4 * var(--gridSize)) 0;
text-align: center;
}

/***
Custom filter highlights.
Projects filters are special, as some elements allow the selection of "everything
worse than" filters (e.g., "Rating B or worse"). We still select a single element,
but we want to give a visual indication that we selected multiple fitlers.
That's where the following selectors come in, which extend and override styles
from ../../components/search-navigator.css
***/

/*
Completely remove the border of the child facet. Handle them at the parent
<li> level.
*/
.search-navigator-facet-worse-than-highlight .search-navigator-facet {
border: 0 !important;
}

.search-navigator-facet-worse-than-highlight {
padding: 1px 0;
border-width: 0 1px;
border-color: transparent;
border-style: solid;
box-sizing: border-box;
}

/*
When:
- Being hovered
- Or, being a sibling of something hovered
- Or, being active
- Or, being a sibling of something active
show the left and right borders.
*/
.search-navigator-facet-worse-than-highlight:hover,
.search-navigator-facet-worse-than-highlight:hover ~ .search-navigator-facet-worse-than-highlight,
.search-navigator-facet-worse-than-highlight.active,
.search-navigator-facet-worse-than-highlight.active ~ .search-navigator-facet-worse-than-highlight {
border-left-color: var(--blue);
border-right-color: var(--blue);
}

/*
When:
- Being hovered
- Or, being active
show the top border, and remove the top padding.
*/
.search-navigator-facet-worse-than-highlight:hover,
.search-navigator-facet-worse-than-highlight.active {
border-top: 1px solid var(--blue) !important;
border-top-left-radius: 2px;
border-top-right-radius: 2px;
padding-top: 0 !important;
}

/*
When:
- Being hovered AND the last element of the highlightable group
- Or, being the last element of the highlightable group AND a sibling of something hovered
- Or, being active AND the last element of the highlightable group
- Or, being the last element of the highlightable group AND a sibling of something active
show the bottom border, and remove the bottom padding.
*/
.search-navigator-facet-worse-than-highlight.last:hover,
.search-navigator-facet-worse-than-highlight:hover
~ .search-navigator-facet-worse-than-highlight.last,
.search-navigator-facet-worse-than-highlight.active.last,
.search-navigator-facet-worse-than-highlight.active
~ .search-navigator-facet-worse-than-highlight.last {
border-bottom: 1px solid var(--blue) !important;
border-bottom-left-radius: 2px;
border-bottom-right-radius: 2px;
padding-bottom: 0 !important;
}

/*
When:
- Being active
- Or, being a sibling of something active
show a light blue background color.
*/
.search-navigator-facet-worse-than-highlight.active,
.search-navigator-facet-worse-than-highlight.active ~ .search-navigator-facet-worse-than-highlight {
background-color: var(--veryLightBlue);
}

/*
When:
- Being hovered AND a sibling of something active
- Or, being a sibling of something hovered AND a sibling of something active
show a darker blue background color.
*/
.search-navigator-facet-worse-than-highlight.active
~ .search-navigator-facet-worse-than-highlight:hover,
.search-navigator-facet-worse-than-highlight.active
~ .search-navigator-facet-worse-than-highlight:hover
~ .search-navigator-facet-worse-than-highlight {
background-color: #a1cde8;
}

.project-filters-list {
/*
* On Firefox on Windows, the scrollbar hides the sidebar's content.

+ 4
- 20
server/sonar-web/src/main/js/apps/quality-gates/components/App.tsx View File

@@ -20,6 +20,7 @@
import { withTheme } from '@emotion/react';
import styled from '@emotion/styled';
import {
Card,
LAYOUT_FOOTER_HEIGHT,
LAYOUT_GLOBAL_NAV_HEIGHT,
LargeCenteredLayout,
@@ -35,12 +36,6 @@ import { useNavigate, useParams } from 'react-router-dom';
import Suggestions from '../../../components/embed-docs-modal/Suggestions';
import '../../../components/search-navigator.css';
import { translate, translateWithParameters } from '../../../helpers/l10n';
import {
addSideBarClass,
addWhitePageClass,
removeSideBarClass,
removeWhitePageClass,
} from '../../../helpers/pages';
import { getQualityGateUrl } from '../../../helpers/urls';
import { useQualityGatesQuery } from '../../../queries/quality-gates';
import { QualityGate } from '../../../types/types';
@@ -72,16 +67,6 @@ export default function App() {
[navigate],
);

useEffect(() => {
addWhitePageClass();
addSideBarClass();

return () => {
removeWhitePageClass();
removeSideBarClass();
};
}, []);

useEffect(() => {
if (!name) {
openDefault(qualityGates);
@@ -102,7 +87,7 @@ export default function App() {
<Suggestions suggestions="quality_gates" />

<StyledContentWrapper
className="sw-col-span-3 sw-px-4 sw-py-6 sw-border-y-0 sw-rounded-0"
className="sw-col-span-3 sw-px-4 sw-py-6 sw-border-y-0"
style={{
height: `calc(100vh - ${LAYOUT_GLOBAL_NAV_HEIGHT + LAYOUT_FOOTER_HEIGHT}px)`,
}}
@@ -120,9 +105,9 @@ export default function App() {
height: `calc(100vh - ${LAYOUT_GLOBAL_NAV_HEIGHT + LAYOUT_FOOTER_HEIGHT}px)`,
}}
>
<StyledContentWrapper className="sw-my-12">
<Card className="sw-my-12">
<Details qualityGateName={name} />
</StyledContentWrapper>
</Card>
</div>
)}
</div>
@@ -133,7 +118,6 @@ export default function App() {

const StyledContentWrapper = withTheme(styled.div`
box-sizing: border-box;
border-radius: 4px;
background-color: ${themeColor('filterbar')};
border: ${themeBorder('default', 'filterbarBorder')};
overflow-x: hidden;

+ 10
- 12
server/sonar-web/src/main/js/apps/quality-gates/components/Details.tsx View File

@@ -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 { Spinner } from 'design-system';
import { Spinner } from '@sonarsource/echoes-react';
import * as React from 'react';
import { Helmet } from 'react-helmet-async';
import { useQualityGateQuery } from '../../../queries/quality-gates';
@@ -32,16 +32,14 @@ export default function Details({ qualityGateName }: Readonly<Props>) {
const { data: qualityGate, isLoading, isFetching } = useQualityGateQuery(qualityGateName);

return (
<main className="layout-page-main">
<Spinner loading={isLoading}>
{qualityGate && (
<>
<Helmet defer={false} title={qualityGate.name} />
<DetailsHeader qualityGate={qualityGate} />
<DetailsContent qualityGate={qualityGate} isFetching={isFetching} />
</>
)}
</Spinner>
</main>
<Spinner wrapperClassName="sw-block sw-text-center" isLoading={isLoading}>
{qualityGate && (
<main>
<Helmet defer={false} title={qualityGate.name} />
<DetailsHeader qualityGate={qualityGate} />
<DetailsContent qualityGate={qualityGate} isFetching={isFetching} />
</main>
)}
</Spinner>
);
}

+ 0
- 1
server/sonar-web/src/main/js/apps/security-hotspots/SecurityHotspotsApp.tsx View File

@@ -46,7 +46,6 @@ import {
import { Component, Dict } from '../../types/types';
import { CurrentUser, isLoggedIn } from '../../types/users';
import SecurityHotspotsAppRenderer from './SecurityHotspotsAppRenderer';
import './styles.css';
import { SECURITY_STANDARDS, getLocations } from './utils';

const PAGE_SIZE = 500;

+ 0
- 1
server/sonar-web/src/main/js/apps/security-hotspots/SecurityHotspotsAppRenderer.tsx View File

@@ -48,7 +48,6 @@ import HotspotSidebarHeader from './components/HotspotSidebarHeader';
import HotspotSimpleList from './components/HotspotSimpleList';
import HotspotFilterByStatus from './components/HotspotStatusFilter';
import HotspotViewer from './components/HotspotViewer';
import './styles.css';

export interface SecurityHotspotsAppRendererProps {
branchLike?: BranchLike;

+ 0
- 5
server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotList.tsx View File

@@ -25,7 +25,6 @@ import * as React from 'react';
import { FormattedMessage } from 'react-intl';
import ListFooter from '../../../components/controls/ListFooter';
import { translate } from '../../../helpers/l10n';
import { removeSideBarClass } from '../../../helpers/pages';
import { HotspotStatusFilter, RawHotspot } from '../../../types/security-hotspots';
import { Dict, StandardSecurityCategories } from '../../../types/types';
import { RISK_EXPOSURE_LEVELS, groupByCategory } from '../utils';
@@ -92,10 +91,6 @@ export default class HotspotList extends React.Component<Props, State> {
}
}

componentWillUnmount() {
removeSideBarClass();
}

groupHotspots = (hotspots: RawHotspot[], securityCategories: StandardSecurityCategories) => {
const risks = groupBy(hotspots, (h) => h.vulnerabilityProbability);


+ 0
- 62
server/sonar-web/src/main/js/apps/security-hotspots/styles.css View File

@@ -1,62 +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.
*/
#security_hotspots .filter-bar-outer {
height: 62px;
}

#security_hotspots .filter-bar {
position: fixed;
background-color: var(--barBackgroundColor);
z-index: var(--pageHeaderZIndex);
left: 0;
right: 0;
}

#security_hotspots .filter-bar-inner {
max-width: 1280px;
margin: 0 auto;
padding: calc(2 * var(--gridSize)) var(--gridSize);
box-sizing: border-box;
border-bottom: 1px solid var(--barBorderColor);
}

#security_hotspots .layout-page-side,
#security_hotspots .layout-page-side-outer {
width: calc(50vw - 330px);
}

#security_hotspots .layout-page-side-inner {
margin-left: calc(50vw - 645px);
}

#security_hotspots .layout-page-main {
padding: 0;
}

@media (max-width: 1320px) {
#security_hotspots .layout-page-side-outer,
#security_hotspots .layout-page-side {
width: 316px;
}

#security_hotspots .layout-page-side-inner {
margin-left: 0;
}
}

+ 0
- 10
server/sonar-web/src/main/js/apps/settings/components/SettingsApp.tsx View File

@@ -20,12 +20,6 @@
import * as React from 'react';
import { getDefinitions } from '../../../api/settings';
import withComponentContext from '../../../app/components/componentContext/withComponentContext';
import {
addSideBarClass,
addWhitePageClass,
removeSideBarClass,
removeWhitePageClass,
} from '../../../helpers/pages';
import { ExtendedSettingDefinition } from '../../../types/settings';
import { Component } from '../../../types/types';
import '../styles.css';
@@ -46,8 +40,6 @@ class SettingsApp extends React.PureComponent<Props, State> {

componentDidMount() {
this.mounted = true;
addSideBarClass();
addWhitePageClass();
this.fetchSettings();
}

@@ -59,8 +51,6 @@ class SettingsApp extends React.PureComponent<Props, State> {

componentWillUnmount() {
this.mounted = false;
removeSideBarClass();
removeWhitePageClass();
}

fetchSettings = async () => {

+ 1
- 169
server/sonar-web/src/main/js/apps/settings/styles.css View File

@@ -17,49 +17,6 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
#settings-page .layout-page-side,
#settings-page .layout-page-side-outer {
width: calc(50vw - 480px);
border-right: none;
}

#settings-page .layout-page-side-inner {
width: 160px;
margin-left: calc(50vw - 639px); /* 640px -1px for overlapping the border */
}

#settings-page .layout-page-main {
padding: 0;
}

#settings-page .layout-page-main-inner {
max-width: 1110px;
}

#settings-page .top-bar-outer {
height: 120px;
}

#settings-page .top-bar {
background-color: #f3f3f3;
position: fixed;
z-index: 55; /* todo */
left: 0;
right: 0;
}

#settings-page .top-bar-inner {
max-width: 1280px;
margin: 0 auto;
height: 120px;
box-sizing: border-box;
}

#settings-page .page-title,
#settings-page .page-description {
float: none;
}

.settings-definitions-list > li + li {
margin-top: 30px;
}
@@ -73,65 +30,23 @@
align-items: stretch;
}

.tabbed-definitions .settings-definition {
margin: 0 -16px;
padding: 10px 16px;
}

.settings-definition-changed {
border-top: 1px solid var(--alertBorderWarning);
border-bottom: 1px solid var(--alertBorderWarning);
background-color: var(--alertBackgroundWarning);
}

.settings-definition-left {
width: 330px;
padding-right: 30px;
box-sizing: border-box;
}

.radio-card .settings-definition-left {
padding-right: 0;
}

.settings-definition-right {
position: relative;
width: calc(100% - 330px);
box-sizing: border-box;
}

.radio-card .settings-definition-right input {
width: 100%;
}

.settings-definition-name {
text-overflow: ellipsis;
}

.settings-definition-key {
line-height: 1.5;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}

.settings-definition-state {
min-height: 32px;
padding-bottom: 4px;
}

.settings-definition-state > span {
display: flex;
}

.settings-definition-changes {
margin-top: 20px;
padding-top: 20px;
border-top: 1px dotted var(--barBorderColor);
}

.settings-sub-categories-list > li + li,
.settings-sub-category {
.settings-sub-categories-list > li + li {
margin: 30px -20px 0;
padding: 30px 20px;
border-top: 1px solid var(--barBorderColor);
@@ -142,91 +57,8 @@
font-size: var(--bigFontSize);
}

.settings-sub-category-description {
margin-top: -15px;
margin-bottom: 20px;
color: var(--secondFontColor);
}

.settings-large-input {
width: 100% !important;
max-width: 400px;
min-width: 200px;
}

.side-tabs-menu {
margin-top: calc(2 * var(--gridSize));
}

.side-tabs-menu > li {
margin-bottom: 4px;
}

.side-tabs-menu > li > a {
display: block;
padding: 10px 10px;
line-height: 1.5;
border-top-left-radius: 3px;
border-bottom-left-radius: 3px;
border: 1px solid var(--barBorderColor);
border-right: none;
overflow: hidden;
text-overflow: ellipsis;
transition:
color 0.3s ease,
background-color 0.3s ease;
}

.side-tabs-menu > li > a:hover,
.side-tabs-menu > li > a:focus,
.side-tabs-menu > li > a.active {
background-color: #fff;
}

.side-tabs-menu > li > a.active {
color: var(--baseFontColor);
cursor: default;
}

@media (max-width: 1320px) {
#settings-page .layout-page-side-outer,
#settings-page .layout-page-side {
width: 180px;
}

#settings-page .layout-page-side-inner {
margin-left: 20px;
}

#settings-page .top-bar-inner {
margin: 0 20px;
}
}

.settings-search-results {
max-height: 50vh;
width: 500px;
overflow-y: auto;
overflow-x: hidden;
}

.settings-search-results > li > a:hover {
background-color: unset;
border-left-color: unset;
}

.settings-search-results > li.active > a {
background-color: var(--neutral50);
border-left-color: var(--blacka60);
}

.fixed-footer {
position: sticky;
bottom: 0px;
align-items: center;
display: flex;
border: 1px solid var(--gray80);
background-color: white;
justify-content: space-between;
margin: 0px -16px;
}

+ 0
- 5
server/sonar-web/src/main/js/apps/web-api/__tests__/WebApi-it.tsx View File

@@ -27,11 +27,6 @@ jest.mock('../../../components/common/ScreenPositionHelper');

const webApiHandler = new WebApiServiceMock();

jest.mock('../../../helpers/pages', () => ({
addSideBarClass: jest.fn(),
removeSideBarClass: jest.fn(),
}));

beforeAll(() => {
webApiHandler.reset();
});

+ 0
- 3
server/sonar-web/src/main/js/apps/web-api/components/WebApiApp.tsx View File

@@ -34,7 +34,6 @@ import A11ySkipTarget from '../../../components/a11y/A11ySkipTarget';
import Suggestions from '../../../components/embed-docs-modal/Suggestions';
import { Location, Router, withRouter } from '../../../components/hoc/withRouter';
import { translate } from '../../../helpers/l10n';
import { addSideBarClass, removeSideBarClass } from '../../../helpers/pages';
import { WebApi } from '../../../types/types';
import '../styles/web-api.css';
import {
@@ -66,7 +65,6 @@ export class WebApiApp extends React.PureComponent<Props, State> {
componentDidMount() {
this.mounted = true;
this.fetchList();
addSideBarClass();
}

componentDidUpdate() {
@@ -76,7 +74,6 @@ export class WebApiApp extends React.PureComponent<Props, State> {

componentWillUnmount() {
this.mounted = false;
removeSideBarClass();
}

fetchList() {

+ 3
- 7
server/sonar-web/src/main/js/components/common/VisibilitySelector.tsx View File

@@ -18,7 +18,7 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import classNames from 'classnames';
import { Note, RadioButton } from 'design-system';
import { RadioButton } from 'design-system';
import * as React from 'react';
import { translate } from '../../helpers/l10n';
import { Visibility } from '../../types/component';
@@ -27,14 +27,13 @@ export interface VisibilitySelectorProps {
canTurnToPrivate?: boolean;
className?: string;
onChange: (visibility: Visibility) => void;
showDetails?: boolean;
visibility?: Visibility;
disabled?: boolean;
loading?: boolean;
}

export default function VisibilitySelector(props: VisibilitySelectorProps) {
const { className, canTurnToPrivate, visibility, showDetails, disabled, loading = false } = props;
const { className, canTurnToPrivate, visibility, disabled, loading = false } = props;
return (
<div className={classNames(className)}>
{Object.values(Visibility).map((v) => (
@@ -46,10 +45,7 @@ export default function VisibilitySelector(props: VisibilitySelectorProps) {
onCheck={props.onChange}
disabled={disabled || (v === Visibility.Private && !canTurnToPrivate) || loading}
>
<div>
{translate('visibility', v)}
{showDetails && <Note as="p">{translate('visibility', v, 'description.long')}</Note>}
</div>
<div>{translate('visibility', v)}</div>
</RadioButton>
))}
</div>

+ 0
- 47
server/sonar-web/src/main/js/helpers/__tests__/pages-test.ts View File

@@ -1,47 +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 {
addSideBarClass,
addWhitePageClass,
removeSideBarClass,
removeWhitePageClass,
} from '../pages';

describe('class adders', () => {
it.each([
[addSideBarClass, 'sidebar-page'],
[addWhitePageClass, 'white-page'],
])('%s should add the class', (fct, cls) => {
const toggle = jest.spyOn(document.body.classList, 'toggle');
fct();
expect(toggle).toHaveBeenCalledWith(cls, true);
});
});

describe('class removers', () => {
it.each([
[removeSideBarClass, 'sidebar-page'],
[removeWhitePageClass, 'white-page'],
])('%s should add the class', (fct, cls) => {
const toggle = jest.spyOn(document.body.classList, 'toggle');
fct();
expect(toggle).toHaveBeenCalledWith(cls, false);
});
});

+ 0
- 44
server/sonar-web/src/main/js/helpers/pages.ts View File

@@ -1,44 +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.
*/
const CLASS_SIDEBAR_PAGE = 'sidebar-page';
const CLASS_WHITE_PAGE = 'white-page';

export function addSideBarClass() {
toggleBodyClass(CLASS_SIDEBAR_PAGE, true);
}

export function addWhitePageClass() {
toggleBodyClass(CLASS_WHITE_PAGE, true);
}

export function removeSideBarClass() {
toggleBodyClass(CLASS_SIDEBAR_PAGE, false);
}

export function removeWhitePageClass() {
toggleBodyClass(CLASS_WHITE_PAGE, false);
}

function toggleBodyClass(className: string, force: boolean) {
document.body.classList.toggle(className, force);
if (document.documentElement) {
document.documentElement.classList.toggle(className, force);
}
}

Loading…
Cancel
Save