Cards typography (#139)
* Fixed some ESLint errors * Better card's design for the editorial process. - Use Card component from react-toolbox - Added "Edit" buttons for cards - Cleaned up CSS and JS Fixes #125 * Better ImageCard and card list view. Fixes #125 * Use collection label instead of name on the CollectionPage
This commit is contained in:
parent
434f45c97c
commit
c3b4fd9013
@ -3,9 +3,9 @@ import ImageCard from './Cards/ImageCard';
|
|||||||
import AlltypeCard from './Cards/AlltypeCard';
|
import AlltypeCard from './Cards/AlltypeCard';
|
||||||
|
|
||||||
const Cards = {
|
const Cards = {
|
||||||
_unknown: UnknownCard,
|
unknown: UnknownCard,
|
||||||
image: ImageCard,
|
image: ImageCard,
|
||||||
alltype: AlltypeCard
|
alltype: AlltypeCard,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Cards;
|
export default Cards;
|
||||||
|
@ -1,18 +1,4 @@
|
|||||||
.root {
|
.root {
|
||||||
|
width: 240px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.root h1 {
|
|
||||||
font-size: 17px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.root h2 {
|
|
||||||
font-weight: 400;
|
|
||||||
font-size: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.root p {
|
|
||||||
color: #555;
|
|
||||||
font-size: 14px;
|
|
||||||
margin-top: 5px;
|
|
||||||
}
|
|
||||||
|
@ -1,31 +1,51 @@
|
|||||||
import React, { PropTypes } from 'react';
|
import React, { PropTypes } from 'react';
|
||||||
import { Card } from '../UI';
|
import { Card, CardMedia, CardTitle, CardText } from 'react-toolbox/lib/card';
|
||||||
import styles from './ImageCard.css';
|
import styles from './ImageCard.css';
|
||||||
|
|
||||||
export default class ImageCard extends React.Component {
|
const ImageCard = (
|
||||||
|
{
|
||||||
render() {
|
author,
|
||||||
const { onClick, onImageLoaded, image, text, description } = this.props;
|
description,
|
||||||
return (
|
image,
|
||||||
<Card onClick={onClick} className={styles.root}>
|
text,
|
||||||
<img src={image} onLoad={onImageLoaded} />
|
onClick,
|
||||||
<h2>{text}</h2>
|
onImageLoaded,
|
||||||
|
}) => (
|
||||||
{description ? <p>{description}</p> : null}
|
<Card
|
||||||
</Card>
|
onClick={onClick}
|
||||||
);
|
className={styles.root}
|
||||||
}
|
>
|
||||||
}
|
<CardTitle
|
||||||
|
title={text}
|
||||||
|
subtitle={`by ${ author }`}
|
||||||
|
/>
|
||||||
|
{
|
||||||
|
image && <CardMedia aspectRatio="wide">
|
||||||
|
<img
|
||||||
|
src={image}
|
||||||
|
alt={text}
|
||||||
|
onLoad={onImageLoaded}
|
||||||
|
/>
|
||||||
|
</CardMedia>
|
||||||
|
}
|
||||||
|
{ description && <CardText>{ description }</CardText> }
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
|
||||||
ImageCard.propTypes = {
|
ImageCard.propTypes = {
|
||||||
|
author: PropTypes.string,
|
||||||
image: PropTypes.string,
|
image: PropTypes.string,
|
||||||
onClick: PropTypes.func,
|
onClick: PropTypes.func,
|
||||||
onImageLoaded: PropTypes.func,
|
onImageLoaded: PropTypes.func,
|
||||||
text: PropTypes.string.isRequired,
|
text: PropTypes.string.isRequired,
|
||||||
description: PropTypes.string
|
description: PropTypes.string,
|
||||||
};
|
};
|
||||||
|
|
||||||
ImageCard.defaultProps = {
|
ImageCard.defaultProps = {
|
||||||
onClick: function() {},
|
onClick: () => {
|
||||||
onImageLoaded: function() {}
|
},
|
||||||
|
onImageLoaded: () => {
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export default ImageCard;
|
||||||
|
@ -1,13 +1,24 @@
|
|||||||
import React, { PropTypes } from 'react';
|
import React, { PropTypes } from 'react';
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
import { Map } from 'immutable';
|
import { Map } from 'immutable';
|
||||||
import Bricks from 'bricks.js';
|
import { throttle } from 'lodash';
|
||||||
|
import bricks from 'bricks.js';
|
||||||
import Waypoint from 'react-waypoint';
|
import Waypoint from 'react-waypoint';
|
||||||
import history from '../routing/history';
|
import history from '../routing/history';
|
||||||
import Cards from './Cards';
|
import Cards from './Cards';
|
||||||
import _ from 'lodash';
|
|
||||||
|
|
||||||
export default class EntryListing extends React.Component {
|
export default class EntryListing extends React.Component {
|
||||||
|
static propTypes = {
|
||||||
|
children: PropTypes.node.isRequired,
|
||||||
|
collections: PropTypes.oneOfType([
|
||||||
|
ImmutablePropTypes.map,
|
||||||
|
ImmutablePropTypes.iterable,
|
||||||
|
]).isRequired,
|
||||||
|
entries: ImmutablePropTypes.list,
|
||||||
|
onPaginate: PropTypes.func.isRequired,
|
||||||
|
page: PropTypes.number,
|
||||||
|
};
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.bricksInstance = null;
|
this.bricksInstance = null;
|
||||||
@ -24,13 +35,13 @@ export default class EntryListing extends React.Component {
|
|||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
this.updateBricks = _.throttle(this.updateBricks.bind(this), 30);
|
this.updateBricks = throttle(this.updateBricks.bind(this), 30);
|
||||||
this.handleLoadMore = this.handleLoadMore.bind(this);
|
this.handleLoadMore = this.handleLoadMore.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.bricksInstance = Bricks({
|
this.bricksInstance = bricks({
|
||||||
container: this._entries,
|
container: this.containerNode,
|
||||||
packed: this.bricksConfig.packed,
|
packed: this.bricksConfig.packed,
|
||||||
sizes: this.bricksConfig.sizes,
|
sizes: this.bricksConfig.sizes,
|
||||||
});
|
});
|
||||||
@ -45,7 +56,8 @@ export default class EntryListing extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
componentDidUpdate(prevProps) {
|
||||||
if ((prevProps.entries === undefined || prevProps.entries.size === 0) && this.props.entries.size === 0) {
|
if ((prevProps.entries === undefined || prevProps.entries.size === 0)
|
||||||
|
&& this.props.entries.size === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -61,16 +73,18 @@ export default class EntryListing extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
cardFor(collection, entry, link) {
|
cardFor(collection, entry, link) {
|
||||||
const cartType = collection.getIn(['card', 'type']) || 'alltype';
|
const cardType = collection.getIn(['card', 'type']) || 'alltype';
|
||||||
const card = Cards[cartType] || Cards._unknown;
|
const card = Cards[cardType] || Cards.unknown;
|
||||||
return React.createElement(card, {
|
return React.createElement(card, {
|
||||||
key: entry.get('slug'),
|
key: entry.get('slug'),
|
||||||
|
author: entry.getIn(['data', 'author']),
|
||||||
collection,
|
collection,
|
||||||
onClick: history.push.bind(this, link),
|
|
||||||
onImageLoaded: this.updateBricks,
|
|
||||||
text: entry.get('label') ? entry.get('label') : entry.getIn(['data', collection.getIn(['card', 'text'])]),
|
|
||||||
description: entry.getIn(['data', collection.getIn(['card', 'description'])]),
|
description: entry.getIn(['data', collection.getIn(['card', 'description'])]),
|
||||||
image: entry.getIn(['data', collection.getIn(['card', 'image'])]),
|
image: entry.getIn(['data', collection.getIn(['card', 'image'])]),
|
||||||
|
link,
|
||||||
|
text: entry.get('label') ? entry.get('label') : entry.getIn(['data', collection.getIn(['card', 'text'])]),
|
||||||
|
onClick: history.push.bind(this, link),
|
||||||
|
onImageLoaded: this.updateBricks,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -78,6 +92,10 @@ export default class EntryListing extends React.Component {
|
|||||||
this.props.onPaginate(this.props.page + 1);
|
this.props.onPaginate(this.props.page + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleRef = (node) => {
|
||||||
|
this.containerNode = node;
|
||||||
|
};
|
||||||
|
|
||||||
renderCards = () => {
|
renderCards = () => {
|
||||||
const { collections, entries } = this.props;
|
const { collections, entries } = this.props;
|
||||||
if (Map.isMap(collections)) {
|
if (Map.isMap(collections)) {
|
||||||
@ -86,35 +104,25 @@ export default class EntryListing extends React.Component {
|
|||||||
const path = `/collections/${ collectionName }/entries/${ entry.get('slug') }`;
|
const path = `/collections/${ collectionName }/entries/${ entry.get('slug') }`;
|
||||||
return this.cardFor(collections, entry, path);
|
return this.cardFor(collections, entry, path);
|
||||||
});
|
});
|
||||||
} else {
|
|
||||||
return entries.map((entry) => {
|
|
||||||
const collection = collections.filter(collection => collection.get('name') === entry.get('collection')).first();
|
|
||||||
const path = `/collections/${ collection.get('name') }/entries/${ entry.get('slug') }`;
|
|
||||||
return this.cardFor(collection, entry, path);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
return entries.map((entry) => {
|
||||||
|
const collection = collections
|
||||||
|
.filter(collection => collection.get('name') === entry.get('collection')).first();
|
||||||
|
const path = `/collections/${ collection.get('name') }/entries/${ entry.get('slug') }`;
|
||||||
|
return this.cardFor(collection, entry, path);
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { children } = this.props;
|
const { children } = this.props;
|
||||||
const cards = this.renderCards();
|
return (
|
||||||
return (<div>
|
<div>
|
||||||
<h1>{children}</h1>
|
<h1>{children}</h1>
|
||||||
<div ref={c => this._entries = c}>
|
<div ref={this.handleRef}>
|
||||||
{cards}
|
{ this.renderCards() }
|
||||||
<Waypoint onEnter={this.handleLoadMore} />
|
<Waypoint onEnter={this.handleLoadMore} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
EntryListing.propTypes = {
|
|
||||||
children: PropTypes.node.isRequired,
|
|
||||||
collections: PropTypes.oneOfType([
|
|
||||||
ImmutablePropTypes.map,
|
|
||||||
ImmutablePropTypes.iterable,
|
|
||||||
]).isRequired,
|
|
||||||
entries: ImmutablePropTypes.list,
|
|
||||||
onPaginate: PropTypes.func.isRequired,
|
|
||||||
page: PropTypes.number,
|
|
||||||
};
|
|
||||||
|
@ -1,54 +1,34 @@
|
|||||||
|
:root {
|
||||||
|
--highlightColor: #e1eeea;
|
||||||
|
--defaultFontSize: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
.container {
|
.container {
|
||||||
display: table;
|
display: flex;
|
||||||
width: 100%;
|
justify-content: space-between;
|
||||||
}
|
}
|
||||||
|
|
||||||
.column {
|
.column {
|
||||||
display: table-cell;
|
flex: 1 33%;
|
||||||
text-align: center;
|
margin: -10px;
|
||||||
width: 33%;
|
padding: 10px;
|
||||||
height: 100%;
|
max-width: 33%;
|
||||||
transition: background-color .5s ease;
|
transition: background-color .5s ease;
|
||||||
& h2 {
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.highlighted {
|
.columnHovered {
|
||||||
background-color: #e1eeea;
|
composes: column;
|
||||||
|
background-color: var(--highlightColor);
|
||||||
}
|
}
|
||||||
|
|
||||||
.column:not(:last-child) {
|
.columnHeading {
|
||||||
padding-right: 20px;
|
font-size: var(--defaultFontSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
.draggable {
|
||||||
|
cursor: move;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card {
|
.card {
|
||||||
width: 100% !important;
|
margin-bottom: 10px;
|
||||||
margin: 7px 0 0 10px;
|
|
||||||
padding: 7px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cardHeading {
|
|
||||||
font-size: 17px;
|
|
||||||
& small {
|
|
||||||
font-weight: normal;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.cardText {
|
|
||||||
color: #555;
|
|
||||||
font-size: 12px;
|
|
||||||
margin-top: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button {
|
|
||||||
margin: 10px 10px 0 0;
|
|
||||||
float: right;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
.clear::after {
|
|
||||||
content:"";
|
|
||||||
display:block;
|
|
||||||
clear:both;
|
|
||||||
}
|
}
|
||||||
|
@ -1,13 +1,20 @@
|
|||||||
import React, { PropTypes } from 'react';
|
import React, { PropTypes } from 'react';
|
||||||
import { DragSource, DropTarget, HTML5DragDrop } from 'react-simple-dnd';
|
import { DragSource, DropTarget, HTML5DragDrop } from 'react-simple-dnd';
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
import moment from 'moment';
|
|
||||||
import { Card } from './UI';
|
|
||||||
import { Link } from 'react-router';
|
import { Link } from 'react-router';
|
||||||
|
import moment from 'moment';
|
||||||
|
import { Card, CardTitle, CardText, CardActions } from 'react-toolbox/lib/card';
|
||||||
|
import Button from 'react-toolbox/lib/button';
|
||||||
import { status, statusDescriptions } from '../constants/publishModes';
|
import { status, statusDescriptions } from '../constants/publishModes';
|
||||||
import styles from './UnpublishedListing.css';
|
import styles from './UnpublishedListing.css';
|
||||||
|
|
||||||
class UnpublishedListing extends React.Component {
|
class UnpublishedListing extends React.Component {
|
||||||
|
static propTypes = {
|
||||||
|
entries: ImmutablePropTypes.orderedMap,
|
||||||
|
handleChangeStatus: PropTypes.func.isRequired,
|
||||||
|
handlePublish: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
handleChangeStatus = (newStatus, dragProps) => {
|
handleChangeStatus = (newStatus, dragProps) => {
|
||||||
const slug = dragProps.slug;
|
const slug = dragProps.slug;
|
||||||
const collection = dragProps.collection;
|
const collection = dragProps.collection;
|
||||||
@ -23,56 +30,78 @@ class UnpublishedListing extends React.Component {
|
|||||||
};
|
};
|
||||||
|
|
||||||
renderColumns = (entries, column) => {
|
renderColumns = (entries, column) => {
|
||||||
if (!entries) return;
|
if (!entries) return null;
|
||||||
|
|
||||||
if (!column) {
|
if (!column) {
|
||||||
/* eslint-disable */
|
|
||||||
return entries.entrySeq().map(([currColumn, currEntries]) => (
|
return entries.entrySeq().map(([currColumn, currEntries]) => (
|
||||||
<DropTarget key={currColumn} onDrop={this.handleChangeStatus.bind(this, currColumn)}>
|
<DropTarget
|
||||||
{(isOver) => (
|
key={currColumn}
|
||||||
<div className={isOver ? `${styles.column} ${styles.highlighted}` : styles.column}>
|
/* eslint-disable */
|
||||||
<h2>{statusDescriptions.get(currColumn)}</h2>
|
onDrop={this.handleChangeStatus.bind(this, currColumn)}
|
||||||
|
/* eslint-enable */
|
||||||
|
>
|
||||||
|
{isHovered => (
|
||||||
|
<div className={isHovered ? styles.columnHovered : styles.column}>
|
||||||
|
<h2 className={styles.columnHeading}>
|
||||||
|
{statusDescriptions.get(currColumn)}
|
||||||
|
</h2>
|
||||||
{this.renderColumns(currEntries, currColumn)}
|
{this.renderColumns(currEntries, currColumn)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</DropTarget>
|
</DropTarget>
|
||||||
/* eslint-enable */
|
|
||||||
));
|
));
|
||||||
} else {
|
|
||||||
return (<div>
|
|
||||||
{entries.map((entry) => {
|
|
||||||
// Look for an "author" field. Fallback to username on backend implementation;
|
|
||||||
const author = entry.getIn(['data', 'author'], entry.getIn(['metaData', 'user']));
|
|
||||||
const timeStamp = moment(entry.getIn(['metaData', 'timeStamp'])).format('llll');
|
|
||||||
const link = `/editorialworkflow/${ entry.getIn(['metaData', 'collection']) }/${ entry.getIn(['metaData', 'status']) }/${ entry.get('slug') }`;
|
|
||||||
const slug = entry.get('slug');
|
|
||||||
const ownStatus = entry.getIn(['metaData', 'status']);
|
|
||||||
const collection = entry.getIn(['metaData', 'collection']);
|
|
||||||
return (
|
|
||||||
/* eslint-disable */
|
|
||||||
<DragSource key={slug} slug={slug} collection={collection} ownStatus={ownStatus}>
|
|
||||||
<div className={styles.drag}>
|
|
||||||
<Card className={styles.card}>
|
|
||||||
<span className={styles.cardHeading}><Link to={link}>{entry.getIn(['data', 'title'])}</Link> <small>by {author}</small></span>
|
|
||||||
<p className={styles.cardText}>Last updated: {timeStamp} by {entry.getIn(['metaData', 'user'])}</p>
|
|
||||||
{(ownStatus === status.last()) &&
|
|
||||||
<button className={styles.button} onClick={this.requestPublish.bind(this, collection, slug, ownStatus)}>Publish now</button>
|
|
||||||
}
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
</DragSource>
|
|
||||||
/* eslint-enable */
|
|
||||||
);
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
</div>);
|
|
||||||
}
|
}
|
||||||
};
|
return (
|
||||||
|
<div>
|
||||||
static propTypes = {
|
{
|
||||||
entries: ImmutablePropTypes.orderedMap,
|
entries.map((entry) => {
|
||||||
handleChangeStatus: PropTypes.func.isRequired,
|
// Look for an "author" field. Fallback to username on backend implementation;
|
||||||
handlePublish: PropTypes.func.isRequired,
|
const author = entry.getIn(['data', 'author'], entry.getIn(['metaData', 'user']));
|
||||||
|
const timeStamp = moment(entry.getIn(['metaData', 'timeStamp'])).format('llll');
|
||||||
|
const link = `editorialworkflow/${ entry.getIn(['metaData', 'collection']) }/${ entry.getIn(['metaData', 'status']) }/${ entry.get('slug') }`;
|
||||||
|
const slug = entry.get('slug');
|
||||||
|
const ownStatus = entry.getIn(['metaData', 'status']);
|
||||||
|
const collection = entry.getIn(['metaData', 'collection']);
|
||||||
|
return (
|
||||||
|
<DragSource
|
||||||
|
key={slug}
|
||||||
|
slug={slug}
|
||||||
|
collection={collection}
|
||||||
|
ownStatus={ownStatus}
|
||||||
|
>
|
||||||
|
<div className={styles.draggable}>
|
||||||
|
<Card className={styles.card}>
|
||||||
|
<CardTitle
|
||||||
|
title={entry.getIn(['data', 'title'])}
|
||||||
|
subtitle={`by ${ author }`}
|
||||||
|
/>
|
||||||
|
<CardText>
|
||||||
|
Last updated: {timeStamp} by {entry.getIn(['metaData', 'user'])}
|
||||||
|
</CardText>
|
||||||
|
<CardActions>
|
||||||
|
<Link to={link}>
|
||||||
|
<Button>Edit</Button>
|
||||||
|
</Link>
|
||||||
|
{
|
||||||
|
ownStatus === status.last() &&
|
||||||
|
<Button
|
||||||
|
accent
|
||||||
|
/* eslint-disable */
|
||||||
|
onClick={this.requestPublish.bind(this, collection, slug, ownStatus)}
|
||||||
|
/* eslint-enable */
|
||||||
|
>
|
||||||
|
Publish now
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
</CardActions>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</DragSource>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
@ -88,4 +117,4 @@ class UnpublishedListing extends React.Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default HTML5DragDrop(UnpublishedListing);
|
export default HTML5DragDrop(UnpublishedListing); // eslint-disable-line
|
||||||
|
@ -20,8 +20,8 @@ registry.registerWidget('list', ListControl, ListPreview);
|
|||||||
registry.registerWidget('markdown', MarkdownControl, MarkdownPreview);
|
registry.registerWidget('markdown', MarkdownControl, MarkdownPreview);
|
||||||
registry.registerWidget('image', ImageControl, ImagePreview);
|
registry.registerWidget('image', ImageControl, ImagePreview);
|
||||||
registry.registerWidget('datetime', DateTimeControl, DateTimePreview);
|
registry.registerWidget('datetime', DateTimeControl, DateTimePreview);
|
||||||
registry.registerWidget('_unknown', UnknownControl, UnknownPreview);
|
registry.registerWidget('unknown', UnknownControl, UnknownPreview);
|
||||||
|
|
||||||
export function resolveWidget(name) {
|
export function resolveWidget(name) {
|
||||||
return registry.getWidget(name) || registry.getWidget('_unknown');
|
return registry.getWidget(name) || registry.getWidget('unknown');
|
||||||
}
|
}
|
||||||
|
@ -50,7 +50,7 @@ class DashboardPage extends React.Component {
|
|||||||
page={page}
|
page={page}
|
||||||
onPaginate={this.handleLoadMore}
|
onPaginate={this.handleLoadMore}
|
||||||
>
|
>
|
||||||
{collection.get('name')}
|
{collection.get('label')}
|
||||||
</EntryListing>
|
</EntryListing>
|
||||||
:
|
:
|
||||||
<Loader active>{['Loading Entries', 'Caching Entries', 'This might take several minutes']}</Loader>
|
<Loader active>{['Loading Entries', 'Caching Entries', 'This might take several minutes']}</Loader>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user