mirror of
https://github.com/lidarr/lidarr.git
synced 2025-08-20 13:33:34 -07:00
New: Make monitored flags clickable
This commit is contained in:
parent
1bc52d0138
commit
a35e773b78
12 changed files with 106 additions and 54 deletions
|
@ -25,10 +25,6 @@ import AlbumStudioFooter from './AlbumStudioFooter';
|
||||||
import styles from './AlbumStudio.css';
|
import styles from './AlbumStudio.css';
|
||||||
|
|
||||||
const columns = [
|
const columns = [
|
||||||
{
|
|
||||||
name: 'monitored',
|
|
||||||
isVisible: true
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: 'status',
|
name: 'status',
|
||||||
isVisible: true
|
isVisible: true
|
||||||
|
@ -94,15 +90,15 @@ class AlbumStudio extends Component {
|
||||||
|
|
||||||
// nasty hack to fix react-virtualized jumping incorrectly
|
// nasty hack to fix react-virtualized jumping incorrectly
|
||||||
// due to variable row heights
|
// due to variable row heights
|
||||||
if (scrollIndex != null) {
|
if (scrollIndex != null && scrollIndex > 0) {
|
||||||
if (jumpCount === 0) {
|
if (jumpCount === 0) {
|
||||||
this.setState({
|
this.setState({
|
||||||
scrollIndex: scrollIndex + 1,
|
scrollIndex: scrollIndex - 1,
|
||||||
jumpCount: 1
|
jumpCount: 1
|
||||||
});
|
});
|
||||||
} else if (jumpCount === 1) {
|
} else if (jumpCount === 1) {
|
||||||
this.setState({
|
this.setState({
|
||||||
scrollIndex: scrollIndex - 1,
|
scrollIndex: scrollIndex + 1,
|
||||||
jumpCount: 2
|
jumpCount: 2
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -12,18 +12,13 @@
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.status,
|
.status {
|
||||||
.monitored {
|
|
||||||
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||||
|
|
||||||
padding: 0;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
width: 20px;
|
padding: 0;
|
||||||
}
|
min-width: 60px;
|
||||||
|
|
||||||
.statusIcon {
|
|
||||||
width: 20px !important;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
|
|
|
@ -1,10 +1,8 @@
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import { icons } from 'Helpers/Props';
|
|
||||||
import Icon from 'Components/Icon';
|
|
||||||
import MonitorToggleButton from 'Components/MonitorToggleButton';
|
|
||||||
import VirtualTableRowCell from 'Components/Table/Cells/VirtualTableRowCell';
|
import VirtualTableRowCell from 'Components/Table/Cells/VirtualTableRowCell';
|
||||||
import VirtualTableSelectCell from 'Components/Table/Cells/VirtualTableSelectCell';
|
import VirtualTableSelectCell from 'Components/Table/Cells/VirtualTableSelectCell';
|
||||||
|
import ArtistStatusCell from 'Artist/Index/Table/ArtistStatusCell';
|
||||||
import ArtistNameLink from 'Artist/ArtistNameLink';
|
import ArtistNameLink from 'Artist/ArtistNameLink';
|
||||||
import AlbumStudioAlbum from './AlbumStudioAlbum';
|
import AlbumStudioAlbum from './AlbumStudioAlbum';
|
||||||
import styles from './AlbumStudioRow.css';
|
import styles from './AlbumStudioRow.css';
|
||||||
|
@ -20,6 +18,7 @@ class AlbumStudioRow extends Component {
|
||||||
status,
|
status,
|
||||||
foreignArtistId,
|
foreignArtistId,
|
||||||
artistName,
|
artistName,
|
||||||
|
artistType,
|
||||||
monitored,
|
monitored,
|
||||||
albums,
|
albums,
|
||||||
isSaving,
|
isSaving,
|
||||||
|
@ -39,22 +38,15 @@ class AlbumStudioRow extends Component {
|
||||||
isDisabled={false}
|
isDisabled={false}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<VirtualTableRowCell className={styles.monitored}>
|
<ArtistStatusCell
|
||||||
<MonitorToggleButton
|
className={styles.status}
|
||||||
|
artistType={artistType}
|
||||||
monitored={monitored}
|
monitored={monitored}
|
||||||
size={14}
|
status={status}
|
||||||
isSaving={isSaving}
|
isSaving={isSaving}
|
||||||
onPress={onArtistMonitoredPress}
|
onMonitoredPress={onArtistMonitoredPress}
|
||||||
|
component={VirtualTableRowCell}
|
||||||
/>
|
/>
|
||||||
</VirtualTableRowCell>
|
|
||||||
|
|
||||||
<VirtualTableRowCell className={styles.status}>
|
|
||||||
<Icon
|
|
||||||
className={styles.statusIcon}
|
|
||||||
name={status === 'ended' ? icons.ARTIST_ENDED : icons.ARTIST_CONTINUING}
|
|
||||||
title={status === 'ended' ? 'Ended' : 'Continuing'}
|
|
||||||
/>
|
|
||||||
</VirtualTableRowCell>
|
|
||||||
|
|
||||||
<VirtualTableRowCell className={styles.title}>
|
<VirtualTableRowCell className={styles.title}>
|
||||||
<ArtistNameLink
|
<ArtistNameLink
|
||||||
|
@ -86,6 +78,7 @@ AlbumStudioRow.propTypes = {
|
||||||
status: PropTypes.string.isRequired,
|
status: PropTypes.string.isRequired,
|
||||||
foreignArtistId: PropTypes.string.isRequired,
|
foreignArtistId: PropTypes.string.isRequired,
|
||||||
artistName: PropTypes.string.isRequired,
|
artistName: PropTypes.string.isRequired,
|
||||||
|
artistType: PropTypes.string,
|
||||||
monitored: PropTypes.bool.isRequired,
|
monitored: PropTypes.bool.isRequired,
|
||||||
albums: PropTypes.arrayOf(PropTypes.object).isRequired,
|
albums: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
isSaving: PropTypes.bool.isRequired,
|
isSaving: PropTypes.bool.isRequired,
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
.monitored,
|
|
||||||
.status {
|
.status {
|
||||||
composes: headerCell from '~Components/Table/VirtualTableHeaderCell.css';
|
composes: headerCell from '~Components/Table/VirtualTableHeaderCell.css';
|
||||||
|
|
||||||
width: 20px;
|
flex: 0 0 60px;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -234,6 +234,7 @@ class ArtistEditor extends Component {
|
||||||
key={item.id}
|
key={item.id}
|
||||||
{...item}
|
{...item}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
|
isSaving={isSaving}
|
||||||
isSelected={selectedState[item.id]}
|
isSelected={selectedState[item.id]}
|
||||||
onSelectedChange={this.onSelectedChange}
|
onSelectedChange={this.onSelectedChange}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -37,7 +37,9 @@ class ArtistEditorRow extends Component {
|
||||||
path,
|
path,
|
||||||
tags,
|
tags,
|
||||||
columns,
|
columns,
|
||||||
|
isSaving,
|
||||||
isSelected,
|
isSelected,
|
||||||
|
onArtistMonitoredPress,
|
||||||
onSelectedChange
|
onSelectedChange
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
|
@ -53,6 +55,8 @@ class ArtistEditorRow extends Component {
|
||||||
artistType={artistType}
|
artistType={artistType}
|
||||||
monitored={monitored}
|
monitored={monitored}
|
||||||
status={status}
|
status={status}
|
||||||
|
isSaving={isSaving}
|
||||||
|
onMonitoredPress={onArtistMonitoredPress}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TableRowCell className={styles.title}>
|
<TableRowCell className={styles.title}>
|
||||||
|
@ -109,7 +113,9 @@ ArtistEditorRow.propTypes = {
|
||||||
path: PropTypes.string.isRequired,
|
path: PropTypes.string.isRequired,
|
||||||
tags: PropTypes.arrayOf(PropTypes.number).isRequired,
|
tags: PropTypes.arrayOf(PropTypes.number).isRequired,
|
||||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
isSaving: PropTypes.bool.isRequired,
|
||||||
isSelected: PropTypes.bool,
|
isSelected: PropTypes.bool,
|
||||||
|
onArtistMonitoredPress: PropTypes.func.isRequired,
|
||||||
onSelectedChange: PropTypes.func.isRequired
|
onSelectedChange: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React from 'react';
|
import React, { Component } from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import createMetadataProfileSelector from 'Store/Selectors/createMetadataProfileSelector';
|
import createMetadataProfileSelector from 'Store/Selectors/createMetadataProfileSelector';
|
||||||
import createQualityProfileSelector from 'Store/Selectors/createQualityProfileSelector';
|
import createQualityProfileSelector from 'Store/Selectors/createQualityProfileSelector';
|
||||||
|
import { toggleArtistMonitored } from 'Store/Actions/artistActions';
|
||||||
import ArtistEditorRow from './ArtistEditorRow';
|
import ArtistEditorRow from './ArtistEditorRow';
|
||||||
|
|
||||||
function createMapStateToProps() {
|
function createMapStateToProps() {
|
||||||
|
@ -19,16 +20,42 @@ function createMapStateToProps() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ArtistEditorRowConnector(props) {
|
const mapDispatchToProps = {
|
||||||
|
toggleArtistMonitored
|
||||||
|
};
|
||||||
|
|
||||||
|
class ArtistEditorRowConnector extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onArtistMonitoredPress = () => {
|
||||||
|
const {
|
||||||
|
id,
|
||||||
|
monitored
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
this.props.toggleArtistMonitored({
|
||||||
|
artistId: id,
|
||||||
|
monitored: !monitored
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
return (
|
return (
|
||||||
<ArtistEditorRow
|
<ArtistEditorRow
|
||||||
{...props}
|
{...this.props}
|
||||||
|
onArtistMonitoredPress={this.onArtistMonitoredPress}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ArtistEditorRowConnector.propTypes = {
|
ArtistEditorRowConnector.propTypes = {
|
||||||
qualityProfileId: PropTypes.number.isRequired
|
id: PropTypes.number.isRequired,
|
||||||
|
monitored: PropTypes.bool.isRequired,
|
||||||
|
qualityProfileId: PropTypes.number.isRequired,
|
||||||
|
toggleArtistMonitored: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
export default connect(createMapStateToProps)(ArtistEditorRowConnector);
|
export default connect(createMapStateToProps, mapDispatchToProps)(ArtistEditorRowConnector);
|
||||||
|
|
|
@ -8,6 +8,7 @@ import createArtistSelector from 'Store/Selectors/createArtistSelector';
|
||||||
import createExecutingCommandsSelector from 'Store/Selectors/createExecutingCommandsSelector';
|
import createExecutingCommandsSelector from 'Store/Selectors/createExecutingCommandsSelector';
|
||||||
import createArtistQualityProfileSelector from 'Store/Selectors/createArtistQualityProfileSelector';
|
import createArtistQualityProfileSelector from 'Store/Selectors/createArtistQualityProfileSelector';
|
||||||
import createArtistMetadataProfileSelector from 'Store/Selectors/createArtistMetadataProfileSelector';
|
import createArtistMetadataProfileSelector from 'Store/Selectors/createArtistMetadataProfileSelector';
|
||||||
|
import { toggleArtistMonitored } from 'Store/Actions/artistActions';
|
||||||
import { executeCommand } from 'Store/Actions/commandActions';
|
import { executeCommand } from 'Store/Actions/commandActions';
|
||||||
import * as commandNames from 'Commands/commandNames';
|
import * as commandNames from 'Commands/commandNames';
|
||||||
|
|
||||||
|
@ -85,7 +86,8 @@ function createMapStateToProps() {
|
||||||
}
|
}
|
||||||
|
|
||||||
const mapDispatchToProps = {
|
const mapDispatchToProps = {
|
||||||
dispatchExecuteCommand: executeCommand
|
dispatchExecuteCommand: executeCommand,
|
||||||
|
toggleArtistMonitored
|
||||||
};
|
};
|
||||||
|
|
||||||
class ArtistIndexItemConnector extends Component {
|
class ArtistIndexItemConnector extends Component {
|
||||||
|
@ -107,6 +109,13 @@ class ArtistIndexItemConnector extends Component {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onMonitoredPress = () => {
|
||||||
|
this.props.toggleArtistMonitored({
|
||||||
|
artistId: this.props.id,
|
||||||
|
monitored: !this.props.monitored
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
// Render
|
// Render
|
||||||
|
|
||||||
|
@ -127,6 +136,7 @@ class ArtistIndexItemConnector extends Component {
|
||||||
id={id}
|
id={id}
|
||||||
onRefreshArtistPress={this.onRefreshArtistPress}
|
onRefreshArtistPress={this.onRefreshArtistPress}
|
||||||
onSearchPress={this.onSearchPress}
|
onSearchPress={this.onSearchPress}
|
||||||
|
onMonitoredPress={this.onMonitoredPress}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -134,8 +144,10 @@ class ArtistIndexItemConnector extends Component {
|
||||||
|
|
||||||
ArtistIndexItemConnector.propTypes = {
|
ArtistIndexItemConnector.propTypes = {
|
||||||
id: PropTypes.number,
|
id: PropTypes.number,
|
||||||
|
monitored: PropTypes.bool.isRequired,
|
||||||
component: PropTypes.elementType.isRequired,
|
component: PropTypes.elementType.isRequired,
|
||||||
dispatchExecuteCommand: PropTypes.func.isRequired
|
dispatchExecuteCommand: PropTypes.func.isRequired,
|
||||||
|
toggleArtistMonitored: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
export default connect(createMapStateToProps, mapDispatchToProps)(ArtistIndexItemConnector);
|
export default connect(createMapStateToProps, mapDispatchToProps)(ArtistIndexItemConnector);
|
||||||
|
|
|
@ -94,13 +94,15 @@ class ArtistIndexRow extends Component {
|
||||||
path,
|
path,
|
||||||
tags,
|
tags,
|
||||||
images,
|
images,
|
||||||
|
isSaving,
|
||||||
showBanners,
|
showBanners,
|
||||||
showSearchAction,
|
showSearchAction,
|
||||||
columns,
|
columns,
|
||||||
isRefreshingArtist,
|
isRefreshingArtist,
|
||||||
isSearchingArtist,
|
isSearchingArtist,
|
||||||
onRefreshArtistPress,
|
onRefreshArtistPress,
|
||||||
onSearchPress
|
onSearchPress,
|
||||||
|
onMonitoredPress
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
@ -138,6 +140,8 @@ class ArtistIndexRow extends Component {
|
||||||
artistType={artistType}
|
artistType={artistType}
|
||||||
monitored={monitored}
|
monitored={monitored}
|
||||||
status={status}
|
status={status}
|
||||||
|
isSaving={isSaving}
|
||||||
|
onMonitoredPress={onMonitoredPress}
|
||||||
component={VirtualTableRowCell}
|
component={VirtualTableRowCell}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
@ -457,13 +461,15 @@ ArtistIndexRow.propTypes = {
|
||||||
ratings: PropTypes.object.isRequired,
|
ratings: PropTypes.object.isRequired,
|
||||||
tags: PropTypes.arrayOf(PropTypes.number).isRequired,
|
tags: PropTypes.arrayOf(PropTypes.number).isRequired,
|
||||||
images: PropTypes.arrayOf(PropTypes.object).isRequired,
|
images: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
isSaving: PropTypes.bool.isRequired,
|
||||||
showBanners: PropTypes.bool.isRequired,
|
showBanners: PropTypes.bool.isRequired,
|
||||||
showSearchAction: PropTypes.bool.isRequired,
|
showSearchAction: PropTypes.bool.isRequired,
|
||||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
isRefreshingArtist: PropTypes.bool.isRequired,
|
isRefreshingArtist: PropTypes.bool.isRequired,
|
||||||
isSearchingArtist: PropTypes.bool.isRequired,
|
isSearchingArtist: PropTypes.bool.isRequired,
|
||||||
onRefreshArtistPress: PropTypes.func.isRequired,
|
onRefreshArtistPress: PropTypes.func.isRequired,
|
||||||
onSearchPress: PropTypes.func.isRequired
|
onSearchPress: PropTypes.func.isRequired,
|
||||||
|
onMonitoredPress: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
ArtistIndexRow.defaultProps = {
|
ArtistIndexRow.defaultProps = {
|
||||||
|
|
|
@ -47,7 +47,8 @@ class ArtistIndexTable extends Component {
|
||||||
const {
|
const {
|
||||||
items,
|
items,
|
||||||
columns,
|
columns,
|
||||||
showBanners
|
showBanners,
|
||||||
|
isSaving
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const artist = items[rowIndex];
|
const artist = items[rowIndex];
|
||||||
|
@ -66,6 +67,7 @@ class ArtistIndexTable extends Component {
|
||||||
qualityProfileId={artist.qualityProfileId}
|
qualityProfileId={artist.qualityProfileId}
|
||||||
metadataProfileId={artist.metadataProfileId}
|
metadataProfileId={artist.metadataProfileId}
|
||||||
showBanners={showBanners}
|
showBanners={showBanners}
|
||||||
|
isSaving={isSaving}
|
||||||
/>
|
/>
|
||||||
</VirtualTableRow>
|
</VirtualTableRow>
|
||||||
);
|
);
|
||||||
|
@ -121,6 +123,7 @@ ArtistIndexTable.propTypes = {
|
||||||
sortKey: PropTypes.string,
|
sortKey: PropTypes.string,
|
||||||
sortDirection: PropTypes.oneOf(sortDirections.all),
|
sortDirection: PropTypes.oneOf(sortDirections.all),
|
||||||
showBanners: PropTypes.bool.isRequired,
|
showBanners: PropTypes.bool.isRequired,
|
||||||
|
isSaving: PropTypes.bool.isRequired,
|
||||||
jumpToCharacter: PropTypes.string,
|
jumpToCharacter: PropTypes.string,
|
||||||
scrollTop: PropTypes.number,
|
scrollTop: PropTypes.number,
|
||||||
scroller: PropTypes.instanceOf(Element).isRequired,
|
scroller: PropTypes.instanceOf(Element).isRequired,
|
||||||
|
|
|
@ -4,6 +4,13 @@
|
||||||
width: 60px;
|
width: 60px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.monitorToggle {
|
||||||
|
composes: toggleButton from '~Components/MonitorToggleButton.css';
|
||||||
|
|
||||||
|
margin: 0;
|
||||||
|
width: 20px !important;
|
||||||
|
}
|
||||||
|
|
||||||
.statusIcon {
|
.statusIcon {
|
||||||
width: 20px !important;
|
width: 20px !important;
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@ import PropTypes from 'prop-types';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { icons } from 'Helpers/Props';
|
import { icons } from 'Helpers/Props';
|
||||||
import Icon from 'Components/Icon';
|
import Icon from 'Components/Icon';
|
||||||
|
import MonitorToggleButton from 'Components/MonitorToggleButton';
|
||||||
import VirtualTableRowCell from 'Components/Table/Cells/TableRowCell';
|
import VirtualTableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||||
import styles from './ArtistStatusCell.css';
|
import styles from './ArtistStatusCell.css';
|
||||||
|
|
||||||
|
@ -11,6 +12,8 @@ function ArtistStatusCell(props) {
|
||||||
artistType,
|
artistType,
|
||||||
monitored,
|
monitored,
|
||||||
status,
|
status,
|
||||||
|
isSaving,
|
||||||
|
onMonitoredPress,
|
||||||
component: Component,
|
component: Component,
|
||||||
...otherProps
|
...otherProps
|
||||||
} = props;
|
} = props;
|
||||||
|
@ -22,10 +25,12 @@ function ArtistStatusCell(props) {
|
||||||
className={className}
|
className={className}
|
||||||
{...otherProps}
|
{...otherProps}
|
||||||
>
|
>
|
||||||
<Icon
|
<MonitorToggleButton
|
||||||
className={styles.statusIcon}
|
className={styles.monitorToggle}
|
||||||
name={monitored ? icons.MONITORED : icons.UNMONITORED}
|
monitored={monitored}
|
||||||
title={monitored ? 'Artist is monitored' : 'Artist is unmonitored'}
|
size={14}
|
||||||
|
isSaving={isSaving}
|
||||||
|
onPress={onMonitoredPress}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Icon
|
<Icon
|
||||||
|
@ -42,6 +47,8 @@ ArtistStatusCell.propTypes = {
|
||||||
artistType: PropTypes.string,
|
artistType: PropTypes.string,
|
||||||
monitored: PropTypes.bool.isRequired,
|
monitored: PropTypes.bool.isRequired,
|
||||||
status: PropTypes.string.isRequired,
|
status: PropTypes.string.isRequired,
|
||||||
|
isSaving: PropTypes.bool.isRequired,
|
||||||
|
onMonitoredPress: PropTypes.func.isRequired,
|
||||||
component: PropTypes.elementType
|
component: PropTypes.elementType
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue