Add Metadata Profiles (#132)

* Add Metadata Profiles

* fixup! Codacy
This commit is contained in:
Qstick 2017-11-25 22:51:37 -05:00 committed by GitHub
commit 5b7339cd73
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
92 changed files with 2611 additions and 145 deletions

View file

@ -0,0 +1,25 @@
import PropTypes from 'prop-types';
import React from 'react';
import Modal from 'Components/Modal/Modal';
import EditMetadataProfileModalContentConnector from './EditMetadataProfileModalContentConnector';
function EditMetadataProfileModal({ isOpen, onModalClose, ...otherProps }) {
return (
<Modal
isOpen={isOpen}
onModalClose={onModalClose}
>
<EditMetadataProfileModalContentConnector
{...otherProps}
onModalClose={onModalClose}
/>
</Modal>
);
}
EditMetadataProfileModal.propTypes = {
isOpen: PropTypes.bool.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default EditMetadataProfileModal;

View file

@ -0,0 +1,43 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { clearPendingChanges } from 'Store/Actions/baseActions';
import EditMetadataProfileModal from './EditMetadataProfileModal';
function mapStateToProps() {
return {};
}
const mapDispatchToProps = {
clearPendingChanges
};
class EditMetadataProfileModalConnector extends Component {
//
// Listeners
onModalClose = () => {
this.props.clearPendingChanges({ section: 'metadataProfiles' });
this.props.onModalClose();
}
//
// Render
render() {
return (
<EditMetadataProfileModal
{...this.props}
onModalClose={this.onModalClose}
/>
);
}
}
EditMetadataProfileModalConnector.propTypes = {
onModalClose: PropTypes.func.isRequired,
clearPendingChanges: PropTypes.func.isRequired
};
export default connect(mapStateToProps, mapDispatchToProps)(EditMetadataProfileModalConnector);

View file

@ -0,0 +1,3 @@
.deleteButtonContainer {
margin-right: auto;
}

View file

@ -0,0 +1,145 @@
import PropTypes from 'prop-types';
import React from 'react';
import { inputTypes, kinds } from 'Helpers/Props';
import Button from 'Components/Link/Button';
import SpinnerErrorButton from 'Components/Link/SpinnerErrorButton';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
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 Form from 'Components/Form/Form';
import FormGroup from 'Components/Form/FormGroup';
import FormLabel from 'Components/Form/FormLabel';
import FormInputGroup from 'Components/Form/FormInputGroup';
import PrimaryTypeItems from './PrimaryTypeItems';
import SecondaryTypeItems from './SecondaryTypeItems';
import styles from './EditMetadataProfileModalContent.css';
function EditMetadataProfileModalContent(props) {
const {
isFetching,
error,
isSaving,
saveError,
primaryAlbumTypes,
secondaryAlbumTypes,
item,
isInUse,
onInputChange,
onSavePress,
onModalClose,
onDeleteMetadataProfilePress,
...otherProps
} = props;
const {
id,
name,
primaryAlbumTypes: itemPrimaryAlbumTypes,
secondaryAlbumTypes: itemSecondaryAlbumTypes
} = item;
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
{id ? 'Edit Metadata Profile' : 'Add Metadata Profile'}
</ModalHeader>
<ModalBody>
{
isFetching &&
<LoadingIndicator />
}
{
!isFetching && !!error &&
<div>Unable to add a new metadata profile, please try again.</div>
}
{
!isFetching && !error &&
<Form
{...otherProps}
>
<FormGroup>
<FormLabel>Name</FormLabel>
<FormInputGroup
type={inputTypes.TEXT}
name="name"
{...name}
onChange={onInputChange}
/>
</FormGroup>
<PrimaryTypeItems
metadataProfileItems={itemPrimaryAlbumTypes.value}
errors={itemPrimaryAlbumTypes.errors}
warnings={itemPrimaryAlbumTypes.warnings}
formLabel="Primary Album Types"
{...otherProps}
/>
<SecondaryTypeItems
metadataProfileItems={itemSecondaryAlbumTypes.value}
errors={itemSecondaryAlbumTypes.errors}
warnings={itemSecondaryAlbumTypes.warnings}
formLabel="Secondary Album Types"
{...otherProps}
/>
</Form>
}
</ModalBody>
<ModalFooter>
{
id &&
<div
className={styles.deleteButtonContainer}
title={isInUse && 'Can\'t delete a metadata profile that is attached to a artist'}
>
<Button
kind={kinds.DANGER}
isDisabled={isInUse}
onPress={onDeleteMetadataProfilePress}
>
Delete
</Button>
</div>
}
<Button
onPress={onModalClose}
>
Cancel
</Button>
<SpinnerErrorButton
isSpinning={isSaving}
error={saveError}
onPress={onSavePress}
>
Save
</SpinnerErrorButton>
</ModalFooter>
</ModalContent>
);
}
EditMetadataProfileModalContent.propTypes = {
isFetching: PropTypes.bool.isRequired,
error: PropTypes.object,
isSaving: PropTypes.bool.isRequired,
saveError: PropTypes.object,
primaryAlbumTypes: PropTypes.arrayOf(PropTypes.object).isRequired,
secondaryAlbumTypes: PropTypes.arrayOf(PropTypes.object).isRequired,
item: PropTypes.object.isRequired,
isInUse: PropTypes.bool.isRequired,
onInputChange: PropTypes.func.isRequired,
onSavePress: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired,
onDeleteMetadataProfilePress: PropTypes.func
};
export default EditMetadataProfileModalContent;

View file

@ -0,0 +1,180 @@
import _ from 'lodash';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { createSelector } from 'reselect';
import createProfileInUseSelector from 'Store/Selectors/createProfileInUseSelector';
import createProviderSettingsSelector from 'Store/Selectors/createProviderSettingsSelector';
import { fetchMetadataProfileSchema, setMetadataProfileValue, saveMetadataProfile } from 'Store/Actions/settingsActions';
import connectSection from 'Store/connectSection';
import EditMetadataProfileModalContent from './EditMetadataProfileModalContent';
function createPrimaryAlbumTypesSelector() {
return createSelector(
createProviderSettingsSelector(),
(metadataProfile) => {
const primaryAlbumTypes = metadataProfile.item.primaryAlbumTypes;
if (!primaryAlbumTypes || !primaryAlbumTypes.value) {
return [];
}
return _.reduceRight(primaryAlbumTypes.value, (result, { allowed, albumType }) => {
if (allowed) {
result.push({
key: albumType.id,
value: albumType.name
});
}
return result;
}, []);
}
);
}
function createSecondaryAlbumTypesSelector() {
return createSelector(
createProviderSettingsSelector(),
(metadataProfile) => {
const secondaryAlbumTypes = metadataProfile.item.secondaryAlbumTypes;
if (!secondaryAlbumTypes || !secondaryAlbumTypes.value) {
return [];
}
return _.reduceRight(secondaryAlbumTypes.value, (result, { allowed, albumType }) => {
if (allowed) {
result.push({
key: albumType.id,
value: albumType.name
});
}
return result;
}, []);
}
);
}
function createMapStateToProps() {
return createSelector(
createProviderSettingsSelector(),
createPrimaryAlbumTypesSelector(),
createSecondaryAlbumTypesSelector(),
createProfileInUseSelector('metadataProfileId'),
(metadataProfile, primaryAlbumTypes, secondaryAlbumTypes, isInUse) => {
return {
primaryAlbumTypes,
secondaryAlbumTypes,
...metadataProfile,
isInUse
};
}
);
}
const mapDispatchToProps = {
fetchMetadataProfileSchema,
setMetadataProfileValue,
saveMetadataProfile
};
class EditMetadataProfileModalContentConnector extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
dragIndex: null,
dropIndex: null
};
}
componentDidMount() {
if (!this.props.id) {
this.props.fetchMetadataProfileSchema();
}
}
componentDidUpdate(prevProps, prevState) {
if (prevProps.isSaving && !this.props.isSaving && !this.props.saveError) {
this.props.onModalClose();
}
}
//
// Listeners
onInputChange = ({ name, value }) => {
this.props.setMetadataProfileValue({ name, value });
}
onSavePress = () => {
this.props.saveMetadataProfile({ id: this.props.id });
}
onMetadataPrimaryTypeItemAllowedChange = (id, allowed) => {
const metadataProfile = _.cloneDeep(this.props.item);
const item = _.find(metadataProfile.primaryAlbumTypes.value, (i) => i.albumType.id === id);
item.allowed = allowed;
this.props.setMetadataProfileValue({
name: 'primaryAlbumTypes',
value: metadataProfile.primaryAlbumTypes.value
});
}
onMetadataSecondaryTypeItemAllowedChange = (id, allowed) => {
const metadataProfile = _.cloneDeep(this.props.item);
const item = _.find(metadataProfile.secondaryAlbumTypes.value, (i) => i.albumType.id === id);
item.allowed = allowed;
this.props.setMetadataProfileValue({
name: 'secondaryAlbumTypes',
value: metadataProfile.secondaryAlbumTypes.value
});
}
//
// Render
render() {
if (_.isEmpty(this.props.item.primaryAlbumTypes) && !this.props.isFetching) {
return null;
}
return (
<EditMetadataProfileModalContent
{...this.state}
{...this.props}
onSavePress={this.onSavePress}
onInputChange={this.onInputChange}
onMetadataPrimaryTypeItemAllowedChange={this.onMetadataPrimaryTypeItemAllowedChange}
onMetadataSecondaryTypeItemAllowedChange={this.onMetadataSecondaryTypeItemAllowedChange}
/>
);
}
}
EditMetadataProfileModalContentConnector.propTypes = {
id: PropTypes.number,
isFetching: PropTypes.bool.isRequired,
isSaving: PropTypes.bool.isRequired,
saveError: PropTypes.object,
item: PropTypes.object.isRequired,
setMetadataProfileValue: PropTypes.func.isRequired,
fetchMetadataProfileSchema: PropTypes.func.isRequired,
saveMetadataProfile: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default connectSection(
createMapStateToProps,
mapDispatchToProps,
undefined,
undefined,
{ section: 'metadataProfiles' }
)(EditMetadataProfileModalContentConnector);

View file

@ -0,0 +1,19 @@
.metadataProfile {
composes: card from 'Components/Card.css';
width: 300px;
}
.name {
@add-mixin truncate;
margin-bottom: 20px;
font-weight: 300;
font-size: 24px;
}
.albumTypes {
display: flex;
flex-wrap: wrap;
margin-top: 5px;
}

View file

@ -0,0 +1,142 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { kinds } from 'Helpers/Props';
import Card from 'Components/Card';
import Label from 'Components/Label';
import ConfirmModal from 'Components/Modal/ConfirmModal';
import EditMetadataProfileModalConnector from './EditMetadataProfileModalConnector';
import styles from './MetadataProfile.css';
class MetadataProfile extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
isEditMetadataProfileModalOpen: false,
isDeleteMetadataProfileModalOpen: false
};
}
//
// Listeners
onEditMetadataProfilePress = () => {
this.setState({ isEditMetadataProfileModalOpen: true });
}
onEditMetadataProfileModalClose = () => {
this.setState({ isEditMetadataProfileModalOpen: false });
}
onDeleteMetadataProfilePress = () => {
this.setState({
isEditMetadataProfileModalOpen: false,
isDeleteMetadataProfileModalOpen: true
});
}
onDeleteMetadataProfileModalClose = () => {
this.setState({ isDeleteMetadataProfileModalOpen: false });
}
onConfirmDeleteMetadataProfile = () => {
this.props.onConfirmDeleteMetadataProfile(this.props.id);
}
//
// Render
render() {
const {
id,
name,
primaryAlbumTypes,
secondaryAlbumTypes,
isDeleting
} = this.props;
return (
<Card
className={styles.metadataProfile}
onPress={this.onEditMetadataProfilePress}
>
<div className={styles.name}>
{name}
</div>
<div className={styles.albumTypes}>
{
primaryAlbumTypes.map((item) => {
if (!item.allowed) {
return null;
}
return (
<Label
key={item.albumType.id}
kind={kinds.default}
title={null}
>
{item.albumType.name}
</Label>
);
})
}
</div>
<div className={styles.albumTypes}>
{
secondaryAlbumTypes.map((item) => {
if (!item.allowed) {
return null;
}
return (
<Label
key={item.albumType.id}
kind={kinds.INFO}
title={null}
>
{item.albumType.name}
</Label>
);
})
}
</div>
<EditMetadataProfileModalConnector
id={id}
isOpen={this.state.isEditMetadataProfileModalOpen}
onModalClose={this.onEditMetadataProfileModalClose}
onDeleteMetadataProfilePress={this.onDeleteMetadataProfilePress}
/>
<ConfirmModal
isOpen={this.state.isDeleteMetadataProfileModalOpen}
kind={kinds.DANGER}
title="Delete Metadata Profile"
message={`Are you sure you want to delete the metadata profile '${name}'?`}
confirmLabel="Delete"
isSpinning={isDeleting}
onConfirm={this.onConfirmDeleteMetadataProfile}
onCancel={this.onDeleteMetadataProfileModalClose}
/>
</Card>
);
}
}
MetadataProfile.propTypes = {
id: PropTypes.number.isRequired,
name: PropTypes.string.isRequired,
primaryAlbumTypes: PropTypes.arrayOf(PropTypes.object).isRequired,
secondaryAlbumTypes: PropTypes.arrayOf(PropTypes.object).isRequired,
isDeleting: PropTypes.bool.isRequired,
onConfirmDeleteMetadataProfile: PropTypes.func.isRequired
};
export default MetadataProfile;

View file

@ -0,0 +1,21 @@
.metadataProfiles {
display: flex;
flex-wrap: wrap;
}
.addMetadataProfile {
composes: metadataProfile from './MetadataProfile.css';
background-color: $cardAlternateBackgroundColor;
color: $gray;
text-align: center;
font-size: 45px;
}
.center {
display: inline-block;
padding: 5px 20px 0;
border: 1px solid $borderColor;
border-radius: 4px;
background-color: $white;
}

View file

@ -0,0 +1,102 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import sortByName from 'Utilities/Array/sortByName';
import { icons } from 'Helpers/Props';
import FieldSet from 'Components/FieldSet';
import Card from 'Components/Card';
import Icon from 'Components/Icon';
import PageSectionContent from 'Components/Page/PageSectionContent';
import MetadataProfile from './MetadataProfile';
import EditMetadataProfileModalConnector from './EditMetadataProfileModalConnector';
import styles from './MetadataProfiles.css';
class MetadataProfiles extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
isMetadataProfileModalOpen: false
};
}
//
// Listeners
onEditMetadataProfilePress = () => {
this.setState({ isMetadataProfileModalOpen: true });
}
onModalClose = () => {
this.setState({ isMetadataProfileModalOpen: false });
}
//
// Render
render() {
const {
items,
isDeleting,
onConfirmDeleteMetadataProfile,
...otherProps
} = this.props;
return (
<FieldSet
legend="Metadata Profiles"
>
<PageSectionContent
errorMessage="Unable to load Metadata Profiles"
{...otherProps}
>
<div className={styles.metadataProfiles}>
{
items.sort(sortByName).map((item) => {
return (
<MetadataProfile
key={item.id}
{...item}
isDeleting={isDeleting}
onConfirmDeleteMetadataProfile={onConfirmDeleteMetadataProfile}
/>
);
})
}
<Card
className={styles.addMetadataProfile}
onPress={this.onEditMetadataProfilePress}
>
<div className={styles.center}>
<Icon
name={icons.ADD}
size={45}
/>
</div>
</Card>
</div>
<EditMetadataProfileModalConnector
isOpen={this.state.isMetadataProfileModalOpen}
onModalClose={this.onModalClose}
/>
</PageSectionContent>
</FieldSet>
);
}
}
MetadataProfiles.propTypes = {
advancedSettings: PropTypes.bool.isRequired,
isFetching: PropTypes.bool.isRequired,
error: PropTypes.object,
items: PropTypes.arrayOf(PropTypes.object).isRequired,
isDeleting: PropTypes.bool.isRequired,
onConfirmDeleteMetadataProfile: PropTypes.func.isRequired
};
export default MetadataProfiles;

View file

@ -0,0 +1,60 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { fetchMetadataProfiles, deleteMetadataProfile } from 'Store/Actions/settingsActions';
import MetadataProfiles from './MetadataProfiles';
function createMapStateToProps() {
return createSelector(
(state) => state.settings.advancedSettings,
(state) => state.settings.metadataProfiles,
(advancedSettings, metadataProfiles) => {
return {
advancedSettings,
...metadataProfiles
};
}
);
}
const mapDispatchToProps = {
fetchMetadataProfiles,
deleteMetadataProfile
};
class MetadataProfilesConnector extends Component {
//
// Lifecycle
componentDidMount() {
this.props.fetchMetadataProfiles();
}
//
// Listeners
onConfirmDeleteMetadataProfile = (id) => {
this.props.deleteMetadataProfile({ id });
}
//
// Render
render() {
return (
<MetadataProfiles
onConfirmDeleteMetadataProfile={this.onConfirmDeleteMetadataProfile}
{...this.props}
/>
);
}
}
MetadataProfilesConnector.propTypes = {
fetchMetadataProfiles: PropTypes.func.isRequired,
deleteMetadataProfile: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(MetadataProfilesConnector);

View file

@ -0,0 +1,60 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import classNames from 'classnames';
import CheckInput from 'Components/Form/CheckInput';
import styles from './TypeItem.css';
class PrimaryTypeItem extends Component {
//
// Listeners
onAllowedChange = ({ value }) => {
const {
albumTypeId,
onMetadataPrimaryTypeItemAllowedChange
} = this.props;
onMetadataPrimaryTypeItemAllowedChange(albumTypeId, value);
}
//
// Render
render() {
const {
name,
allowed
} = this.props;
return (
<div
className={classNames(
styles.metadataProfileItem
)}
>
<label
className={styles.albumTypeName}
>
<CheckInput
containerClassName={styles.checkContainer}
name={name}
value={allowed}
onChange={this.onAllowedChange}
/>
{name}
</label>
</div>
);
}
}
PrimaryTypeItem.propTypes = {
albumTypeId: PropTypes.number.isRequired,
name: PropTypes.string.isRequired,
allowed: PropTypes.bool.isRequired,
sortIndex: PropTypes.number.isRequired,
onMetadataPrimaryTypeItemAllowedChange: PropTypes.func
};
export default PrimaryTypeItem;

View file

@ -0,0 +1,87 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import FormGroup from 'Components/Form/FormGroup';
import FormLabel from 'Components/Form/FormLabel';
import FormInputHelpText from 'Components/Form/FormInputHelpText';
import PrimaryTypeItem from './PrimaryTypeItem';
import styles from './TypeItems.css';
class PrimaryTypeItems extends Component {
//
// Render
render() {
const {
metadataProfileItems,
errors,
warnings,
...otherProps
} = this.props;
return (
<FormGroup>
<FormLabel>Primary Types</FormLabel>
<div>
{
errors.map((error, index) => {
return (
<FormInputHelpText
key={index}
text={error.message}
isError={true}
isCheckInput={false}
/>
);
})
}
{
warnings.map((warning, index) => {
return (
<FormInputHelpText
key={index}
text={warning.message}
isWarning={true}
isCheckInput={false}
/>
);
})
}
<div className={styles.albumTypes}>
{
metadataProfileItems.map(({ allowed, albumType }, index) => {
return (
<PrimaryTypeItem
key={albumType.id}
albumTypeId={albumType.id}
name={albumType.name}
allowed={allowed}
sortIndex={index}
{...otherProps}
/>
);
}).reverse()
}
</div>
</div>
</FormGroup>
);
}
}
PrimaryTypeItems.propTypes = {
metadataProfileItems: PropTypes.arrayOf(PropTypes.object).isRequired,
errors: PropTypes.arrayOf(PropTypes.object),
warnings: PropTypes.arrayOf(PropTypes.object),
formLabel: PropTypes.string
};
PrimaryTypeItems.defaultProps = {
errors: [],
warnings: []
};
export default PrimaryTypeItems;

View file

@ -0,0 +1,60 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import classNames from 'classnames';
import CheckInput from 'Components/Form/CheckInput';
import styles from './TypeItem.css';
class SecondaryTypeItem extends Component {
//
// Listeners
onAllowedChange = ({ value }) => {
const {
albumTypeId,
onMetadataSecondaryTypeItemAllowedChange
} = this.props;
onMetadataSecondaryTypeItemAllowedChange(albumTypeId, value);
}
//
// Render
render() {
const {
name,
allowed
} = this.props;
return (
<div
className={classNames(
styles.metadataProfileItem
)}
>
<label
className={styles.albumTypeName}
>
<CheckInput
containerClassName={styles.checkContainer}
name={name}
value={allowed}
onChange={this.onAllowedChange}
/>
{name}
</label>
</div>
);
}
}
SecondaryTypeItem.propTypes = {
albumTypeId: PropTypes.number.isRequired,
name: PropTypes.string.isRequired,
allowed: PropTypes.bool.isRequired,
sortIndex: PropTypes.number.isRequired,
onMetadataSecondaryTypeItemAllowedChange: PropTypes.func
};
export default SecondaryTypeItem;

View file

@ -0,0 +1,87 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import FormGroup from 'Components/Form/FormGroup';
import FormLabel from 'Components/Form/FormLabel';
import FormInputHelpText from 'Components/Form/FormInputHelpText';
import SecondaryTypeItem from './SecondaryTypeItem';
import styles from './TypeItems.css';
class SecondaryTypeItems extends Component {
//
// Render
render() {
const {
metadataProfileItems,
errors,
warnings,
...otherProps
} = this.props;
return (
<FormGroup>
<FormLabel>Secondary Types</FormLabel>
<div>
{
errors.map((error, index) => {
return (
<FormInputHelpText
key={index}
text={error.message}
isError={true}
isCheckInput={false}
/>
);
})
}
{
warnings.map((warning, index) => {
return (
<FormInputHelpText
key={index}
text={warning.message}
isWarning={true}
isCheckInput={false}
/>
);
})
}
<div className={styles.albumTypes}>
{
metadataProfileItems.map(({ allowed, albumType }, index) => {
return (
<SecondaryTypeItem
key={albumType.id}
albumTypeId={albumType.id}
name={albumType.name}
allowed={allowed}
sortIndex={index}
{...otherProps}
/>
);
}).reverse()
}
</div>
</div>
</FormGroup>
);
}
}
SecondaryTypeItems.propTypes = {
metadataProfileItems: PropTypes.arrayOf(PropTypes.object).isRequired,
errors: PropTypes.arrayOf(PropTypes.object),
warnings: PropTypes.arrayOf(PropTypes.object),
formLabel: PropTypes.string
};
SecondaryTypeItems.defaultProps = {
errors: [],
warnings: []
};
export default SecondaryTypeItems;

View file

@ -0,0 +1,25 @@
.metadataProfileItem {
display: flex;
align-items: stretch;
width: 100%;
}
.checkContainer {
position: relative;
margin-right: 4px;
margin-bottom: 7px;
margin-left: 8px;
}
.albumTypeName {
display: flex;
flex-grow: 1;
margin-bottom: 0;
margin-left: 2px;
font-weight: normal;
line-height: 36px;
}
.isDragging {
opacity: 0.25;
}

View file

@ -0,0 +1,6 @@
.albumTypes {
margin-top: 10px;
/* TODO: This should consider the number of types in the list */
min-height: 200px;
user-select: none;
}

View file

@ -6,6 +6,7 @@ import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector';
import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector';
import QualityProfilesConnector from './Quality/QualityProfilesConnector';
import LanguageProfilesConnector from './Language/LanguageProfilesConnector';
import MetadataProfilesConnector from './Metadata/MetadataProfilesConnector';
import DelayProfilesConnector from './Delay/DelayProfilesConnector';
class Profiles extends Component {
@ -23,6 +24,7 @@ class Profiles extends Component {
<PageContentBodyConnector>
<QualityProfilesConnector />
<LanguageProfilesConnector />
<MetadataProfilesConnector />
<DelayProfilesConnector />
</PageContentBodyConnector>
</PageContent>

View file

@ -32,7 +32,7 @@ function Settings() {
</Link>
<div className={styles.summary}>
Quality, Language and Delay profiles
Quality, Language, Metadata, and Delay profiles
</div>
<Link