New: Release Profiles, Frontend updates (#580)
* New: Release Profiles - UI Updates * New: Release Profiles - API Changes * New: Release Profiles - Test Updates * New: Release Profiles - Backend Updates * New: Interactive Artist Search * New: Change Montiored on Album Details Page * New: Show Duration on Album Details Page * Fixed: Manual Import not working if no albums are Missing * Fixed: Sort search input by sortTitle * Fixed: Queue columnLabel throwing JS errorpull/646/head
parent
f126eafd26
commit
3f064c94b9
@ -0,0 +1,5 @@
|
|||||||
|
.description {
|
||||||
|
composes: title from 'Components/DescriptionList/DescriptionListItemDescription.css';
|
||||||
|
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
}
|
@ -0,0 +1,77 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component, Fragment } from 'react';
|
||||||
|
import { inputTypes } from 'Helpers/Props';
|
||||||
|
import FormGroup from 'Components/Form/FormGroup';
|
||||||
|
import FormLabel from 'Components/Form/FormLabel';
|
||||||
|
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||||
|
|
||||||
|
class QueueOptions extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Lifecycle
|
||||||
|
|
||||||
|
constructor(props, context) {
|
||||||
|
super(props, context);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
includeUnknownArtistItems: props.includeUnknownArtistItems
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate(prevProps) {
|
||||||
|
const {
|
||||||
|
includeUnknownArtistItems
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
if (includeUnknownArtistItems !== prevProps.includeUnknownArtistItems) {
|
||||||
|
this.setState({
|
||||||
|
includeUnknownArtistItems
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onOptionChange = ({ name, value }) => {
|
||||||
|
this.setState({
|
||||||
|
[name]: value
|
||||||
|
}, () => {
|
||||||
|
this.props.onOptionChange({
|
||||||
|
[name]: value
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
includeUnknownArtistItems
|
||||||
|
} = this.state;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Fragment>
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>Show Unknown Artist Items</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.CHECK}
|
||||||
|
name="includeUnknownArtistItems"
|
||||||
|
value={includeUnknownArtistItems}
|
||||||
|
helpText="Show items without a artist in the queue, this could include removed artists, movies or anything else in Lidarr's category"
|
||||||
|
onChange={this.onOptionChange}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QueueOptions.propTypes = {
|
||||||
|
includeUnknownArtistItems: PropTypes.bool.isRequired,
|
||||||
|
onOptionChange: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default QueueOptions;
|
@ -0,0 +1,19 @@
|
|||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import { setQueueOption } from 'Store/Actions/queueActions';
|
||||||
|
import QueueOptions from './QueueOptions';
|
||||||
|
|
||||||
|
function createMapStateToProps() {
|
||||||
|
return createSelector(
|
||||||
|
(state) => state.queue.options,
|
||||||
|
(options) => {
|
||||||
|
return options;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapDispatchToProps = {
|
||||||
|
onOptionChange: setQueueOption
|
||||||
|
};
|
||||||
|
|
||||||
|
export default connect(createMapStateToProps, mapDispatchToProps)(QueueOptions);
|
@ -1,48 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import { deleteRootFolder } from 'Store/Actions/rootFolderActions';
|
|
||||||
import ImportArtistRootFolderRow from './ImportArtistRootFolderRow';
|
|
||||||
|
|
||||||
function createMapStateToProps() {
|
|
||||||
return createSelector(
|
|
||||||
() => {
|
|
||||||
return {
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const mapDispatchToProps = {
|
|
||||||
deleteRootFolder
|
|
||||||
};
|
|
||||||
|
|
||||||
class ImportArtistRootFolderRowConnector extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onDeletePress = () => {
|
|
||||||
this.props.deleteRootFolder({ id: this.props.id });
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<ImportArtistRootFolderRow
|
|
||||||
{...this.props}
|
|
||||||
onDeletePress={this.onDeletePress}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ImportArtistRootFolderRowConnector.propTypes = {
|
|
||||||
id: PropTypes.number.isRequired,
|
|
||||||
deleteRootFolder: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default connect(createMapStateToProps, mapDispatchToProps)(ImportArtistRootFolderRowConnector);
|
|
@ -0,0 +1,36 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React from 'react';
|
||||||
|
import Modal from 'Components/Modal/Modal';
|
||||||
|
import AlbumInteractiveSearchModalContent from './AlbumInteractiveSearchModalContent';
|
||||||
|
|
||||||
|
function AlbumInteractiveSearchModal(props) {
|
||||||
|
const {
|
||||||
|
isOpen,
|
||||||
|
albumId,
|
||||||
|
albumTitle,
|
||||||
|
onModalClose
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
isOpen={isOpen}
|
||||||
|
closeOnBackgroundClick={false}
|
||||||
|
onModalClose={onModalClose}
|
||||||
|
>
|
||||||
|
<AlbumInteractiveSearchModalContent
|
||||||
|
albumId={albumId}
|
||||||
|
albumTitle={albumTitle}
|
||||||
|
onModalClose={onModalClose}
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
AlbumInteractiveSearchModal.propTypes = {
|
||||||
|
isOpen: PropTypes.bool.isRequired,
|
||||||
|
albumId: PropTypes.number.isRequired,
|
||||||
|
albumTitle: PropTypes.string.isRequired,
|
||||||
|
onModalClose: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AlbumInteractiveSearchModal;
|
@ -0,0 +1,15 @@
|
|||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { cancelFetchReleases, clearReleases } from 'Store/Actions/releaseActions';
|
||||||
|
import AlbumInteractiveSearchModal from './AlbumInteractiveSearchModal';
|
||||||
|
|
||||||
|
function createMapDispatchToProps(dispatch, props) {
|
||||||
|
return {
|
||||||
|
onModalClose() {
|
||||||
|
dispatch(cancelFetchReleases());
|
||||||
|
dispatch(clearReleases());
|
||||||
|
props.onModalClose();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(null, createMapDispatchToProps)(AlbumInteractiveSearchModal);
|
@ -0,0 +1,47 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React from 'react';
|
||||||
|
import Button from 'Components/Link/Button';
|
||||||
|
import ModalContent from 'Components/Modal/ModalContent';
|
||||||
|
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||||
|
import ModalBody from 'Components/Modal/ModalBody';
|
||||||
|
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||||
|
import InteractiveSearchConnector from 'InteractiveSearch/InteractiveSearchConnector';
|
||||||
|
|
||||||
|
function AlbumInteractiveSearchModalContent(props) {
|
||||||
|
const {
|
||||||
|
albumId,
|
||||||
|
albumTitle,
|
||||||
|
onModalClose
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ModalContent onModalClose={onModalClose}>
|
||||||
|
<ModalHeader>
|
||||||
|
Interactive Search {albumId != null && `- ${albumTitle}`}
|
||||||
|
</ModalHeader>
|
||||||
|
|
||||||
|
<ModalBody>
|
||||||
|
<InteractiveSearchConnector
|
||||||
|
type="album"
|
||||||
|
searchPayload={{
|
||||||
|
albumId
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</ModalBody>
|
||||||
|
|
||||||
|
<ModalFooter>
|
||||||
|
<Button onPress={onModalClose}>
|
||||||
|
Close
|
||||||
|
</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</ModalContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
AlbumInteractiveSearchModalContent.propTypes = {
|
||||||
|
albumId: PropTypes.number.isRequired,
|
||||||
|
albumTitle: PropTypes.string.isRequired,
|
||||||
|
onModalClose: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AlbumInteractiveSearchModalContent;
|
@ -0,0 +1,6 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
const ColorImpairedContext = React.createContext(false);
|
||||||
|
export const ColorImpairedConsumer = ColorImpairedContext.Consumer;
|
||||||
|
|
||||||
|
export default ColorImpairedContext;
|
@ -1,172 +1,25 @@
|
|||||||
import _ from 'lodash';
|
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React, { Component } from 'react';
|
import React from 'react';
|
||||||
import LazyLoad from 'react-lazyload';
|
import ArtistImage from './ArtistImage';
|
||||||
|
|
||||||
const bannerPlaceholder = 'data:image/png;base64,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';
|
const bannerPlaceholder = 'data:image/png;base64,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';
|
||||||
|
|
||||||
function findBanner(images) {
|
function ArtistBanner(props) {
|
||||||
return _.find(images, { coverType: 'banner' });
|
return (
|
||||||
}
|
<ArtistImage
|
||||||
|
{...props}
|
||||||
function getBannerUrl(banner, size) {
|
coverType="banner"
|
||||||
if (banner) {
|
placeholder={bannerPlaceholder}
|
||||||
if (banner.url.contains('lastWrite=') || (/^https?:/).test(banner.url)) {
|
/>
|
||||||
// Remove protocol
|
);
|
||||||
let url = banner.url.replace(/^https?:/, '');
|
|
||||||
url = url.replace('banner.jpg', `banner-${size}.jpg`);
|
|
||||||
|
|
||||||
return url;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class ArtistBanner extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
constructor(props, context) {
|
|
||||||
super(props, context);
|
|
||||||
|
|
||||||
const pixelRatio = Math.floor(window.devicePixelRatio);
|
|
||||||
|
|
||||||
const {
|
|
||||||
images,
|
|
||||||
size
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
const banner = findBanner(images);
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
pixelRatio,
|
|
||||||
banner,
|
|
||||||
bannerUrl: getBannerUrl(banner, pixelRatio * size),
|
|
||||||
isLoaded: false,
|
|
||||||
hasError: false
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
|
||||||
const {
|
|
||||||
images,
|
|
||||||
size
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const {
|
|
||||||
banner,
|
|
||||||
pixelRatio
|
|
||||||
} = this.state;
|
|
||||||
|
|
||||||
const nextBanner = findBanner(images);
|
|
||||||
|
|
||||||
if (nextBanner && (!banner || nextBanner.url !== banner.url)) {
|
|
||||||
this.setState({
|
|
||||||
banner: nextBanner,
|
|
||||||
bannerUrl: getBannerUrl(nextBanner, pixelRatio * size),
|
|
||||||
hasError: false,
|
|
||||||
isLoaded: true
|
|
||||||
});
|
|
||||||
} else if (!nextBanner && banner) {
|
|
||||||
this.setState({
|
|
||||||
banner: nextBanner,
|
|
||||||
bannerUrl: bannerPlaceholder,
|
|
||||||
hasError: false
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onError = () => {
|
|
||||||
this.setState({ hasError: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
onLoad = () => {
|
|
||||||
this.setState({
|
|
||||||
isLoaded: true,
|
|
||||||
hasError: false
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
className,
|
|
||||||
style,
|
|
||||||
size,
|
|
||||||
lazy,
|
|
||||||
overflow
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const {
|
|
||||||
bannerUrl,
|
|
||||||
hasError,
|
|
||||||
isLoaded
|
|
||||||
} = this.state;
|
|
||||||
|
|
||||||
if (hasError || !bannerUrl) {
|
|
||||||
return (
|
|
||||||
<img
|
|
||||||
className={className}
|
|
||||||
style={style}
|
|
||||||
src={bannerPlaceholder}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (lazy) {
|
|
||||||
return (
|
|
||||||
<LazyLoad
|
|
||||||
height={size}
|
|
||||||
offset={100}
|
|
||||||
overflow={overflow}
|
|
||||||
placeholder={
|
|
||||||
<img
|
|
||||||
className={className}
|
|
||||||
style={style}
|
|
||||||
src={bannerPlaceholder}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
className={className}
|
|
||||||
style={style}
|
|
||||||
src={bannerUrl}
|
|
||||||
onError={this.onError}
|
|
||||||
/>
|
|
||||||
</LazyLoad>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<img
|
|
||||||
className={className}
|
|
||||||
style={style}
|
|
||||||
src={isLoaded ? bannerUrl : bannerPlaceholder}
|
|
||||||
onError={this.onError}
|
|
||||||
onLoad={this.onLoad}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ArtistBanner.propTypes = {
|
ArtistBanner.propTypes = {
|
||||||
className: PropTypes.string,
|
size: PropTypes.number.isRequired
|
||||||
style: PropTypes.object,
|
|
||||||
images: PropTypes.arrayOf(PropTypes.object).isRequired,
|
|
||||||
size: PropTypes.number.isRequired,
|
|
||||||
lazy: PropTypes.bool.isRequired,
|
|
||||||
overflow: PropTypes.bool.isRequired
|
|
||||||
};
|
};
|
||||||
|
|
||||||
ArtistBanner.defaultProps = {
|
ArtistBanner.defaultProps = {
|
||||||
size: 70,
|
size: 70
|
||||||
lazy: true,
|
|
||||||
overflow: false
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ArtistBanner;
|
export default ArtistBanner;
|
||||||
|
@ -0,0 +1,199 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import LazyLoad from 'react-lazyload';
|
||||||
|
|
||||||
|
function findImage(images, coverType) {
|
||||||
|
return images.find((image) => image.coverType === coverType);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getUrl(image, coverType, size) {
|
||||||
|
if (image) {
|
||||||
|
// Remove protocol
|
||||||
|
let url = image.url.replace(/^https?:/, '');
|
||||||
|
url = url.replace(`${coverType}.jpg`, `${coverType}-${size}.jpg`);
|
||||||
|
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ArtistImage extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Lifecycle
|
||||||
|
|
||||||
|
constructor(props, context) {
|
||||||
|
super(props, context);
|
||||||
|
|
||||||
|
const pixelRatio = Math.floor(window.devicePixelRatio);
|
||||||
|
|
||||||
|
const {
|
||||||
|
images,
|
||||||
|
coverType,
|
||||||
|
size
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const image = findImage(images, coverType);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
pixelRatio,
|
||||||
|
image,
|
||||||
|
url: getUrl(image, coverType, pixelRatio * size),
|
||||||
|
isLoaded: false,
|
||||||
|
hasError: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
if (!this.state.url && this.props.onError) {
|
||||||
|
this.props.onError();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate() {
|
||||||
|
const {
|
||||||
|
images,
|
||||||
|
coverType,
|
||||||
|
placeholder,
|
||||||
|
size,
|
||||||
|
onError
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const {
|
||||||
|
image,
|
||||||
|
pixelRatio
|
||||||
|
} = this.state;
|
||||||
|
|
||||||
|
const nextImage = findImage(images, coverType);
|
||||||
|
|
||||||
|
if (nextImage && (!image || nextImage.url !== image.url)) {
|
||||||
|
this.setState({
|
||||||
|
image: nextImage,
|
||||||
|
url: getUrl(nextImage, coverType, pixelRatio * size),
|
||||||
|
hasError: false
|
||||||
|
// Don't reset isLoaded, as we want to immediately try to
|
||||||
|
// show the new image, whether an image was shown previously
|
||||||
|
// or the placeholder was shown.
|
||||||
|
});
|
||||||
|
} else if (!nextImage && image) {
|
||||||
|
this.setState({
|
||||||
|
image: nextImage,
|
||||||
|
url: placeholder,
|
||||||
|
hasError: false
|
||||||
|
});
|
||||||
|
|
||||||
|
if (onError) {
|
||||||
|
onError();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onError = () => {
|
||||||
|
this.setState({
|
||||||
|
hasError: true
|
||||||
|
});
|
||||||
|
|
||||||
|
if (this.props.onError) {
|
||||||
|
this.props.onError();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onLoad = () => {
|
||||||
|
this.setState({
|
||||||
|
isLoaded: true,
|
||||||
|
hasError: false
|
||||||
|
});
|
||||||
|
|
||||||
|
if (this.props.onLoad) {
|
||||||
|
this.props.onLoad();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
className,
|
||||||
|
style,
|
||||||
|
placeholder,
|
||||||
|
size,
|
||||||
|
lazy,
|
||||||
|
overflow
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const {
|
||||||
|
url,
|
||||||
|
hasError,
|
||||||
|
isLoaded
|
||||||
|
} = this.state;
|
||||||
|
|
||||||
|
if (hasError || !url) {
|
||||||
|
return (
|
||||||
|
<img
|
||||||
|
className={className}
|
||||||
|
style={style}
|
||||||
|
src={placeholder}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lazy) {
|
||||||
|
return (
|
||||||
|
<LazyLoad
|
||||||
|
height={size}
|
||||||
|
offset={100}
|
||||||
|
overflow={overflow}
|
||||||
|
placeholder={
|
||||||
|
<img
|
||||||
|
className={className}
|
||||||
|
style={style}
|
||||||
|
src={placeholder}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
className={className}
|
||||||
|
style={style}
|
||||||
|
src={url}
|
||||||
|
onError={this.onError}
|
||||||
|
onLoad={this.onLoad}
|
||||||
|
/>
|
||||||
|
</LazyLoad>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<img
|
||||||
|
className={className}
|
||||||
|
style={style}
|
||||||
|
src={isLoaded ? url : placeholder}
|
||||||
|
onError={this.onError}
|
||||||
|
onLoad={this.onLoad}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ArtistImage.propTypes = {
|
||||||
|
className: PropTypes.string,
|
||||||
|
style: PropTypes.object,
|
||||||
|
images: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
coverType: PropTypes.string.isRequired,
|
||||||
|
placeholder: PropTypes.string.isRequired,
|
||||||
|
size: PropTypes.number.isRequired,
|
||||||
|
lazy: PropTypes.bool.isRequired,
|
||||||
|
overflow: PropTypes.bool.isRequired,
|
||||||
|
onError: PropTypes.func,
|
||||||
|
onLoad: PropTypes.func
|
||||||
|
};
|
||||||
|
|
||||||
|
ArtistImage.defaultProps = {
|
||||||
|
size: 250,
|
||||||
|
lazy: true,
|
||||||
|
overflow: false
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ArtistImage;
|
@ -1,172 +1,25 @@
|
|||||||
import _ from 'lodash';
|
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React, { Component } from 'react';
|
import React from 'react';
|
||||||
import LazyLoad from 'react-lazyload';
|
import ArtistImage from './ArtistImage';
|
||||||
|
|
||||||
const posterPlaceholder = 'data:image/png;base64,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';
|
const posterPlaceholder = 'data:image/png;base64,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';
|
||||||
|
|
||||||
function findPoster(images) {
|
function ArtistPoster(props) {
|
||||||
return _.find(images, { coverType: 'poster' });
|
return (
|
||||||
}
|
<ArtistImage
|
||||||
|
{...props}
|
||||||
function getPosterUrl(poster, size) {
|
coverType="poster"
|
||||||
if (poster) {
|
placeholder={posterPlaceholder}
|
||||||
if (poster.url.contains('lastWrite=') || (/^https?:/).test(poster.url)) {
|
/>
|
||||||
// Remove protocol
|
);
|
||||||
let url = poster.url.replace(/^https?:/, '');
|
|
||||||
url = url.replace('poster.jpg', `poster-${size}.jpg`);
|
|
||||||
|
|
||||||
return url;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class ArtistPoster extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
constructor(props, context) {
|
|
||||||
super(props, context);
|
|
||||||
|
|
||||||
const pixelRatio = Math.floor(window.devicePixelRatio);
|
|
||||||
|
|
||||||
const {
|
|
||||||
images,
|
|
||||||
size
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
const poster = findPoster(images);
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
pixelRatio,
|
|
||||||
poster,
|
|
||||||
posterUrl: getPosterUrl(poster, pixelRatio * size),
|
|
||||||
isLoaded: false,
|
|
||||||
hasError: false
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
|
||||||
const {
|
|
||||||
images,
|
|
||||||
size
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const {
|
|
||||||
poster,
|
|
||||||
pixelRatio
|
|
||||||
} = this.state;
|
|
||||||
|
|
||||||
const nextPoster = findPoster(images);
|
|
||||||
|
|
||||||
if (nextPoster && (!poster || nextPoster.url !== poster.url)) {
|
|
||||||
this.setState({
|
|
||||||
poster: nextPoster,
|
|
||||||
posterUrl: getPosterUrl(nextPoster, pixelRatio * size),
|
|
||||||
hasError: false,
|
|
||||||
isLoaded: true
|
|
||||||
});
|
|
||||||
} else if (!nextPoster && poster) {
|
|
||||||
this.setState({
|
|
||||||
poster: nextPoster,
|
|
||||||
posterUrl: posterPlaceholder,
|
|
||||||
hasError: false
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onError = () => {
|
|
||||||
this.setState({ hasError: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
onLoad = () => {
|
|
||||||
this.setState({
|
|
||||||
isLoaded: true,
|
|
||||||
hasError: false
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
className,
|
|
||||||
style,
|
|
||||||
size,
|
|
||||||
lazy,
|
|
||||||
overflow
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const {
|
|
||||||
posterUrl,
|
|
||||||
hasError,
|
|
||||||
isLoaded
|
|
||||||
} = this.state;
|
|
||||||
|
|
||||||
if (hasError || !posterUrl) {
|
|
||||||
return (
|
|
||||||
<img
|
|
||||||
className={className}
|
|
||||||
style={style}
|
|
||||||
src={posterPlaceholder}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (lazy) {
|
|
||||||
return (
|
|
||||||
<LazyLoad
|
|
||||||
height={size}
|
|
||||||
offset={100}
|
|
||||||
overflow={overflow}
|
|
||||||
placeholder={
|
|
||||||
<img
|
|
||||||
className={className}
|
|
||||||
style={style}
|
|
||||||
src={posterPlaceholder}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
className={className}
|
|
||||||
style={style}
|
|
||||||
src={posterUrl}
|
|
||||||
onError={this.onError}
|
|
||||||
/>
|
|
||||||
</LazyLoad>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<img
|
|
||||||
className={className}
|
|
||||||
style={style}
|
|
||||||
src={isLoaded ? posterUrl : posterPlaceholder}
|
|
||||||
onError={this.onError}
|
|
||||||
onLoad={this.onLoad}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ArtistPoster.propTypes = {
|
ArtistPoster.propTypes = {
|
||||||
className: PropTypes.string,
|
size: PropTypes.number.isRequired
|
||||||
style: PropTypes.object,
|
|
||||||
images: PropTypes.arrayOf(PropTypes.object).isRequired,
|
|
||||||
size: PropTypes.number.isRequired,
|
|
||||||
lazy: PropTypes.bool.isRequired,
|
|
||||||
overflow: PropTypes.bool.isRequired
|
|
||||||
};
|
};
|
||||||
|
|
||||||
ArtistPoster.defaultProps = {
|
ArtistPoster.defaultProps = {
|
||||||
size: 250,
|
size: 250
|
||||||
lazy: true,
|
|
||||||
overflow: false
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ArtistPoster;
|
export default ArtistPoster;
|
||||||
|
@ -1,109 +1,86 @@
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React, { Component } from 'react';
|
import React from 'react';
|
||||||
|
import classNames from 'classnames';
|
||||||
import { icons } from 'Helpers/Props';
|
import { icons } from 'Helpers/Props';
|
||||||
import IconButton from 'Components/Link/IconButton';
|
import IconButton from 'Components/Link/IconButton';
|
||||||
import VirtualTableHeader from 'Components/Table/VirtualTableHeader';
|
import VirtualTableHeader from 'Components/Table/VirtualTableHeader';
|
||||||
import VirtualTableHeaderCell from 'Components/Table/VirtualTableHeaderCell';
|
import VirtualTableHeaderCell from 'Components/Table/VirtualTableHeaderCell';
|
||||||
import TableOptionsModal from 'Components/Table/TableOptions/TableOptionsModal';
|
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
|
||||||
|
import hasGrowableColumns from './hasGrowableColumns';
|
||||||
import ArtistIndexTableOptionsConnector from './ArtistIndexTableOptionsConnector';
|
import ArtistIndexTableOptionsConnector from './ArtistIndexTableOptionsConnector';
|
||||||
import styles from './ArtistIndexHeader.css';
|
import styles from './ArtistIndexHeader.css';
|
||||||
|
|
||||||
class ArtistIndexHeader extends Component {
|
function ArtistIndexHeader(props) {
|
||||||
|
const {
|
||||||
//
|
showBanners,
|
||||||
// Lifecycle
|
columns,
|
||||||
|
onTableOptionChange,
|
||||||
constructor(props, context) {
|
...otherProps
|
||||||
super(props, context);
|
} = props;
|
||||||
|
|
||||||
this.state = {
|
return (
|
||||||
isTableOptionsModalOpen: false
|
<VirtualTableHeader>
|
||||||
};
|
{
|
||||||
}
|
columns.map((column) => {
|
||||||
|
const {
|
||||||
//
|
name,
|
||||||
// Listeners
|
label,
|
||||||
|
isSortable,
|
||||||
onTableOptionsPress = () => {
|
isVisible
|
||||||
this.setState({ isTableOptionsModalOpen: true });
|
} = column;
|
||||||
}
|
|
||||||
|
if (!isVisible) {
|
||||||
onTableOptionsModalClose = () => {
|
return null;
|
||||||
this.setState({ isTableOptionsModalOpen: false });
|
}
|
||||||
}
|
|
||||||
|
if (name === 'actions') {
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
showSearchAction,
|
|
||||||
columns,
|
|
||||||
onTableOptionChange,
|
|
||||||
...otherProps
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<VirtualTableHeader>
|
|
||||||
{
|
|
||||||
columns.map((column) => {
|
|
||||||
const {
|
|
||||||
name,
|
|
||||||
label,
|
|
||||||
isSortable,
|
|
||||||
isVisible
|
|
||||||
} = column;
|
|
||||||
|
|
||||||
if (!isVisible) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'actions') {
|
|
||||||
return (
|
|
||||||
<VirtualTableHeaderCell
|
|
||||||
key={name}
|
|
||||||
className={styles[name]}
|
|
||||||
name={name}
|
|
||||||
isSortable={false}
|
|
||||||
{...otherProps}
|
|
||||||
>
|
|
||||||
<IconButton
|
|
||||||
name={icons.ADVANCED_SETTINGS}
|
|
||||||
onPress={this.onTableOptionsPress}
|
|
||||||
/>
|
|
||||||
</VirtualTableHeaderCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<VirtualTableHeaderCell
|
<VirtualTableHeaderCell
|
||||||
key={name}
|
key={name}
|
||||||
className={styles[name]}
|
className={styles[name]}
|
||||||
name={name}
|
name={name}
|
||||||
isSortable={isSortable}
|
isSortable={false}
|
||||||
{...otherProps}
|
{...otherProps}
|
||||||
>
|
>
|
||||||
{label}
|
|
||||||
|
<TableOptionsModalWrapper
|
||||||
|
columns={columns}
|
||||||
|
optionsComponent={ArtistIndexTableOptionsConnector}
|
||||||
|
onTableOptionChange={onTableOptionChange}
|
||||||
|
>
|
||||||
|
<IconButton
|
||||||
|
name={icons.ADVANCED_SETTINGS}
|
||||||
|
/>
|
||||||
|
</TableOptionsModalWrapper>
|
||||||
</VirtualTableHeaderCell>
|
</VirtualTableHeaderCell>
|
||||||
);
|
);
|
||||||
})
|
}
|
||||||
}
|
|
||||||
|
return (
|
||||||
<TableOptionsModal
|
<VirtualTableHeaderCell
|
||||||
isOpen={this.state.isTableOptionsModalOpen}
|
key={name}
|
||||||
columns={columns}
|
className={classNames(
|
||||||
optionsComponent={ArtistIndexTableOptionsConnector}
|
styles[name],
|
||||||
onTableOptionChange={onTableOptionChange}
|
name === 'sortName' && showBanners && styles.banner,
|
||||||
onModalClose={this.onTableOptionsModalClose}
|
name === 'sortName' && showBanners && !hasGrowableColumns(columns) && styles.bannerGrow
|
||||||
/>
|
)}
|
||||||
</VirtualTableHeader>
|
name={name}
|
||||||
);
|
isSortable={isSortable}
|
||||||
}
|
{...otherProps}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</VirtualTableHeaderCell>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</VirtualTableHeader>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
ArtistIndexHeader.propTypes = {
|
ArtistIndexHeader.propTypes = {
|
||||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
onTableOptionChange: PropTypes.func.isRequired
|
onTableOptionChange: PropTypes.func.isRequired,
|
||||||
|
showBanners: PropTypes.bool.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ArtistIndexHeader;
|
export default ArtistIndexHeader;
|
||||||
|
@ -0,0 +1,17 @@
|
|||||||
|
const growableColumns = [
|
||||||
|
'qualityProfileId',
|
||||||
|
'languageProfileId',
|
||||||
|
'path',
|
||||||
|
'tags'
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function hasGrowableColumns(columns) {
|
||||||
|
return columns.some((column) => {
|
||||||
|
const {
|
||||||
|
name,
|
||||||
|
isVisible
|
||||||
|
} = column;
|
||||||
|
|
||||||
|
return growableColumns.includes(name) && isVisible;
|
||||||
|
});
|
||||||
|
}
|
@ -0,0 +1,33 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React from 'react';
|
||||||
|
import Modal from 'Components/Modal/Modal';
|
||||||
|
import ArtistInteractiveSearchModalContent from './ArtistInteractiveSearchModalContent';
|
||||||
|
|
||||||
|
function ArtistInteractiveSearchModal(props) {
|
||||||
|
const {
|
||||||
|
isOpen,
|
||||||
|
artistId,
|
||||||
|
onModalClose
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
isOpen={isOpen}
|
||||||
|
closeOnBackgroundClick={false}
|
||||||
|
onModalClose={onModalClose}
|
||||||
|
>
|
||||||
|
<ArtistInteractiveSearchModalContent
|
||||||
|
artistId={artistId}
|
||||||
|
onModalClose={onModalClose}
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
ArtistInteractiveSearchModal.propTypes = {
|
||||||
|
isOpen: PropTypes.bool.isRequired,
|
||||||
|
artistId: PropTypes.number.isRequired,
|
||||||
|
onModalClose: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ArtistInteractiveSearchModal;
|
@ -0,0 +1,15 @@
|
|||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { cancelFetchReleases, clearReleases } from 'Store/Actions/releaseActions';
|
||||||
|
import ArtistInteractiveSearchModal from './ArtistInteractiveSearchModal';
|
||||||
|
|
||||||
|
function createMapDispatchToProps(dispatch, props) {
|
||||||
|
return {
|
||||||
|
onModalClose() {
|
||||||
|
dispatch(cancelFetchReleases());
|
||||||
|
dispatch(clearReleases());
|
||||||
|
props.onModalClose();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(null, createMapDispatchToProps)(ArtistInteractiveSearchModal);
|
@ -0,0 +1,45 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React from 'react';
|
||||||
|
import Button from 'Components/Link/Button';
|
||||||
|
import ModalContent from 'Components/Modal/ModalContent';
|
||||||
|
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||||
|
import ModalBody from 'Components/Modal/ModalBody';
|
||||||
|
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||||
|
import InteractiveSearchConnector from 'InteractiveSearch/InteractiveSearchConnector';
|
||||||
|
|
||||||
|
function ArtistInteractiveSearchModalContent(props) {
|
||||||
|
const {
|
||||||
|
artistId,
|
||||||
|
onModalClose
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ModalContent onModalClose={onModalClose}>
|
||||||
|
<ModalHeader>
|
||||||
|
Interactive Search
|
||||||
|
</ModalHeader>
|
||||||
|
|
||||||
|
<ModalBody>
|
||||||
|
<InteractiveSearchConnector
|
||||||
|
type="artist"
|
||||||
|
searchPayload={{
|
||||||
|
artistId
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</ModalBody>
|
||||||
|
|
||||||
|
<ModalFooter>
|
||||||
|
<Button onPress={onModalClose}>
|
||||||
|
Close
|
||||||
|
</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</ModalContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
ArtistInteractiveSearchModalContent.propTypes = {
|
||||||
|
artistId: PropTypes.number.isRequired,
|
||||||
|
onModalClose: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ArtistInteractiveSearchModalContent;
|
@ -0,0 +1,19 @@
|
|||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
||||||
|
import Legend from './Legend';
|
||||||
|
|
||||||
|
function createMapStateToProps() {
|
||||||
|
return createSelector(
|
||||||
|
(state) => state.calendar.options,
|
||||||
|
createUISettingsSelector(),
|
||||||
|
(calendarOptions, uiSettings) => {
|
||||||
|
return {
|
||||||
|
...calendarOptions,
|
||||||
|
colorImpairedMode: uiSettings.enableColorImpairedMode
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(createMapStateToProps)(Legend);
|
@ -0,0 +1,10 @@
|
|||||||
|
.legendIconItem {
|
||||||
|
margin: 3px 0;
|
||||||
|
margin-right: 6px;
|
||||||
|
width: 150px;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
@ -0,0 +1,37 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React from 'react';
|
||||||
|
import Icon from 'Components/Icon';
|
||||||
|
import styles from './LegendIconItem.css';
|
||||||
|
|
||||||
|
function LegendIconItem(props) {
|
||||||
|
const {
|
||||||
|
name,
|
||||||
|
icon,
|
||||||
|
kind,
|
||||||
|
tooltip
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={styles.legendIconItem}
|
||||||
|
title={tooltip}
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
className={styles.icon}
|
||||||
|
name={icon}
|
||||||
|
kind={kind}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{name}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
LegendIconItem.propTypes = {
|
||||||
|
name: PropTypes.string.isRequired,
|
||||||
|
icon: PropTypes.object.isRequired,
|
||||||
|
kind: PropTypes.string.isRequired,
|
||||||
|
tooltip: PropTypes.string.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LegendIconItem;
|
@ -0,0 +1,216 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import { inputTypes } from 'Helpers/Props';
|
||||||
|
import FieldSet from 'Components/FieldSet';
|
||||||
|
import Button from 'Components/Link/Button';
|
||||||
|
import Form from 'Components/Form/Form';
|
||||||
|
import FormGroup from 'Components/Form/FormGroup';
|
||||||
|
import FormLabel from 'Components/Form/FormLabel';
|
||||||
|
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||||
|
import ModalContent from 'Components/Modal/ModalContent';
|
||||||
|
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||||
|
import ModalBody from 'Components/Modal/ModalBody';
|
||||||
|
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||||
|
import { firstDayOfWeekOptions, weekColumnOptions, timeFormatOptions } from 'Settings/UI/UISettings';
|
||||||
|
|
||||||
|
class CalendarOptionsModalContent extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Lifecycle
|
||||||
|
|
||||||
|
constructor(props, context) {
|
||||||
|
super(props, context);
|
||||||
|
|
||||||
|
const {
|
||||||
|
firstDayOfWeek,
|
||||||
|
calendarWeekColumnHeader,
|
||||||
|
timeFormat,
|
||||||
|
enableColorImpairedMode
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
firstDayOfWeek,
|
||||||
|
calendarWeekColumnHeader,
|
||||||
|
timeFormat,
|
||||||
|
enableColorImpairedMode
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate(prevProps) {
|
||||||
|
const {
|
||||||
|
firstDayOfWeek,
|
||||||
|
calendarWeekColumnHeader,
|
||||||
|
timeFormat,
|
||||||
|
enableColorImpairedMode
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
if (
|
||||||
|
prevProps.firstDayOfWeek !== firstDayOfWeek ||
|
||||||
|
prevProps.calendarWeekColumnHeader !== calendarWeekColumnHeader ||
|
||||||
|
prevProps.timeFormat !== timeFormat ||
|
||||||
|
prevProps.enableColorImpairedMode !== enableColorImpairedMode
|
||||||
|
) {
|
||||||
|
this.setState({
|
||||||
|
firstDayOfWeek,
|
||||||
|
calendarWeekColumnHeader,
|
||||||
|
timeFormat,
|
||||||
|
enableColorImpairedMode
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onOptionInputChange = ({ name, value }) => {
|
||||||
|
const {
|
||||||
|
dispatchSetCalendarOption
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
dispatchSetCalendarOption({ [name]: value });
|
||||||
|
}
|
||||||
|
|
||||||
|
onGlobalInputChange = ({ name, value }) => {
|
||||||
|
const {
|
||||||
|
dispatchSaveUISettings
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const setting = { [name]: value };
|
||||||
|
|
||||||
|
this.setState(setting, () => {
|
||||||
|
dispatchSaveUISettings(setting);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onLinkFocus = (event) => {
|
||||||
|
event.target.select();
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
collapseMultipleAlbums,
|
||||||
|
showCutoffUnmetIcon,
|
||||||
|
onModalClose
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const {
|
||||||
|
firstDayOfWeek,
|
||||||
|
calendarWeekColumnHeader,
|
||||||
|
timeFormat,
|
||||||
|
enableColorImpairedMode
|
||||||
|
} = this.state;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ModalContent onModalClose={onModalClose}>
|
||||||
|
<ModalHeader>
|
||||||
|
Calendar Options
|
||||||
|
</ModalHeader>
|
||||||
|
|
||||||
|
<ModalBody>
|
||||||
|
<FieldSet legend="Local">
|
||||||
|
<Form>
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>Collapse Multiple Albums</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.CHECK}
|
||||||
|
name="collapseMultipleAlbums"
|
||||||
|
value={collapseMultipleAlbums}
|
||||||
|
helpText="Collapse multiple albums releasing on the same day"
|
||||||
|
onChange={this.onOptionInputChange}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>Icon for Cutoff Unmet</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.CHECK}
|
||||||
|
name="showCutoffUnmetIcon"
|
||||||
|
value={showCutoffUnmetIcon}
|
||||||
|
helpText="Show icon for files when the cutoff hasn't been met"
|
||||||
|
onChange={this.onOptionInputChange}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
</Form>
|
||||||
|
</FieldSet>
|
||||||
|
|
||||||
|
<FieldSet legend="Global">
|
||||||
|
<Form>
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>First Day of Week</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.SELECT}
|
||||||
|
name="firstDayOfWeek"
|
||||||
|
values={firstDayOfWeekOptions}
|
||||||
|
value={firstDayOfWeek}
|
||||||
|
onChange={this.onGlobalInputChange}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>Week Column Header</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.SELECT}
|
||||||
|
name="calendarWeekColumnHeader"
|
||||||
|
values={weekColumnOptions}
|
||||||
|
value={calendarWeekColumnHeader}
|
||||||
|
onChange={this.onGlobalInputChange}
|
||||||
|
helpText="Shown above each column when week is the active view"
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>Time Format</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.SELECT}
|
||||||
|
name="timeFormat"
|
||||||
|
values={timeFormatOptions}
|
||||||
|
value={timeFormat}
|
||||||
|
onChange={this.onGlobalInputChange}
|
||||||
|
/>
|
||||||
|
</FormGroup><FormGroup>
|
||||||
|
<FormLabel>Enable Color-Impaired Mode</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.CHECK}
|
||||||
|
name="enableColorImpairedMode"
|
||||||
|
value={enableColorImpairedMode}
|
||||||
|
helpText="Altered style to allow color-impaired users to better distinguish color coded information"
|
||||||
|
onChange={this.onGlobalInputChange}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
</Form>
|
||||||
|
</FieldSet>
|
||||||
|
</ModalBody>
|
||||||
|
|
||||||
|
<ModalFooter>
|
||||||
|
<Button onPress={onModalClose}>
|
||||||
|
Close
|
||||||
|
</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</ModalContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CalendarOptionsModalContent.propTypes = {
|
||||||
|
collapseMultipleAlbums: PropTypes.bool.isRequired,
|
||||||
|
showCutoffUnmetIcon: PropTypes.bool.isRequired,
|
||||||
|
firstDayOfWeek: PropTypes.number.isRequired,
|
||||||
|
calendarWeekColumnHeader: PropTypes.string.isRequired,
|
||||||
|
timeFormat: PropTypes.string.isRequired,
|
||||||
|
enableColorImpairedMode: PropTypes.bool.isRequired,
|
||||||
|
dispatchSetCalendarOption: PropTypes.func.isRequired,
|
||||||
|
dispatchSaveUISettings: PropTypes.func.isRequired,
|
||||||
|
onModalClose: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CalendarOptionsModalContent;
|
@ -0,0 +1,25 @@
|
|||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import { setCalendarOption } from 'Store/Actions/calendarActions';
|
||||||
|
import CalendarOptionsModalContent from './CalendarOptionsModalContent';
|
||||||
|
import { saveUISettings } from 'Store/Actions/settingsActions';
|
||||||
|
|
||||||
|
function createMapStateToProps() {
|
||||||
|
return createSelector(
|
||||||
|
(state) => state.calendar.options,
|
||||||
|
(state) => state.settings.ui.item,
|
||||||
|
(options, uiSettings) => {
|
||||||
|
return {
|
||||||
|
...options,
|
||||||
|
...uiSettings
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapDispatchToProps = {
|
||||||
|
dispatchSetCalendarOption: setCalendarOption,
|
||||||
|
dispatchSaveUISettings: saveUISettings
|
||||||
|
};
|
||||||
|
|
||||||
|
export default connect(createMapStateToProps, mapDispatchToProps)(CalendarOptionsModalContent);
|
@ -0,0 +1,58 @@
|
|||||||
|
.input {
|
||||||
|
composes: input from 'Components/Form/Input.css';
|
||||||
|
}
|
||||||
|
|
||||||
|
.hasError {
|
||||||
|
composes: hasError from 'Components/Form/Input.css';
|
||||||
|
}
|
||||||
|
|
||||||
|
.hasWarning {
|
||||||
|
composes: hasWarning from 'Components/Form/Input.css';
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputWrapper {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputContainer {
|
||||||
|
position: relative;
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
@add-mixin scrollbar;
|
||||||
|
@add-mixin scrollbarTrack;
|
||||||
|
@add-mixin scrollbarThumb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputContainerOpen {
|
||||||
|
.container {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
max-height: 200px;
|
||||||
|
width: 100%;
|
||||||
|
border: 1px solid $inputBorderColor;
|
||||||
|
border-radius: 4px;
|
||||||
|
background-color: $white;
|
||||||
|
box-shadow: inset 0 1px 1px $inputBoxShadowColor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.list {
|
||||||
|
margin: 5px 0;
|
||||||
|
padding-left: 0;
|
||||||
|
list-style-type: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.listItem {
|
||||||
|
padding: 0 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.match {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.highlighted {
|
||||||
|
background-color: $menuItemHoverBackgroundColor;
|
||||||
|
}
|
@ -0,0 +1,162 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import Autosuggest from 'react-autosuggest';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import jdu from 'jdu';
|
||||||
|
import styles from './AutoCompleteInput.css';
|
||||||
|
|
||||||
|
class AutoCompleteInput extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Lifecycle
|
||||||
|
|
||||||
|
constructor(props, context) {
|
||||||
|
super(props, context);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
suggestions: []
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Control
|
||||||
|
|
||||||
|
getSuggestionValue(item) {
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderSuggestion(item) {
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onInputChange = (event, { newValue }) => {
|
||||||
|
this.props.onChange({
|
||||||
|
name: this.props.name,
|
||||||
|
value: newValue
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onInputKeyDown = (event) => {
|
||||||
|
const {
|
||||||
|
name,
|
||||||
|
value,
|
||||||
|
onChange
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const { suggestions } = this.state;
|
||||||
|
|
||||||
|
if (
|
||||||
|
event.key === 'Tab' &&
|
||||||
|
suggestions.length &&
|
||||||
|
suggestions[0] !== this.props.value
|
||||||
|
) {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
if (value) {
|
||||||
|
onChange({
|
||||||
|
name,
|
||||||
|
value: suggestions[0]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onInputBlur = () => {
|
||||||
|
this.setState({ suggestions: [] });
|
||||||
|
}
|
||||||
|
|
||||||
|
onSuggestionsFetchRequested = ({ value }) => {
|
||||||
|
const { values } = this.props;
|
||||||
|
const lowerCaseValue = jdu.replace(value).toLowerCase();
|
||||||
|
|
||||||
|
const filteredValues = values.filter((v) => {
|
||||||
|
return jdu.replace(v).toLowerCase().contains(lowerCaseValue);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.setState({ suggestions: filteredValues });
|
||||||
|
}
|
||||||
|
|
||||||
|
onSuggestionsClearRequested = () => {
|
||||||
|
this.setState({ suggestions: [] });
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
className,
|
||||||
|
inputClassName,
|
||||||
|
name,
|
||||||
|
value,
|
||||||
|
placeholder,
|
||||||
|
hasError,
|
||||||
|
hasWarning
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const { suggestions } = this.state;
|
||||||
|
|
||||||
|
const inputProps = {
|
||||||
|
className: classNames(
|
||||||
|
inputClassName,
|
||||||
|
hasError && styles.hasError,
|
||||||
|
hasWarning && styles.hasWarning,
|
||||||
|
),
|
||||||
|
name,
|
||||||
|
value,
|
||||||
|
placeholder,
|
||||||
|
autoComplete: 'off',
|
||||||
|
spellCheck: false,
|
||||||
|
onChange: this.onInputChange,
|
||||||
|
onKeyDown: this.onInputKeyDown,
|
||||||
|
onBlur: this.onInputBlur
|
||||||
|
};
|
||||||
|
|
||||||
|
const theme = {
|
||||||
|
container: styles.inputContainer,
|
||||||
|
containerOpen: styles.inputContainerOpen,
|
||||||
|
suggestionsContainer: styles.container,
|
||||||
|
suggestionsList: styles.list,
|
||||||
|
suggestion: styles.listItem,
|
||||||
|
suggestionHighlighted: styles.highlighted
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={className}>
|
||||||
|
<Autosuggest
|
||||||
|
id={name}
|
||||||
|
inputProps={inputProps}
|
||||||
|
theme={theme}
|
||||||
|
suggestions={suggestions}
|
||||||
|
getSuggestionValue={this.getSuggestionValue}
|
||||||
|
renderSuggestion={this.renderSuggestion}
|
||||||
|
onSuggestionsFetchRequested={this.onSuggestionsFetchRequested}
|
||||||
|
onSuggestionsClearRequested={this.onSuggestionsClearRequested}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AutoCompleteInput.propTypes = {
|
||||||
|
className: PropTypes.string.isRequired,
|
||||||
|
inputClassName: PropTypes.string.isRequired,
|
||||||
|
name: PropTypes.string.isRequired,
|
||||||
|
value: PropTypes.string,
|
||||||
|
values: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||||
|
placeholder: PropTypes.string,
|
||||||
|
hasError: PropTypes.bool,
|
||||||
|
hasWarning: PropTypes.bool,
|
||||||
|
onChange: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
AutoCompleteInput.defaultProps = {
|
||||||
|
className: styles.inputWrapper,
|
||||||
|
inputClassName: styles.input,
|
||||||
|
value: ''
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AutoCompleteInput;
|
@ -0,0 +1,3 @@
|
|||||||
|
.validationFailures {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
@ -0,0 +1,21 @@
|
|||||||
|
.inputContainer {
|
||||||
|
composes: input from 'Components/Form/Input.css';
|
||||||
|
|
||||||
|
position: relative;
|
||||||
|
min-height: 35px;
|
||||||
|
height: auto;
|
||||||
|
|
||||||
|
&.isFocused {
|
||||||
|
outline: 0;
|
||||||
|
border-color: $inputFocusBorderColor;
|
||||||
|
box-shadow: inset 0 1px 1px $inputBoxShadowColor, 0 0 8px $inputFocusBoxShadowColor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.hasError {
|
||||||
|
composes: hasError from 'Components/Form/Input.css';
|
||||||
|
}
|
||||||
|
|
||||||
|
.hasWarning {
|
||||||
|
composes: hasWarning from 'Components/Form/Input.css';
|
||||||
|
}
|
@ -0,0 +1,152 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import KeyValueListInputItem from './KeyValueListInputItem';
|
||||||
|
import styles from './KeyValueListInput.css';
|
||||||
|
|
||||||
|
class KeyValueListInput extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Lifecycle
|
||||||
|
|
||||||
|
constructor(props, context) {
|
||||||
|
super(props, context);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
isFocused: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onItemChange = (index, itemValue) => {
|
||||||
|
const {
|
||||||
|
name,
|
||||||
|
value,
|
||||||
|
onChange
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const newValue = [...value];
|
||||||
|
|
||||||
|
if (index == null) {
|
||||||
|
newValue.push(itemValue);
|
||||||
|
} else {
|
||||||
|
newValue.splice(index, 1, itemValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
onChange({
|
||||||
|
name,
|
||||||
|
value: newValue
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onRemoveItem = (index) => {
|
||||||
|
const {
|
||||||
|
name,
|
||||||
|
value,
|
||||||
|
onChange
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const newValue = [...value];
|
||||||
|
newValue.splice(index, 1);
|
||||||
|
|
||||||
|
onChange({
|
||||||
|
name,
|
||||||
|
value: newValue
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onFocus = () => {
|
||||||
|
this.setState({
|
||||||
|
isFocused: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onBlur = () => {
|
||||||
|
this.setState({
|
||||||
|
isFocused: false
|
||||||
|
});
|
||||||
|
|
||||||
|
const {
|
||||||
|
name,
|
||||||
|
value,
|
||||||
|
onChange
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const newValue = value.reduce((acc, v) => {
|
||||||
|
if (v.key || v.value) {
|
||||||
|
acc.push(v);
|
||||||
|
}
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (newValue.length !== value.length) {
|
||||||
|
onChange({
|
||||||
|
name,
|
||||||
|
value: newValue
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
className,
|
||||||
|
value,
|
||||||
|
keyPlaceholder,
|
||||||
|
valuePlaceholder
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const { isFocused } = this.state;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={classNames(
|
||||||
|
className,
|
||||||
|
isFocused && styles.isFocused
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{
|
||||||
|
[...value, { key: '', value: '' }].map((v, index) => {
|
||||||
|
return (
|
||||||
|
<KeyValueListInputItem
|
||||||
|
key={index}
|
||||||
|
index={index}
|
||||||
|
keyValue={v.key}
|
||||||
|
value={v.value}
|
||||||
|
keyPlaceholder={keyPlaceholder}
|
||||||
|
valuePlaceholder={valuePlaceholder}
|
||||||
|
isNew={index === value.length}
|
||||||
|
onChange={this.onItemChange}
|
||||||
|
onRemove={this.onRemoveItem}
|
||||||
|
onFocus={this.onFocus}
|
||||||
|
onBlur={this.onBlur}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
KeyValueListInput.propTypes = {
|
||||||
|
className: PropTypes.string.isRequired,
|
||||||
|
name: PropTypes.string.isRequired,
|
||||||
|
value: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
hasError: PropTypes.bool,
|
||||||
|
hasWarning: PropTypes.bool,
|
||||||
|
keyPlaceholder: PropTypes.string,
|
||||||
|
valuePlaceholder: PropTypes.string,
|
||||||
|
onChange: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
KeyValueListInput.defaultProps = {
|
||||||
|
className: styles.inputContainer,
|
||||||
|
value: []
|
||||||
|
};
|
||||||
|
|
||||||
|
export default KeyValueListInput;
|
@ -0,0 +1,14 @@
|
|||||||
|
.itemContainer {
|
||||||
|
display: flex;
|
||||||
|
margin-bottom: 3px;
|
||||||
|
border-bottom: 1px solid $inputBorderColor;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.keyInput,
|
||||||
|
.valueInput {
|
||||||
|
border: none;
|
||||||
|
}
|
@ -0,0 +1,117 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import { icons } from 'Helpers/Props';
|
||||||
|
import IconButton from 'Components/Link/IconButton';
|
||||||
|
import TextInput from './TextInput';
|
||||||
|
import styles from './KeyValueListInputItem.css';
|
||||||
|
|
||||||
|
class KeyValueListInputItem extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onKeyChange = ({ value: keyValue }) => {
|
||||||
|
const {
|
||||||
|
index,
|
||||||
|
value,
|
||||||
|
onChange
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
onChange(index, { key: keyValue, value });
|
||||||
|
}
|
||||||
|
|
||||||
|
onValueChange = ({ value }) => {
|
||||||
|
// TODO: Validate here or validate at a lower level component
|
||||||
|
|
||||||
|
const {
|
||||||
|
index,
|
||||||
|
keyValue,
|
||||||
|
onChange
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
onChange(index, { key: keyValue, value });
|
||||||
|
}
|
||||||
|
|
||||||
|
onRemovePress = () => {
|
||||||
|
const {
|
||||||
|
index,
|
||||||
|
onRemove
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
onRemove(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
onFocus = () => {
|
||||||
|
this.props.onFocus();
|
||||||
|
}
|
||||||
|
|
||||||
|
onBlur = () => {
|
||||||
|
this.props.onBlur();
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
keyValue,
|
||||||
|
value,
|
||||||
|
keyPlaceholder,
|
||||||
|
valuePlaceholder,
|
||||||
|
isNew
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.itemContainer}>
|
||||||
|
<TextInput
|
||||||
|
className={styles.keyInput}
|
||||||
|
name="key"
|
||||||
|
value={keyValue}
|
||||||
|
placeholder={keyPlaceholder}
|
||||||
|
onChange={this.onKeyChange}
|
||||||
|
onFocus={this.onFocus}
|
||||||
|
onBlur={this.onBlur}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TextInput
|
||||||
|
className={styles.valueInput}
|
||||||
|
name="value"
|
||||||
|
value={value}
|
||||||
|
placeholder={valuePlaceholder}
|
||||||
|
onChange={this.onValueChange}
|
||||||
|
onFocus={this.onFocus}
|
||||||
|
onBlur={this.onBlur}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{
|
||||||
|
!isNew &&
|
||||||
|
<IconButton
|
||||||
|
name={icons.REMOVE}
|
||||||
|
tabIndex={-1}
|
||||||
|
onPress={this.onRemovePress}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
KeyValueListInputItem.propTypes = {
|
||||||
|
index: PropTypes.number,
|
||||||
|
keyValue: PropTypes.string.isRequired,
|
||||||
|
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
||||||
|
keyPlaceholder: PropTypes.string.isRequired,
|
||||||
|
valuePlaceholder: PropTypes.string.isRequired,
|
||||||
|
isNew: PropTypes.bool.isRequired,
|
||||||
|
onChange: PropTypes.func.isRequired,
|
||||||
|
onRemove: PropTypes.func.isRequired,
|
||||||
|
onFocus: PropTypes.func.isRequired,
|
||||||
|
onBlur: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
KeyValueListInputItem.defaultProps = {
|
||||||
|
keyPlaceholder: 'Key',
|
||||||
|
valuePlaceholder: 'Value'
|
||||||
|
};
|
||||||
|
|
||||||
|
export default KeyValueListInputItem;
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue