+
-
Sort Comments
- selectSort(sort)}>
-
-
-
+
+
+
diff --git a/client/coral-admin/src/routes/Stories/components/Stories.css b/client/coral-admin/src/routes/Stories/components/Stories.css
index be6b6f921..e8e2b8474 100644
--- a/client/coral-admin/src/routes/Stories/components/Stories.css
+++ b/client/coral-admin/src/routes/Stories/components/Stories.css
@@ -76,31 +76,6 @@
display: block;
}
-.statusMenu {
- border-radius: 2px;
- width: 10em;
- text-align: center;
- float: right;
- color: #fff;
- cursor: pointer;
- letter-spacing: 0.7px;
- font-weight: 400;
- box-shadow: 0 2px 2px 0 rgba(0,0,0,.14), 0 3px 1px -2px rgba(0,0,0,.2), 0 1px 5px 0 rgba(0,0,0,.12);
-}
-
-.statusMenuOpen {
- padding: 10px;
- background-color: #268D81;
-}
-
-.statusMenuIcon {
- float: right;
-}
-
-.statusMenuClosed {
- padding: 10px;
- background-color: #262626;
-}
.hidden {
display: none;
diff --git a/client/coral-admin/src/routes/Stories/components/Stories.js b/client/coral-admin/src/routes/Stories/components/Stories.js
index d57b8f779..746c733d7 100644
--- a/client/coral-admin/src/routes/Stories/components/Stories.js
+++ b/client/coral-admin/src/routes/Stories/components/Stories.js
@@ -1,16 +1,16 @@
import React, {Component} from 'react';
-import styles from './Stories.css';
-import t from 'coral-framework/services/i18n';
import {Link} from 'react-router';
-
-import {Pager, Icon} from 'coral-ui';
-import {DataTable, TableHeader, RadioGroup, Radio} from 'react-mdl';
-import EmptyCard from 'coral-admin/src/components/EmptyCard';
+import PropTypes from 'prop-types';
import sortBy from 'lodash/sortBy';
+import {Dropdown, Option, Pager, Icon} from 'coral-ui';
+import {DataTable, TableHeader, RadioGroup, Radio} from 'react-mdl';
+import t from 'coral-framework/services/i18n';
+import styles from './Stories.css';
+import EmptyCard from 'coral-admin/src/components/EmptyCard';
const limit = 25;
-export default class Stories extends Component {
+class Stories extends Component {
state = {
search: '',
@@ -50,51 +50,28 @@ export default class Stories extends Component {
return `${d.getMonth() + 1}/${d.getDate()}/${d.getFullYear()}`;
}
- onStatusClick = (closeStream, id, statusMenuOpen) => async () => {
- if (statusMenuOpen) {
- this.setState((prev) => {
- prev.statusMenus[id] = false;
- return prev;
- });
-
- try {
- await this.props.updateAssetState(id, closeStream ? Date.now() : null);
- const {search, sort, filter, page} = this.state;
- this.props.fetchAssets(page, limit, search, sort, filter);
- } catch (err) {
-
- // TODO: handle error.
- console.error(err);
- }
- } else {
- this.setState((prev) => {
- prev.statusMenus[id] = true;
- return prev;
- });
+ onStatusChange = async (closeStream, id) => {
+ try {
+ this.props.updateAssetState(id, closeStream ? Date.now() : null);
+ const {search, sort, filter, page} = this.state;
+ this.props.fetchAssets(page, limit, search, sort, filter);
+ } catch(err) {
+ console.error(err);
}
}
renderTitle = (title, {id}) =>
{title}
renderStatus = (closedAt, {id}) => {
- const closed = closedAt && new Date(closedAt).getTime() < Date.now();
- const statusMenuOpen = this.state.statusMenus[id];
- return
-
- {!statusMenuOpen && }
- {closed ? t('streams.closed') : t('streams.open')}
-
- {
- statusMenuOpen &&
-
- {!closed ? t('streams.closed') : t('streams.open')}
-
- }
-
;
+ const closed = !!(closedAt && new Date(closedAt).getTime() < Date.now());
+ return (
+
this.onStatusChange(value, id)}>
+
+
+
+ );
}
onPageClick = (page) => {
@@ -174,3 +151,11 @@ export default class Stories extends Component {
}
}
+Stories.propTypes = {
+ assets: PropTypes.object,
+ fetchAssets: PropTypes.func,
+ updateAssetState: PropTypes.func,
+};
+
+export default Stories;
+
diff --git a/client/coral-ui/components/Dropdown.css b/client/coral-ui/components/Dropdown.css
new file mode 100644
index 000000000..1d548bc18
--- /dev/null
+++ b/client/coral-ui/components/Dropdown.css
@@ -0,0 +1,62 @@
+.dropdown {
+ position: relative;
+ width: 150px;
+ height: 36px;
+ background: #2c2c2c;
+ box-sizing: border-box;
+ color: white;
+ border-radius: 2px;
+ box-shadow: 0 2px 2px 0 rgba(0,0,0,.14), 0 3px 1px -2px rgba(0,0,0,.2), 0 1px 5px 0 rgba(0,0,0,.12);
+
+ line-height: 1.4;
+ font-size: 13px;
+}
+
+.toggle {
+ padding: 10px 15px;
+ cursor: pointer;
+ outline: none;
+
+ &:focus {
+ background: #888;
+ }
+}
+
+.toggleOpen {
+ background: #888;
+}
+
+.label {
+ text-transform: capitalize;
+}
+
+.list {
+ position: absolute;
+ top: 30px;
+ left: 10px;
+ color: #2c2c2c;
+ border-radius: 2px;
+ background: white;
+ box-shadow: 0 2px 2px 0 rgba(0,0,0,.14), 0 3px 1px -2px rgba(0,0,0,.2), 0 1px 5px 0 rgba(0,0,0,.12);
+ width: 100%;
+ list-style: none;
+ padding: 0;
+ margin: 0;
+ transform: scale(0);
+ transition: transform .1s cubic-bezier(.4,0,.2,1),opacity .1s cubic-bezier(.4,0,.2,1),-webkit-transform .1s cubic-bezier(.4,0,.2,1);
+ transform-origin: 0 0;
+}
+
+.listActive {
+ opacity: 1;
+ transform: scale(1);
+ z-index: 999;
+}
+
+.arrow {
+ position: absolute;
+ right: 15px;
+ font-size: 1.2rem;
+ vertical-align: middle;
+ top: 13px;
+}
diff --git a/client/coral-ui/components/Dropdown.js b/client/coral-ui/components/Dropdown.js
new file mode 100644
index 000000000..97467b49b
--- /dev/null
+++ b/client/coral-ui/components/Dropdown.js
@@ -0,0 +1,187 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import styles from './Dropdown.css';
+import Icon from './Icon';
+import cn from 'classnames';
+import ClickOutside from 'coral-framework/components/ClickOutside';
+
+class Dropdown extends React.Component {
+
+ toggleRef = null;
+ optionsRef = [];
+
+ state = {
+ isOpen: false
+ };
+
+ componentDidUpdate(_, prevState) {
+ if (!this.state.isOpen && prevState.isOpen) {
+
+ // Refocus on the toggle element when menu closes.
+ this.toggleRef.focus();
+ }
+ }
+
+ goUp = () => {
+ const index = this.optionsRef.findIndex((ref) => ref.hasFocus());
+ if (index > 0) {
+ this.optionsRef[index - 1].focus();
+ }
+ }
+
+ goDown = () => {
+ const index = this.optionsRef.findIndex((ref) => ref.hasFocus());
+ if (index < this.optionsRef.length - 1) {
+ this.optionsRef[index + 1].focus();
+ }
+ }
+
+ handleOptionKeyDown = (value, e) => {
+ const code = e.which;
+
+ switch (code) {
+ case 13: // 13 = Return
+ case 32: // 32 = Space
+ e.preventDefault();
+ this.setValue(value);
+ break;
+ case 38: // 38 = Arrow Up
+ e.preventDefault();
+ this.goUp();
+ break;
+ case 40: // 40 = Arrow Down
+ e.preventDefault();
+ this.goDown();
+ break;
+ }
+ }
+
+ handleOptionClick = (value) => {
+ this.setValue(value);
+ }
+
+ setValue = (value) => {
+ if (this.props.onChange) {
+ this.props.onChange(value);
+ }
+
+ this.setState({
+ isOpen: false
+ });
+ }
+
+ toggle = () => {
+ this.setState({
+ isOpen: !this.state.isOpen
+ });
+ }
+
+ handleClick = () => {
+ this.toggle();
+ }
+
+ handleKeyDown = (e) => {
+ const code = e.which;
+
+ // 13 = Return, 32 = Space
+ if ((code === 13) || (code === 32)) {
+ e.preventDefault();
+ this.toggle();
+ }
+ }
+
+ hideMenu = () => {
+ this.setState({
+ isOpen: false
+ });
+ }
+
+ handleToggleRef = (ref) => this.toggleRef = ref;
+
+ handleOptionsRef = (ref, index) => {
+ this.optionsRef[index] = ref;
+
+ // Focus on current value when menu opens.
+ if (ref) {
+ if (ref.props.value === this.props.value || index === 0 && !this.props.value) {
+ ref.focus();
+ return;
+ }
+ }
+ }
+
+ // Trap keyboard focus inside the dropdown until a value has been chosen.
+ trapFocusBegin = () => this.optionsRef[this.optionsRef.length - 1].focus();
+ trapFocusEnd = () => this.optionsRef[0].focus();
+
+ renderLabel() {
+ const options = React.Children.toArray(this.props.children);
+ const option = options.find((option) => option.props.value === this.props.value);
+
+ if (option) {
+ return option.props.label ? option.props.label : option.props.value;
+ } else if (this.props.value) {
+ return this.props.value;
+ } else {
+ return this.props.placeholder;
+ }
+ }
+
+ render() {
+ const {className, toggleClassName, toggleOpenClassName} = this.props;
+ return (
+
+
+
+ {this.props.icon && }
+ {this.renderLabel()}
+ {this.state.isOpen ? : }
+
+ {this.state.isOpen &&
+
+
+
+ {React.Children.toArray(this.props.children)
+ .map((child, i) =>
+ React.cloneElement(child, {
+ key: child.props.value,
+ ref: (ref) => this.handleOptionsRef(ref, i),
+ index: i,
+ onClick: () => this.handleOptionClick(child.props.value),
+ onKeyDown: (e) => this.handleOptionKeyDown(child.props.value, e)
+ }))}
+
+
+
+ }
+
+
+ );
+ }
+}
+
+Dropdown.propTypes = {
+ className: PropTypes.string,
+ toggleClassName: PropTypes.string,
+ toggleOpenClassName: PropTypes.string,
+ placeholder: PropTypes.string,
+ icon: PropTypes.string,
+ onChange: PropTypes.func.isRequired,
+ children: PropTypes.node.isRequired,
+ value: PropTypes.oneOfType([
+ PropTypes.number,
+ PropTypes.string,
+ PropTypes.bool
+ ]),
+};
+
+export default Dropdown;
diff --git a/client/coral-ui/components/Icon.js b/client/coral-ui/components/Icon.js
index e0dfea3a7..344bd7e04 100644
--- a/client/coral-ui/components/Icon.js
+++ b/client/coral-ui/components/Icon.js
@@ -4,12 +4,13 @@ import {Icon as IconMDL} from 'react-mdl';
import cn from 'classnames';
import styles from './Icon.css';
-const Icon = ({className = '', name}) => (
-
+const Icon = ({className = '', ...rest}) => (
+
);
Icon.propTypes = {
- name: PropTypes.string.isRequired
+ name: PropTypes.string.isRequired,
+ className: PropTypes.string,
};
export default Icon;
diff --git a/client/coral-ui/components/Option.css b/client/coral-ui/components/Option.css
index 54784d8fc..1e7627681 100644
--- a/client/coral-ui/components/Option.css
+++ b/client/coral-ui/components/Option.css
@@ -1,3 +1,10 @@
-.Option {
-
+.option {
+ padding: 10px;
+ text-transform: capitalize;
+ outline: none;
+
+ &:focus, &:hover {
+ background-color: #ccc;
+ cursor: pointer;
+ }
}
diff --git a/client/coral-ui/components/Option.js b/client/coral-ui/components/Option.js
index e2c45664c..5b752eb48 100644
--- a/client/coral-ui/components/Option.js
+++ b/client/coral-ui/components/Option.js
@@ -1,14 +1,39 @@
import React from 'react';
-import {Option as OptionMDL} from 'react-mdl-selectfield';
+import PropTypes from 'prop-types';
import styles from './Option.css';
+import cn from 'classnames';
-const Option = (props) => {
- const {children, ...attrs} = props;
- return (
-
- {children}
-
- );
+class Option extends React.Component {
+
+ ref = null;
+
+ handleRef = (ref) => {
+ this.ref = ref;
+ };
+
+ focus = () => {this.ref.focus();}
+ hasFocus = () => document.activeElement === this.ref;
+
+ render() {
+ const {className, label = '', onClick, onKeyDown} = this.props;
+ return (
+
+ {label}
+
+ );
+ }
+}
+
+Option.propTypes = {
+ className: PropTypes.string,
+ label: PropTypes.string,
+ onClick: PropTypes.func,
+ onKeyDown: PropTypes.func,
+ value: PropTypes.oneOfType([
+ PropTypes.number,
+ PropTypes.string,
+ PropTypes.bool
+ ]),
};
export default Option;
diff --git a/client/coral-ui/components/Select.css b/client/coral-ui/components/Select.css
deleted file mode 100644
index ac93a843c..000000000
--- a/client/coral-ui/components/Select.css
+++ /dev/null
@@ -1,33 +0,0 @@
-.Select {
- position: relative;
- width: 100%;
- height: 40px;
- background: #2c2c2c;
- padding: 10px 15px;
- box-sizing: border-box;
- color: white;
- border-radius: 2px;
- box-shadow: 0 2px 2px 0 rgba(0,0,0,.14), 0 3px 1px -2px rgba(0,0,0,.2), 0 1px 5px 0 rgba(0,0,0,.12);
-
- > div {
- padding: 0;
- }
-
- i {
- position: absolute;
- top: 7px;
- right: 7px;
- }
-
- input {
- padding: 0;
- font-size: 13px;
- letter-spacing: 0.7px;
- font-weight: 400;
- }
-
- &:hover {
- cursor: pointer;
- }
-
-}
diff --git a/client/coral-ui/components/Select.js b/client/coral-ui/components/Select.js
deleted file mode 100644
index 2e71d2417..000000000
--- a/client/coral-ui/components/Select.js
+++ /dev/null
@@ -1,14 +0,0 @@
-import React from 'react';
-import {SelectField} from 'react-mdl-selectfield';
-import styles from './Select.css';
-
-const Select = (props) => {
- const {children, ...attrs} = props;
- return (
-
- {children}
-
- );
-};
-
-export default Select;
diff --git a/client/coral-ui/index.js b/client/coral-ui/index.js
index 93fe5af64..c688857a1 100644
--- a/client/coral-ui/index.js
+++ b/client/coral-ui/index.js
@@ -21,10 +21,10 @@ export {default as Success} from './components/Success';
export {default as Pager} from './components/Pager';
export {default as Wizard} from './components/Wizard';
export {default as WizardNav} from './components/WizardNav';
-export {default as Select} from './components/Select';
-export {default as Option} from './components/Option';
export {default as SnackBar} from './components/SnackBar';
export {default as TextArea} from './components/TextArea';
export {default as Drawer} from './components/Drawer';
export {default as Label} from './components/Label';
export {default as FlagLabel} from './components/FlagLabel';
+export {default as Dropdown} from './components/Dropdown';
+export {default as Option} from './components/Option';
diff --git a/graph/loaders/comments.js b/graph/loaders/comments.js
index b6a347b8e..2ad94deac 100644
--- a/graph/loaders/comments.js
+++ b/graph/loaders/comments.js
@@ -92,7 +92,7 @@ const getCommentCountByQuery = (context, {ids, statuses, asset_id, parent_id, au
let query = CommentModel.find();
// If user queries for statuses other than NONE and/or ACCEPTED statuses, it needs
- // special priviledges.
+ // special privileges.
if (
(!statuses || statuses.some((status) => !['NONE', 'ACCEPTED'].includes(status))) &&
(context.user == null || !context.user.can(SEARCH_NON_NULL_OR_ACCEPTED_COMMENTS))
@@ -100,7 +100,7 @@ const getCommentCountByQuery = (context, {ids, statuses, asset_id, parent_id, au
return null;
}
- if (statuses) {
+ if (statuses && statuses.length > 0) {
query = query.where({status: {$in: statuses}});
}
diff --git a/locales/en.yml b/locales/en.yml
index 17eed0ed5..76e0f4bac 100644
--- a/locales/en.yml
+++ b/locales/en.yml
@@ -75,6 +75,7 @@ en:
status: Status
username_and_email: "Username and Email"
yes_ban_user: "Yes Ban User"
+ none: "None"
configure:
apply: Apply
banned_word_text: "Comments which contain these words or phrases (not case-sensitive) will be automatically removed from the comment stream. Type a word and press Enter or Tab to add. Optionally paste a comma-separated list."
@@ -289,6 +290,7 @@ en:
select_stream: "Select Stream"
shift_key: "⇧"
shortcuts: "Shortcuts"
+ sort: "Sort"
show_shortcuts: "Show Shortcuts"
singleview: "Toggle single comment edit view"
thismenu: "Open this menu"
diff --git a/locales/es.yml b/locales/es.yml
index 62fc94256..de3d06692 100644
--- a/locales/es.yml
+++ b/locales/es.yml
@@ -74,6 +74,7 @@ es:
status: Estado
username_and_email: "Usuario y Correo"
yes_ban_user: "Si, Suspendan el usuario"
+ none: "Ninguno"
configure:
apply: Aplicar
banned_word_text: "Comentarios que contengan estas palabras o frases, en mayusculas o minúsculas, serán automáticamente eliminados del hilo de comentario. Escribir una palabra y apretar Enter o Tabulador para agregarla. O pueden pegar una lista de palabras separadas por coma."
@@ -280,6 +281,7 @@ es:
select_stream: "Seleccionar hilo de comentarios"
shift_key: ⇧
shortcuts: Atajos
+ sort: "Ordenar"
show_shortcuts: "Mostrar Atajos"
singleview: "Colocar vista de edición de comentario único"
thismenu: "Abrir este menu"