diff --git a/client/coral-admin/src/AppRouter.js b/client/coral-admin/src/AppRouter.js index 6d55d7b18..bc9747f34 100644 --- a/client/coral-admin/src/AppRouter.js +++ b/client/coral-admin/src/AppRouter.js @@ -4,6 +4,7 @@ import {Router, Route, IndexRoute, browserHistory} from 'react-router'; import ModerationQueue from 'containers/ModerationQueue/ModerationQueue'; import CommentStream from 'containers/CommentStream/CommentStream'; import Configure from 'containers/Configure/Configure'; +import Streams from 'containers/Streams/Streams'; import CommunityContainer from 'containers/Community/CommunityContainer'; import LayoutContainer from 'containers/LayoutContainer'; @@ -13,6 +14,7 @@ const routes = ( + ); diff --git a/client/coral-admin/src/actions/assets.js b/client/coral-admin/src/actions/assets.js new file mode 100644 index 000000000..f431f1ad6 --- /dev/null +++ b/client/coral-admin/src/actions/assets.js @@ -0,0 +1,36 @@ +import { + FETCH_ASSETS_REQUEST, + FETCH_ASSETS_SUCCESS, + FETCH_ASSETS_FAILURE, + UPDATE_ASSET_STATE_REQUEST, + UPDATE_ASSET_STATE_SUCCESS, + UPDATE_ASSET_STATE_FAILURE +} from '../constants/assets'; +import coralApi from '../../../coral-framework/helpers/response'; + +/** + * Action disptacher related to assets + */ + +// Fetch a page of assets +// Get comments to fill each of the three lists on the mod queue +export const fetchAssets = (skip = '', limit = '', search = '', sort = '', filter = '') => (dispatch) => { + dispatch({type: FETCH_ASSETS_REQUEST}); + return coralApi(`/assets?skip=${skip}&limit=${limit}&sort=${sort}&search=${search}&filter=${filter}`) + .then(({result, count}) => + dispatch({type: FETCH_ASSETS_SUCCESS, + assets: result, + count + })) + .catch(error => dispatch({type: FETCH_ASSETS_FAILURE, error})); +}; + +// Update an asset state +// Get comments to fill each of the three lists on the mod queue +export const updateAssetState = (id, closedAt) => (dispatch) => { + dispatch({type: UPDATE_ASSET_STATE_REQUEST}); + return coralApi(`/assets/${id}/status`, {method: 'PUT', body: {closedAt}}) + .then(() => + dispatch({type: UPDATE_ASSET_STATE_SUCCESS})) + .catch(error => dispatch({type: UPDATE_ASSET_STATE_FAILURE, error})); +}; diff --git a/client/coral-admin/src/components/ui/Header.js b/client/coral-admin/src/components/ui/Header.js index e4d151d30..4c76424cc 100644 --- a/client/coral-admin/src/components/ui/Header.js +++ b/client/coral-admin/src/components/ui/Header.js @@ -16,6 +16,8 @@ export default ({handleLogout}) => ( activeClassName={styles.active}>{lang.t('configure.community')} {lang.t('configure.configure')} + {lang.t('configure.streams')}
    diff --git a/client/coral-admin/src/constants/assets.js b/client/coral-admin/src/constants/assets.js new file mode 100644 index 000000000..0a2ecf73c --- /dev/null +++ b/client/coral-admin/src/constants/assets.js @@ -0,0 +1,6 @@ +export const FETCH_ASSETS_REQUEST = 'FETCH_ASSETS_REQUEST'; +export const FETCH_ASSETS_SUCCESS = 'FETCH_ASSETS_SUCCESS'; +export const FETCH_ASSETS_FAILURE = 'FETCH_ASSETS_FAILURE'; +export const UPDATE_ASSET_STATE_REQUEST = 'UPDATE_ASSET_STATE_REQUEST'; +export const UPDATE_ASSET_STATE_SUCCESS = 'UPDATE_ASSET_STATE_SUCCESS'; +export const UPDATE_ASSET_STATE_FAILURE = 'UPDATE_ASSET_STATE_FAILURE'; diff --git a/client/coral-admin/src/constants/comments.js b/client/coral-admin/src/constants/comments.js index c75911587..c17aa1fd1 100644 --- a/client/coral-admin/src/constants/comments.js +++ b/client/coral-admin/src/constants/comments.js @@ -1,6 +1,5 @@ export const SHOW_BANUSER_DIALOG = 'SHOW_BANUSER_DIALOG'; export const HIDE_BANUSER_DIALOG = 'HIDE_BANUSER_DIALOG'; -export const USER_BAN_SUCESS = 'USER_BAN_SUCESS'; export const USERS_MODERATION_QUEUE_FETCH_SUCCESS = 'USERS_MODERATION_QUEUE_FETCH_SUCCESS'; export const COMMENTS_MODERATION_QUEUE_FETCH = 'COMMENTS_MODERATION_QUEUE_FETCH'; export const COMMENTS_MODERATION_QUEUE_FETCH_SUCCESS = 'COMMENTS_MODERATION_QUEUE_FETCH_SUCCESS'; diff --git a/client/coral-admin/src/containers/Community/Community.js b/client/coral-admin/src/containers/Community/Community.js index e798266f0..286a7f364 100644 --- a/client/coral-admin/src/containers/Community/Community.js +++ b/client/coral-admin/src/containers/Community/Community.js @@ -7,7 +7,7 @@ import styles from './Community.css'; import Table from './Table'; import Loading from './Loading'; import NoResults from './NoResults'; -import Pager from './Pager'; +import Pager from 'coral-ui/components/Pager'; const lang = new I18n(translations); diff --git a/client/coral-admin/src/containers/Streams/Streams.css b/client/coral-admin/src/containers/Streams/Streams.css new file mode 100644 index 000000000..c64de7974 --- /dev/null +++ b/client/coral-admin/src/containers/Streams/Streams.css @@ -0,0 +1,83 @@ +.container { + padding: 10px; + display: flex; +} + +.leftColumn { + width: 200px; +} + +.mainContent { + width: calc(90% - 200px); +} + +.searchIcon { + vertical-align: middle; + font-size: 18px; + color: #ccc; +} + +.searchBox { + padding: 3px; + border: 1px solid #ccc; + border-radius: 3px; + width: 90%; + display: flex; +} + +.searchBoxInput { + border: none; + flex: 1; + font-size: 14px; +} + +.searchBoxInput:focus { + outline: none; +} + +.optionHeader { + font-size: 16px; + font-weight: 900; + margin-top: 15px; +} + +.optionDetail { + font-size: 16px; + margin-top: 3px; +} + +.streamsTable { + width: 100%; +} + +.radio { + display: block; +} + +.statusMenu { + border-radius: 3px; + width: 10em; + text-align: center; + float: right; + border: 1px solid #ccc; + color: #fff; + cursor: pointer; +} + +.statusMenuOpen { + padding: 10px; + background-color: #4caf50; +} + +.statusMenuIcon { + float: right; +} + +.statusMenuClosed { + padding: 10px; + background-color: #000; +} + +.hidden { + display: none; +} diff --git a/client/coral-admin/src/containers/Streams/Streams.js b/client/coral-admin/src/containers/Streams/Streams.js new file mode 100644 index 000000000..a154516dd --- /dev/null +++ b/client/coral-admin/src/containers/Streams/Streams.js @@ -0,0 +1,180 @@ +import React, {Component} from 'react'; +import styles from './Streams.css'; +import {connect} from 'react-redux'; +import I18n from 'coral-framework/modules/i18n/i18n'; +import {fetchAssets, updateAssetState} from '../../actions/assets'; +import translations from '../../translations.json'; +import { + RadioGroup, + Radio, + Icon, + DataTable, + TableHeader +} from 'react-mdl'; +import Pager from 'coral-ui/components/Pager'; + +const limit = 25; + +class Streams extends Component { + + state = { + search: '', + sort: 'desc', + filter: 'all', + statusMenus: {}, + timer: null, + page: 0 + } + + componentDidMount () { + this.props.fetchAssets(0, limit, '', this.state.sortBy); + } + + onSettingChange = (setting) => (e) => { + let options = this.state; + this.setState({[setting]: e.target.value}); + options[setting] = e.target.value; + this.props.fetchAssets(0, limit, options.search, options.sort, options.filter); + } + + onSearchChange = (e) => { + this.setState((prevState) => { + prevState.search = e.target.value; + clearTimeout(prevState.timer); + const fetchAssets = this.props.fetchAssets; + prevState.timer = setTimeout(() => { + fetchAssets(0, limit, e.target.value, this.state.sort, this.state.filter); + }, 350); + return prevState; + }); + } + + renderDate = (date) => { + const d = new Date(date); + return `${d.getMonth() + 1}/${d.getDate()}/${d.getFullYear()}`; + } + + onStatusClick = (closeStream, id, statusMenuOpen) => () => { + if (statusMenuOpen) { + this.setState(prev => { + prev.statusMenus[id] = false; + return prev; + }); + this.props.updateAssetState(id, closeStream ? Date.now() : null) + .then(() => { + const {search, sort, filter, page} = this.state; + this.props.fetchAssets(page, limit, search, sort, filter); + }); + } else { + this.setState(prev => { + prev.statusMenus[id] = true; + return prev; + }); + } + } + + renderStatus = (closedAt, {id}) => { + const closed = closedAt && new Date(closedAt).getTime() < Date.now(); + const statusMenuOpen = this.state.statusMenus[id]; + return
    +
    + {closed ? lang.t('streams.closed') : lang.t('streams.open')} + {!statusMenuOpen && } +
    + { + statusMenuOpen && +
    + {!closed ? lang.t('streams.closed') : lang.t('streams.open')} +
    + } +
    ; + } + + onPageClick = (page) => { + this.setState({page}); + const {search, sort, filter} = this.state; + this.props.fetchAssets((page - 1) * limit, limit, search, sort, filter); + } + + render () { + const {search, sort, filter} = this.state; + const {assets} = this.props; + + return
    +
    + +
    + + +
    + +
    {lang.t('streams.filter-streams')}
    +
    {lang.t('streams.stream-status')}
    + + {lang.t('streams.all')} + {lang.t('streams.open')} + {lang.t('streams.closed')} + +
    {lang.t('streams.sort-by')}
    + + {lang.t('streams.newest')} + {lang.t('streams.oldest')} + +
    + +
    + assets.byId[id])}> + {lang.t('streams.article')} + + {lang.t('streams.pubdate')} + + + {lang.t('streams.status')} + + + +
    +
    ; + } +} + +const mapStateToProps = ({assets}) => { + return { + assets: assets.toJS() + }; +}; +const mapDispatchToProps = (dispatch) => { + return { + fetchAssets: (...args) => { + dispatch(fetchAssets.apply(this, args)); + }, + updateAssetState: (...args) => dispatch(updateAssetState.apply(this, args)) + }; +}; + +export default connect(mapStateToProps, mapDispatchToProps)(Streams); + +const lang = new I18n(translations); diff --git a/client/coral-admin/src/reducers/assets.js b/client/coral-admin/src/reducers/assets.js new file mode 100644 index 000000000..77d0bf081 --- /dev/null +++ b/client/coral-admin/src/reducers/assets.js @@ -0,0 +1,24 @@ +import {Map, List, fromJS} from 'immutable'; +import {FETCH_ASSETS_SUCCESS, UPDATE_ASSET_STATE_REQUEST} from '../constants/assets'; + +const initialState = Map({ + byId: Map(), + ids: List() +}); + +export default (state = initialState, action) => { + switch (action.type) { + case FETCH_ASSETS_SUCCESS: + return replaceAssets(action, state); + case UPDATE_ASSET_STATE_REQUEST: + return state.setIn(['byId', action.id, 'closedAt'], action.closedAt); + default: return state; + } +}; + +const replaceAssets = (action, state) => { + const assets = fromJS(action.assets.reduce((prev, curr) => { prev[curr.id] = curr; return prev; }, {})); + return state.set('byId', assets) + .set('count', action.count) + .set('ids', List(assets.keys())); +}; diff --git a/client/coral-admin/src/reducers/comments.js b/client/coral-admin/src/reducers/comments.js index 2856b0a34..27dc79177 100644 --- a/client/coral-admin/src/reducers/comments.js +++ b/client/coral-admin/src/reducers/comments.js @@ -33,6 +33,7 @@ export default (state = initialState, action) => { case actions.COMMENT_STREAM_FETCH_SUCCESS: return replaceComments(action, state); case actions.SHOW_BANUSER_DIALOG: return setBanUser(state, true, action); case actions.HIDE_BANUSER_DIALOG: return setBanUser(state, false, action); + case actions.USER_BAN_SUCCESS: return setBanUser(state, false, action); case userActions.UPDATE_STATUS_SUCCESS: return setBanUser(state, false, action); default: return state; } diff --git a/client/coral-admin/src/reducers/index.js b/client/coral-admin/src/reducers/index.js index 1f1b444fc..fca4694d8 100644 --- a/client/coral-admin/src/reducers/index.js +++ b/client/coral-admin/src/reducers/index.js @@ -4,6 +4,7 @@ import settings from 'reducers/settings'; import community from 'reducers/community'; import users from 'reducers/users'; import auth from 'reducers/auth'; +import assets from 'reducers/assets'; // Combine all reducers into a main one export default combineReducers({ @@ -11,5 +12,6 @@ export default combineReducers({ comments, community, auth, - users + users, + assets }); diff --git a/client/coral-admin/src/translations.json b/client/coral-admin/src/translations.json index 6f8e76c4b..11218552f 100644 --- a/client/coral-admin/src/translations.json +++ b/client/coral-admin/src/translations.json @@ -56,6 +56,7 @@ "moderate": "Moderate", "configure": "Configure", "community": "Community", + "streams": "Streams", "closed-comments-desc": "Write a message for closed threads", "closed-comments-label": "Write a message...", "hours": "Hours", @@ -73,6 +74,22 @@ "note": "Note: Banning this user will also place this comment in the Rejected queue.", "cancel": "Cancel", "yes_ban_user": "Yes, Ban User" + }, + "streams": { + "search": "Search", + "filter-streams": "Filter Streams", + "stream-status": "Stream Status", + "all": "All", + "open": "Open", + "closed": "Closed", + "newest": "Newest", + "oldest": "Oldest", + "sort-by": "Sort By", + "open": "Open", + "closed": "Closed", + "article": "Article", + "pubdate": "Publication Date", + "status": "Status" } }, "es": { @@ -139,6 +156,22 @@ "note": "Nota: Suspender este usuario también va a colocar este comentario en la cola de Rechazados.", "cancel": "Cancelar", "yes_ban_user": "Si, Suspendan el usuario" + }, + "streams": { + "search": "", + "filter-streams": "", + "stream-status": "", + "all": "", + "open": "", + "closed": "", + "newest": "", + "oldest": "", + "sort-by": "", + "open": "", + "closed": "", + "article": "", + "pubdate": "", + "status": "" } } } diff --git a/client/coral-embed-stream/style/default.css b/client/coral-embed-stream/style/default.css index 463c8dc68..28b2066e6 100644 --- a/client/coral-embed-stream/style/default.css +++ b/client/coral-embed-stream/style/default.css @@ -114,7 +114,6 @@ hr { .coral-plugin-commentbox-char-count { color: #ccc; text-align: right; - font-size: 12px; } .coral-plugin-commentbox-char-max { @@ -230,7 +229,7 @@ hr { .coral-plugin-flags-popup-header { font-weight: bolder; - font-size: 16px; + font-size: 1.33rem; margin-bottom: 10px; } @@ -240,7 +239,8 @@ hr { .coral-plugin-flags-popup-radio-label { margin:5px; - font-size: 14px; + font-weight: 700; + font-size: .9rem; } .coral-plugin-flags-popup-counter { @@ -254,8 +254,9 @@ hr { margin-top: 10px; } -.coral-plugin-flags-other-text { +.coral-plugin-flags-reason-text { margin-left: 20px; + margin-top: 5px; width: 75%; } @@ -290,7 +291,7 @@ hr { .close-comments-alert { background-color: #d65344; color: white; - font-size: 16px; + font-size: 1.33rem; padding: 5px; } diff --git a/client/coral-plugin-flags/FlagBio.js b/client/coral-plugin-flags/FlagBio.js index 90cc11a7a..edc0a5286 100644 --- a/client/coral-plugin-flags/FlagBio.js +++ b/client/coral-plugin-flags/FlagBio.js @@ -18,7 +18,7 @@ const getPopupMenu = [ {val: 'other', text: lang.t('other')} ], button: lang.t('continue'), - sets: 'detail' + sets: 'reason' }; }, () => { diff --git a/client/coral-plugin-flags/FlagButton.js b/client/coral-plugin-flags/FlagButton.js index 61c87ddcf..097ac9d5b 100644 --- a/client/coral-plugin-flags/FlagButton.js +++ b/client/coral-plugin-flags/FlagButton.js @@ -10,10 +10,9 @@ class FlagButton extends Component { state = { showMenu: false, - showOther: false, itemType: '', - detail: '', - otherText: '', + reason: '', + note: '', step: 0, posted: false } @@ -30,7 +29,7 @@ class FlagButton extends Component { onPopupContinue = () => { const {postAction, addItem, updateItem, flag, id, author_id} = this.props; - const {itemType, field, detail, step, otherText, posted} = this.state; + const {itemType, field, reason, step, note, posted} = this.state; // Proceed to the next step or close the menu if we've reached the end if (step + 1 >= this.props.getPopupMenu.length) { @@ -39,11 +38,10 @@ class FlagButton extends Component { this.setState({step: step + 1}); } - // If itemType and detail are both set, post the action - if (itemType && detail && !posted) { + // If itemType and reason are both set, post the action + if (itemType && reason && !posted) { // Set the text from the "other" field if it exists. - const updatedDetail = otherText || detail; let item_id; switch(itemType) { case 'comments': @@ -55,8 +53,11 @@ class FlagButton extends Component { } const action = { action_type: 'flag', - field, - detail: updatedDetail + metadata: { + field, + reason, + note + } }; postAction(item_id, itemType, action) .then((action) => { @@ -70,11 +71,6 @@ class FlagButton extends Component { onPopupOptionClick = (sets) => (e) => { - // If the "other" option is clicked, show the other textbox - if(sets === 'detail' && e.target.value === 'other') { - this.setState({showOther: true}); - } - // If flagging a user, indicate that this is referencing the username rather than the bio if(sets === 'itemType' && e.target.value === 'user') { this.setState({field: 'username'}); @@ -92,8 +88,8 @@ class FlagButton extends Component { this.setState({[sets]: e.target.value}); } - onOtherTextChange = (e) => { - this.setState({otherText: e.target.value}); + onNoteTextChange = (e) => { + this.setState({note: e.target.value}); } handleClickOutside () { @@ -142,16 +138,16 @@ class FlagButton extends Component { ) } { - this.state.showOther &&
    - -
    + this.state.reason &&
    +
    +