From 5284ccc8b70aaf695eaca83b5050130c862afaf3 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Mon, 12 Dec 2016 15:05:19 -0700 Subject: [PATCH 01/54] Added support for the suspect wordlist --- .eslintrc.json | 1 - models/action.js | 11 ++++- models/asset.js | 14 ++++--- models/setting.js | 9 ++++- routes/api/comments/index.js | 14 ++++++- services/wordlist.js | 65 +++++++++++++++++++++--------- tests/routes/api/comments/index.js | 46 ++++++++++++++++++--- tests/services/wordlist.js | 37 ++++++++++------- 8 files changed, 148 insertions(+), 49 deletions(-) diff --git a/.eslintrc.json b/.eslintrc.json index 035a86189..0c31cbb90 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -26,7 +26,6 @@ "yoda": [1], "no-path-concat": [2], "eol-last": [1], - "no-continue": [1], "no-nested-ternary": [1], "no-tabs": [2], "no-unneeded-ternary": [1], diff --git a/models/action.js b/models/action.js index ed18e48c5..b098f871f 100644 --- a/models/action.js +++ b/models/action.js @@ -39,8 +39,17 @@ ActionSchema.statics.findById = function(id) { */ ActionSchema.statics.insertUserAction = (action) => { + // Actions are made unique by using a query that can be reproducable, i.e., + // not containing user inputable values. + let query = { + action_type: action.action_type, + item_type: action.item_type, + item_id: action.item_id, + user_id: action.user_id + }; + // Create/Update the action. - return Action.findOneAndUpdate(action, action, { + return Action.findOneAndUpdate(query, action, { // Ensure that if it's new, we return the new object created. new: true, diff --git a/models/asset.js b/models/asset.js index 3c68b7c13..094f25363 100644 --- a/models/asset.js +++ b/models/asset.js @@ -25,10 +25,6 @@ const AssetSchema = new Schema({ type: Date, default: null }, - settings: { - type: Schema.Types.Mixed, - default: null - }, closedAt: { type: Date, default: null @@ -44,7 +40,15 @@ const AssetSchema = new Schema({ subsection: String, author: String, publication_date: Date, - modified_date: Date + modified_date: Date, + + // This object is used exclusivly for storing settings that are to override + // the base settings from the base Settings object. This is to be accessed + // always after running `rectifySettings` against it. + settings: { + type: Schema.Types.Mixed, + default: null + }, }, { versionKey: false, timestamps: { diff --git a/models/setting.js b/models/setting.js index c7589ca56..d8554bc0d 100644 --- a/models/setting.js +++ b/models/setting.js @@ -3,6 +3,13 @@ const Schema = mongoose.Schema; const _ = require('lodash'); const cache = require('../cache'); +const WordlistSchema = new Schema({ + banned: [String], + suspect: [String] +}, { + _id: false +}); + /** * SettingSchema manages application settings that get used on front and backend. * @type {Schema} @@ -38,7 +45,7 @@ const SettingSchema = new Schema({ type: String, default: '' }, - wordlist: [String] + wordlist: WordlistSchema, }, { timestamps: { createdAt: 'created_at', diff --git a/routes/api/comments/index.js b/routes/api/comments/index.js index 168b89fc3..d0d490bb3 100644 --- a/routes/api/comments/index.js +++ b/routes/api/comments/index.js @@ -76,7 +76,7 @@ router.post('/', wordlist.filter('body'), (req, res, next) => { // premod, set it to `premod`. let status; - if (req.wordlist.matched) { + if (req.wordlist.banned) { status = Promise.resolve('rejected'); } else { status = Asset @@ -97,7 +97,7 @@ router.post('/', wordlist.filter('body'), (req, res, next) => { // Return `premod` if pre-moderation is enabled and an empty "new" status // in the event that it is not in pre-moderation mode. - .then(({moderation}) => moderation === 'pre' ? 'premod' : ''); + .then(({moderation}) => moderation === 'pre' ? 'premod' : false); } status.then((status) => Comment.publicCreate({ @@ -108,6 +108,16 @@ router.post('/', wordlist.filter('body'), (req, res, next) => { author_id: req.user.id })) .then((comment) => { + if (req.wordlist.suspect) { + return Comment + .addAction(comment.id, null, 'flag', 'body', 'Matched suspect word filters.') + .then(() => comment); + } + + return comment; + }) + .then((comment) => { + // The comment was created! Send back the created comment. res.status(201).json(comment); }) diff --git a/services/wordlist.js b/services/wordlist.js index 59a842817..2103c7267 100644 --- a/services/wordlist.js +++ b/services/wordlist.js @@ -9,7 +9,10 @@ const Setting = require('../models/setting'); * @type {Object} */ const wordlist = { - list: [], + lists: { + banned: [], + suspect: [] + }, enabled: false }; @@ -32,15 +35,19 @@ wordlist.init = () => { * Inserts the wordlist data and enables the wordlist. * @param {Array} list list of words to be added to the wordlist */ -wordlist.insert = (list) => { +wordlist.insert = (lists) => { // Add the words to this array, but also lowercase the words so that an // easy comparison can take place. - wordlist.list = _.uniq(wordlist.list.concat(list.map((word) => { - return tokenizer.tokenize(word.toLowerCase()); - }))); + ['banned', 'suspect'].forEach((k) => { + if (!(k in lists)) { + return; + } - debug(`Added ${list.length} words to the wordlist, now the wordlist is ${wordlist.list.length} entries long.`); + wordlist.lists[k] = wordlist.parseList(lists[k]); + + debug(`Added ${lists[k].length} words to the ${k} wordlist.`); + }); // Enable the wordlist. wordlist.enabled = true; @@ -48,12 +55,21 @@ wordlist.insert = (list) => { return Promise.resolve(wordlist); }; +/** + * Parses the list content. + * @param {Array} list array of words to parse for a list. + * @return {Array} the parsed list + */ +wordlist.parseList = (list) => _.uniq(list.map((word) => { + return tokenizer.tokenize(word.toLowerCase()); +})); + /** * Tests the phrase to see if it contains any of the defined blockwords. * @param {String} phrase value to check for blockwords. * @return {Boolean} true if a blockword is found, false otherwise. */ -wordlist.match = (phrase) => { +wordlist.match = (list, phrase) => { // Lowercase the word to ensure that we don't miss a match due to // capitalization. @@ -61,7 +77,7 @@ wordlist.match = (phrase) => { // This will return true in the event that at least one blockword is found // in the phrase. - return wordlist.list.some((blockphrase) => { + return list.some((blockphrase) => { // First, let's see if we can find the first word in the blockphrase in the // source phrase. @@ -133,28 +149,39 @@ wordlist.filter = (...fields) => (req, res, next) => { } // Loop over all the fields from the body that we want to check. - const containsProfanity = fields.some((field) => { + for (let i = 0; i < fields.length; i++) { + let field = fields[i]; + let phrase = _.get(req.body, field, false); // If the field doesn't exist in the body, then it can't be profane! if (!phrase) { // Return that there wasn't a profane word here. - return false; + continue; } - // Check if the field contains a profane word. - if (wordlist.match(phrase)) { - debug(`the field "${field}" contained a phrase "${phrase}" which contained a wordlisted word/phrase`); - return true; + // Check if the field contains a banned word. + if (wordlist.match(wordlist.lists.banned, phrase)) { + debug(`the field "${field}" contained a phrase "${phrase}" which contained a banned word/phrase`); + + req.wordlist.banned = ErrContainsProfanity; + + // Stop looping through the fields now, we discovered the worst possible + // situation (a banned word). + break; } - return false; - }); + // Check if the field contains a banned word. + if (wordlist.match(wordlist.lists.suspect, phrase)) { + debug(`the field "${field}" contained a phrase "${phrase}" which contained a suspected word/phrase`); - // The body could contain some profanity, address that here. - if (containsProfanity) { - req.wordlist.matched = ErrContainsProfanity; + req.wordlist.suspect = ErrContainsProfanity; + + // Continue looping through the fields now, we discovered a possible bad + // word (suspect). + continue; + } } next(); diff --git a/tests/routes/api/comments/index.js b/tests/routes/api/comments/index.js index b3e4e2675..c720130c0 100644 --- a/tests/routes/api/comments/index.js +++ b/tests/routes/api/comments/index.js @@ -21,7 +21,7 @@ describe('/api/v1/comments', () => { // Ensure that the settings are always available. beforeEach(() => Promise.all([ - wordlist.insert(['bad words']), + wordlist.insert({banned: ['bad words'], suspect: ['suspect words']}), Setting.init(settings) ])); @@ -145,12 +145,22 @@ describe('/api/v1/comments', () => { describe('#post', () => { let asset_id; + let postmod_asset_id; - beforeEach(() => Asset.findOrCreateByUrl('https://coralproject.net/section/article-is-the-best').then((asset) => { + beforeEach(() => Promise.all([ + Asset.findOrCreateByUrl('https://coralproject.net/section/article-is-the-best').then((asset) => { - // Update the asset id. - asset_id = asset.id; - })); + // Update the asset id. + asset_id = asset.id; + }), + Asset.findOrCreateByUrl('https://coralproject.net/section/postmod-article-is-the-best').then((asset) => { + + // Update the asset id. + postmod_asset_id = asset.id; + + return Asset.overrideSettings(postmod_asset_id, {moderation: 'post'}); + }), + ])); it('should create a comment', () => { return chai.request(app) @@ -175,6 +185,32 @@ describe('/api/v1/comments', () => { }); }); + it('should create a comment with no status and a flag if it contains a suspected word', () => { + return chai.request(app) + .post('/api/v1/comments') + .set(passport.inject({roles: []})) + .send({'body': 'suspect words are the most suspicious', 'author_id': '123', 'asset_id': postmod_asset_id, 'parent_id': ''}) + .then((res) => { + expect(res).to.have.status(201); + expect(res.body).to.have.property('id'); + expect(res.body).to.have.property('status', null); + + return Promise.all([ + res.body, + Action.findByType('flag', 'comments') + ]); + }) + .then(([comment, actions]) => { + expect(actions).to.have.length(1); + + let action = actions[0]; + + expect(action).to.have.property('item_id', comment.id); + expect(action).to.have.property('field', 'body'); + expect(action).to.have.property('detail', 'Matched suspect word filters.'); + }); + }); + it('should create a comment with a premod status if it\'s asset is has pre-moderation enabled', () => { return Asset .findOrCreateByUrl('https://coralproject.net/article1') diff --git a/tests/services/wordlist.js b/tests/services/wordlist.js index 0ae76c176..c65be3b11 100644 --- a/tests/services/wordlist.js +++ b/tests/services/wordlist.js @@ -4,22 +4,25 @@ const wordlist = require('../../services/wordlist'); describe('wordlist: services', () => { - before(() => wordlist.insert([ - 'BAD', - 'bad', - 'how to murder', - 'how to kill' - ])); - - beforeEach(() => { - expect(wordlist.list).to.not.be.empty; - expect(wordlist.enabled).to.be.true; - }); + const wordlists = { + banned: [ + 'BAD', + 'bad', + 'how to murder', + 'how to kill' + ], + suspect: [ + 'murder' + ] + }; describe('#init', () => { + before(() => wordlist.insert(wordlists)); + it('has entries', () => { - expect(wordlist.list).to.not.be.empty; + expect(wordlist.lists.banned).to.not.be.empty; + expect(wordlist.lists.suspect).to.not.be.empty; expect(wordlist.enabled).to.be.true; }); @@ -27,6 +30,8 @@ describe('wordlist: services', () => { describe('#match', () => { + const bannedList = wordlist.parseList(wordlists.banned); + it('does match on a bad word', () => { [ 'how to kill', @@ -36,7 +41,7 @@ describe('wordlist: services', () => { 'how to murder', 'How To mUrDer' ].forEach((word) => { - expect(wordlist.match(word)).to.be.true; + expect(wordlist.match(bannedList, word)).to.be.true; }); }); @@ -48,7 +53,7 @@ describe('wordlist: services', () => { 'how to be a great person?', 'how to not kill?' ].forEach((word) => { - expect(wordlist.match(word)).to.be.false; + expect(wordlist.match(bannedList, word)).to.be.false; }); }); @@ -56,6 +61,8 @@ describe('wordlist: services', () => { describe('#filter', () => { + before(() => wordlist.insert(wordlists)); + it('matches on bodies containing bad words', (done) => { let req = { @@ -68,7 +75,7 @@ describe('wordlist: services', () => { expect(err).to.be.undefined; expect(req).to.have.property('wordlist'); expect(req.wordlist).to.have.property('matched'); - expect(req.wordlist.matched).to.be.equal(wordlist.ErrContainsProfanity); + expect(req.wordlist.banned).to.be.equal(wordlist.ErrContainsProfanity); done(); }); From fe2487dea56a51b9f2fffb1ad33d7714c6a05941 Mon Sep 17 00:00:00 2001 From: David Jay Date: Wed, 14 Dec 2016 15:02:26 -0500 Subject: [PATCH 02/54] Add streams option and sidebar. --- client/coral-admin/src/AppRouter.js | 2 + .../coral-admin/src/components/ui/Header.js | 2 + .../src/containers/Streams/Streams.css | 40 +++++++++++++++++++ .../src/containers/Streams/Streams.js | 40 +++++++++++++++++++ client/coral-admin/src/translations.json | 1 + 5 files changed, 85 insertions(+) create mode 100644 client/coral-admin/src/containers/Streams/Streams.css create mode 100644 client/coral-admin/src/containers/Streams/Streams.js 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/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/containers/Streams/Streams.css b/client/coral-admin/src/containers/Streams/Streams.css new file mode 100644 index 000000000..56d757863 --- /dev/null +++ b/client/coral-admin/src/containers/Streams/Streams.css @@ -0,0 +1,40 @@ +.container { + padding: 10px; + display: flex; +} + +.leftColumn { + width: 200px; +} + +.mainContent { + width: calc(70% - 300px) +} + +.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; +} + +.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..0e3e0d3f1 --- /dev/null +++ b/client/coral-admin/src/containers/Streams/Streams.js @@ -0,0 +1,40 @@ +import React from 'react'; +import styles from './Streams.css'; +import I18n from 'coral-framework/modules/i18n/i18n'; +import translations from '../../translations.json'; +import { + RadioGroup, + Radio, + Icon +} from 'react-mdl'; + +const Streams = () =>
    +
    +
    + + {}} + placeholder="Search"/> +
    +
    {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')} + +
    +
    +
    +
    ; + +export default Streams; + +const lang = new I18n(translations); diff --git a/client/coral-admin/src/translations.json b/client/coral-admin/src/translations.json index e462f7567..4d3525dd9 100644 --- a/client/coral-admin/src/translations.json +++ b/client/coral-admin/src/translations.json @@ -55,6 +55,7 @@ "moderate": "Moderate", "configure": "Configure", "community": "Community", + "streams": "Streams", "closed-comments-desc": "Write a message for closed threads", "closed-comments-label": "Write a message...", "comment-count-header": "Limit Comment Length", From dad46cc8defa45288d50041a0350d09b00be2562 Mon Sep 17 00:00:00 2001 From: David Jay Date: Wed, 14 Dec 2016 15:15:13 -0500 Subject: [PATCH 03/54] Add style and language to left nav. --- .../src/containers/Streams/Streams.css | 15 +++++++++++++ .../src/containers/Streams/Streams.js | 10 ++++----- client/coral-admin/src/translations.json | 22 +++++++++++++++++++ 3 files changed, 42 insertions(+), 5 deletions(-) diff --git a/client/coral-admin/src/containers/Streams/Streams.css b/client/coral-admin/src/containers/Streams/Streams.css index 56d757863..07724baa8 100644 --- a/client/coral-admin/src/containers/Streams/Streams.css +++ b/client/coral-admin/src/containers/Streams/Streams.css @@ -35,6 +35,21 @@ outline: none; } +.optionHeader { + font-size: 16px; + font-weight: 900; + margin-top: 15px; +} + +.optionDetail { + font-size: 16px; + margin-top: 3px; +} + +.radio { + display: block; +} + .hidden { display: none; } diff --git a/client/coral-admin/src/containers/Streams/Streams.js b/client/coral-admin/src/containers/Streams/Streams.js index 0e3e0d3f1..ee5d205ae 100644 --- a/client/coral-admin/src/containers/Streams/Streams.js +++ b/client/coral-admin/src/containers/Streams/Streams.js @@ -16,16 +16,16 @@ const Streams = () =>
    type='text' className={styles.searchBoxInput} onChange={() => {}} - placeholder="Search"/> + placeholder={lang.t('streams.search')}/>
    -
    {lang.t('streams.filter-streams')}
    -
    {lang.t('streams.stream-status')}
    - +
    {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.sort-by')}
    {lang.t('streams.newest')} {lang.t('streams.oldest')} diff --git a/client/coral-admin/src/translations.json b/client/coral-admin/src/translations.json index 4d3525dd9..da859c49c 100644 --- a/client/coral-admin/src/translations.json +++ b/client/coral-admin/src/translations.json @@ -69,6 +69,17 @@ "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" } }, "es": { @@ -129,6 +140,17 @@ "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": "" } } } From 7c6299243f6ecd77f4eb49806736fc1d494925d7 Mon Sep 17 00:00:00 2001 From: David Jay Date: Wed, 14 Dec 2016 15:29:56 -0500 Subject: [PATCH 04/54] Connecting sorting options to state. --- .../src/containers/Streams/Streams.js | 80 ++++++++++++------- 1 file changed, 53 insertions(+), 27 deletions(-) diff --git a/client/coral-admin/src/containers/Streams/Streams.js b/client/coral-admin/src/containers/Streams/Streams.js index ee5d205ae..c6c9feeb3 100644 --- a/client/coral-admin/src/containers/Streams/Streams.js +++ b/client/coral-admin/src/containers/Streams/Streams.js @@ -1,4 +1,4 @@ -import React from 'react'; +import React, {Component} from 'react'; import styles from './Streams.css'; import I18n from 'coral-framework/modules/i18n/i18n'; import translations from '../../translations.json'; @@ -8,32 +8,58 @@ import { Icon } from 'react-mdl'; -const Streams = () =>
    -
    -
    - - {}} - placeholder={lang.t('streams.search')}/> -
    -
    {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')} - -
    -
    -
    -
    ; +class Streams extends Component { + + state = { + searchTerm: '', + sortBy: 'newest', + statusFilter: 'all' + } + + onSettingChange = (setting) => (e) => { + this.setState({[setting]: e.target.value}); + } + + render () { + const {searchTerm, sortBy, statusFilter} = this.state; + + 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')} + +
    +
    +
    +
    ; + } +} export default Streams; From 65b308a0612789aeda3da097ad51942394938146 Mon Sep 17 00:00:00 2001 From: David Jay Date: Wed, 14 Dec 2016 16:01:43 -0500 Subject: [PATCH 05/54] Adding initial stream list. --- .../src/containers/Streams/Streams.css | 2 +- .../src/containers/Streams/Streams.js | 89 ++++++++++++------- 2 files changed, 60 insertions(+), 31 deletions(-) diff --git a/client/coral-admin/src/containers/Streams/Streams.css b/client/coral-admin/src/containers/Streams/Streams.css index 07724baa8..7ab1b5199 100644 --- a/client/coral-admin/src/containers/Streams/Streams.css +++ b/client/coral-admin/src/containers/Streams/Streams.css @@ -8,7 +8,7 @@ } .mainContent { - width: calc(70% - 300px) + width: calc(90% - 200px); } .searchIcon { diff --git a/client/coral-admin/src/containers/Streams/Streams.js b/client/coral-admin/src/containers/Streams/Streams.js index c6c9feeb3..bec61e995 100644 --- a/client/coral-admin/src/containers/Streams/Streams.js +++ b/client/coral-admin/src/containers/Streams/Streams.js @@ -5,7 +5,9 @@ import translations from '../../translations.json'; import { RadioGroup, Radio, - Icon + Icon, + DataTable, + TableHeader } from 'react-mdl'; class Streams extends Component { @@ -20,42 +22,69 @@ class Streams extends Component { this.setState({[setting]: e.target.value}); } + renderDate = (date) => { + const d = new Date(date); + return `${d.getMonth()}/${d.getDate()}/${d.getYear()}`; + } + + renderStatus = (status) => { + return
    {status}
    ; + } + render () { const {searchTerm, sortBy, statusFilter} = this.state; 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')} - + +
    + + +
    + +
    {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')} +
    +
    + + {lang.t('streams.article')} + + {lang.t('streams.pubdate')} + + + {lang.t('streams.status')} + +
    ; } From b98201afa3269c1bae428367effd03a33f4d6b08 Mon Sep 17 00:00:00 2001 From: David Jay Date: Wed, 14 Dec 2016 19:03:07 -0500 Subject: [PATCH 06/54] Adding and testing function to fetch assets in talk reducer. --- client/coral-admin/src/actions/assets.js | 0 client/coral-admin/src/constants/comments.js | 2 +- client/coral-admin/src/reducers/comments.js | 2 +- .../coral-admin/src/services/talk-adapter.js | 34 ++++++--- .../coral-admin/services/talk-adapter.js | 75 +++++++++++++++++++ .../{itemActions.spec.js => itemActions.js} | 0 .../{itemReducer.spec.js => itemReducer.js} | 0 7 files changed, 100 insertions(+), 13 deletions(-) create mode 100644 client/coral-admin/src/actions/assets.js create mode 100644 tests/client/coral-admin/services/talk-adapter.js rename tests/client/coral-framework/store/{itemActions.spec.js => itemActions.js} (100%) rename tests/client/coral-framework/store/{itemReducer.spec.js => itemReducer.js} (100%) diff --git a/client/coral-admin/src/actions/assets.js b/client/coral-admin/src/actions/assets.js new file mode 100644 index 000000000..e69de29bb diff --git a/client/coral-admin/src/constants/comments.js b/client/coral-admin/src/constants/comments.js index 856f619d0..16d72e889 100644 --- a/client/coral-admin/src/constants/comments.js +++ b/client/coral-admin/src/constants/comments.js @@ -1,3 +1,3 @@ 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 USER_BAN_SUCCESS = 'USER_BAN_SUCCESS'; diff --git a/client/coral-admin/src/reducers/comments.js b/client/coral-admin/src/reducers/comments.js index dca726b5e..d5fd85ab6 100644 --- a/client/coral-admin/src/reducers/comments.js +++ b/client/coral-admin/src/reducers/comments.js @@ -32,7 +32,7 @@ export default (state = initialState, action) => { case '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_SUCESS: return setBanUser(state, false, action); + case actions.USER_BAN_SUCCESS: return setBanUser(state, false, action); default: return state; } }; diff --git a/client/coral-admin/src/services/talk-adapter.js b/client/coral-admin/src/services/talk-adapter.js index ab542b4a0..6dc4d5827 100644 --- a/client/coral-admin/src/services/talk-adapter.js +++ b/client/coral-admin/src/services/talk-adapter.js @@ -11,22 +11,20 @@ import coralApi from '../../../coral-framework/helpers/response'; // Intercept redux actions and act over the ones we are interested export default store => next => action => { + next(action); + switch (action.type) { case 'COMMENTS_MODERATION_QUEUE_FETCH': - fetchModerationQueueComments(store); - break; + return fetchModerationQueueComments(store); case 'COMMENT_UPDATE': - updateComment(store, action.comment); - break; + return updateComment(store, action.comment); case 'COMMENT_CREATE': - createComment(store, action.name, action.body); - break; + return createComment(store, action.name, action.body); case 'USER_BAN': - userStatusUpdate(store, action.status, action.userId, action.commentId); - break; + return userStatusUpdate(store, action.status, action.userId, action.commentId); + case 'ASSETS_FETCH': + return fetchAssets(store, action); } - - next(action); }; // Get comments to fill each of the three lists on the mod queue @@ -88,6 +86,20 @@ const createComment = (store, name, comment) => { // Ban a user const userStatusUpdate = (store, status, userId, commentId) => { return coralApi(`/users/${userId}/status`, {method: 'POST', body: {status: status, comment_id: commentId}}) - .then(res => store.dispatch({type: 'USER_BAN_SUCESS', res})) + .then(res => store.dispatch({type: 'USER_BAN_SUCCESS', res})) .catch(error => store.dispatch({type: 'USER_BAN_FAILED', error})); }; + +// Fetch a page of assets +// Get comments to fill each of the three lists on the mod queue +const fetchAssets = (store, action) => { + const {skip, limit, search} = action; + return coralApi(`/assets?skip=${skip}&limit=${limit}&search=${search}`) + .then(({result, count}) => + /* Post comments and users to redux store. Actions will be posted when they are needed. */ + store.dispatch({type: 'ASSETS_FETCH_SUCCESS', + assets: result, + count + })) + .catch(error => store.dispatch({type: 'ASSETS_FETCH_FAILED', error})); +}; diff --git a/tests/client/coral-admin/services/talk-adapter.js b/tests/client/coral-admin/services/talk-adapter.js new file mode 100644 index 000000000..27ae8efcb --- /dev/null +++ b/tests/client/coral-admin/services/talk-adapter.js @@ -0,0 +1,75 @@ +import 'react'; +import 'redux'; +import {expect} from 'chai'; +import fetchMock from 'fetch-mock'; +import adapter from '../../../../client/coral-admin/src/services/talk-adapter'; +import {Map} from 'immutable'; + +import configureStore from 'redux-mock-store'; + +const mockStore = configureStore(); + +describe('talk-adapter.js', () => { + let store; + + beforeEach(() => { + store = mockStore(new Map({})); + fetchMock.restore(); + }); + + describe('ASSETS_FETCH', () => { + + const assets = [ + { + url: 'http://test.com', + id: '123', + status: 'closed' + }, + { + url: 'http://test.org', + id: '456', + status: 'open' + } + ]; + + it('should fetch a list of assets', () => { + + const action = { + type: 'ASSETS_FETCH', + skip: 2, + limit: 20, + search: '' + }; + + fetchMock.get('*', JSON.stringify({ + result: assets, + count: 2 + })); + + return adapter(store)(()=>{})(action) + .then(() => { + expect(store.getActions()[0]).to.have.property('type', 'ASSETS_FETCH_SUCCESS'); + expect(store.getActions()[0]).to.have.property('count', 2); + expect(store.getActions()[0]).to.have.property('assets'). + and.to.deep.equal(assets); + }); + }); + + it('should return an error appropriatly', () => { + + const action = { + type: 'ASSETS_FETCH', + skip: 2, + limit: 20, + search: '' + }; + + fetchMock.get('*', 404); + + return adapter(store)(()=>{})(action) + .then(() => { + expect(store.getActions()[0]).to.have.property('type', 'ASSETS_FETCH_FAILED'); + }); + }); + }); +}); diff --git a/tests/client/coral-framework/store/itemActions.spec.js b/tests/client/coral-framework/store/itemActions.js similarity index 100% rename from tests/client/coral-framework/store/itemActions.spec.js rename to tests/client/coral-framework/store/itemActions.js diff --git a/tests/client/coral-framework/store/itemReducer.spec.js b/tests/client/coral-framework/store/itemReducer.js similarity index 100% rename from tests/client/coral-framework/store/itemReducer.spec.js rename to tests/client/coral-framework/store/itemReducer.js From 78c81aeaf4f538ae2419226490a5108036eab15f Mon Sep 17 00:00:00 2001 From: David Jay Date: Wed, 14 Dec 2016 19:30:59 -0500 Subject: [PATCH 07/54] Adding asset reducer. --- client/coral-admin/src/actions/assets.js | 0 client/coral-admin/src/reducers/assets.js | 20 +++++++++++ tests/client/coral-admin/reducers/assets.js | 39 +++++++++++++++++++++ 3 files changed, 59 insertions(+) delete mode 100644 client/coral-admin/src/actions/assets.js create mode 100644 client/coral-admin/src/reducers/assets.js create mode 100644 tests/client/coral-admin/reducers/assets.js diff --git a/client/coral-admin/src/actions/assets.js b/client/coral-admin/src/actions/assets.js deleted file mode 100644 index e69de29bb..000000000 diff --git a/client/coral-admin/src/reducers/assets.js b/client/coral-admin/src/reducers/assets.js new file mode 100644 index 000000000..bdd1bcaf9 --- /dev/null +++ b/client/coral-admin/src/reducers/assets.js @@ -0,0 +1,20 @@ +import {Map, List, fromJS} from 'immutable'; + +const initialState = Map({ + byId: Map(), + ids: List() +}); + +export default (state = initialState, action) => { + switch (action.type) { + case 'ASSETS_FETCH_SUCCESS': return replaceAssets(action, state); + 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/tests/client/coral-admin/reducers/assets.js b/tests/client/coral-admin/reducers/assets.js new file mode 100644 index 000000000..cf3e0dac4 --- /dev/null +++ b/tests/client/coral-admin/reducers/assets.js @@ -0,0 +1,39 @@ +import {Map} from 'immutable'; +import {expect} from 'chai'; +import assetsReducer from '../../../../client/coral-admin/src/reducers/assets'; + +describe ('assetsReducer', () => { + describe('ASSETS_FETCH_SUCCESS', () => { + it('should replace the existing assets', () => { + const action = { + type: 'ASSETS_FETCH_SUCCESS', + count: 200, + assets: [ + { + id: '123', + url: 'http://test.com', + closedAt: 'tomorrow' + }, + { + id: '456', + url: 'http://test2.com', + closedAt: 'thursday' + }, + ] + }; + const store = new Map({}); + const result = assetsReducer(store, action); + console.log(result.getIn(['byId', '123']).toJS()); + expect(result.getIn(['byId', '123']).toJS()).to.deep.equal({ + url: 'http://test.com', + closedAt: 'tomorrow', + id: '123' + }); + expect(result.getIn(['ids']).toJS()).to.deep.equal([ + '123', + '456' + ]); + expect(result.getIn(['count'])).to.equal(200); + }); + }); +}); From faafbfa78cf3142b35430abb198002985910b173 Mon Sep 17 00:00:00 2001 From: David Jay Date: Wed, 14 Dec 2016 19:58:04 -0500 Subject: [PATCH 08/54] Loading assets and adding to store. --- client/coral-admin/src/constants/assets.js | 1 + .../src/containers/Streams/Streams.js | 23 ++++++++++++++++++- client/coral-admin/src/reducers/index.js | 4 +++- routes/api/assets/index.js | 4 ++-- 4 files changed, 28 insertions(+), 4 deletions(-) create mode 100644 client/coral-admin/src/constants/assets.js diff --git a/client/coral-admin/src/constants/assets.js b/client/coral-admin/src/constants/assets.js new file mode 100644 index 000000000..59b76390f --- /dev/null +++ b/client/coral-admin/src/constants/assets.js @@ -0,0 +1 @@ +export const ASSETS_FETCH = 'ASSETS_FETCH'; diff --git a/client/coral-admin/src/containers/Streams/Streams.js b/client/coral-admin/src/containers/Streams/Streams.js index bec61e995..ea365d0d1 100644 --- a/client/coral-admin/src/containers/Streams/Streams.js +++ b/client/coral-admin/src/containers/Streams/Streams.js @@ -1,6 +1,8 @@ import React, {Component} from 'react'; import styles from './Streams.css'; +import {connect} from 'react-redux'; import I18n from 'coral-framework/modules/i18n/i18n'; +import {ASSETS_FETCH} from '../../constants/assets'; import translations from '../../translations.json'; import { RadioGroup, @@ -18,6 +20,10 @@ class Streams extends Component { statusFilter: 'all' } + componentDidMount () { + this.props.fetchAssets(0, 25, '', 'desc'); + } + onSettingChange = (setting) => (e) => { this.setState({[setting]: e.target.value}); } @@ -90,6 +96,21 @@ class Streams extends Component { } } -export default Streams; +const mapStateToProps = () => {}; +const mapDispatchToProps = (dispatch) => { + return { + fetchAssets: (skip, limit, search, sort) => { + dispatch({ + type: ASSETS_FETCH, + skip, + limit, + search, + sort + }); + } + }; +}; + +export default connect(mapStateToProps, mapDispatchToProps)(Streams); const lang = new I18n(translations); 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/routes/api/assets/index.js b/routes/api/assets/index.js index 5aa9b8cc6..a602d7fd0 100644 --- a/routes/api/assets/index.js +++ b/routes/api/assets/index.js @@ -20,8 +20,8 @@ router.get('/', (req, res, next) => { Asset .search(search) .sort({[field]: (sort === 'asc') ? 1 : -1}) - .skip(skip) - .limit(limit), + .skip(parseInt(skip)) + .limit(parseInt(limit)), Asset .search(search) .count() From d5268989627c9e2f1e1bd2e1778e275c60716217 Mon Sep 17 00:00:00 2001 From: David Jay Date: Wed, 14 Dec 2016 20:19:27 -0500 Subject: [PATCH 09/54] Loading assets into Streams component. --- .../src/containers/Streams/Streams.css | 4 +++ .../src/containers/Streams/Streams.js | 26 ++++++++++--------- client/coral-admin/src/translations.json | 14 ++++++++-- 3 files changed, 30 insertions(+), 14 deletions(-) diff --git a/client/coral-admin/src/containers/Streams/Streams.css b/client/coral-admin/src/containers/Streams/Streams.css index 7ab1b5199..5ad875b35 100644 --- a/client/coral-admin/src/containers/Streams/Streams.css +++ b/client/coral-admin/src/containers/Streams/Streams.css @@ -46,6 +46,10 @@ margin-top: 3px; } +.streamsTable { + width: 100%; +} + .radio { display: block; } diff --git a/client/coral-admin/src/containers/Streams/Streams.js b/client/coral-admin/src/containers/Streams/Streams.js index ea365d0d1..7b1778012 100644 --- a/client/coral-admin/src/containers/Streams/Streams.js +++ b/client/coral-admin/src/containers/Streams/Streams.js @@ -30,15 +30,17 @@ class Streams extends Component { renderDate = (date) => { const d = new Date(date); - return `${d.getMonth()}/${d.getDate()}/${d.getYear()}`; + return `${d.getMonth() + 1}/${d.getDate()}/${d.getFullYear()}`; } - renderStatus = (status) => { + renderStatus = (closedAt) => { + const status = closedAt && new Date(closedAt) < Date.now ? lang.t('streams.closed') : lang.t('streams.open'); return
    {status}
    ; } render () { const {searchTerm, sortBy, statusFilter} = this.state; + const {assets} = this.props; return
    @@ -77,17 +79,13 @@ class Streams extends Component {
    - {lang.t('streams.article')} - + className={styles.streamsTable} + rows={assets.ids.map((id) => assets.byId[id])}> + {lang.t('streams.article')} + {lang.t('streams.pubdate')} - + {lang.t('streams.status')} @@ -96,7 +94,11 @@ class Streams extends Component { } } -const mapStateToProps = () => {}; +const mapStateToProps = ({assets}) => { + return { + assets: assets.toJS() + }; +}; const mapDispatchToProps = (dispatch) => { return { fetchAssets: (skip, limit, search, sort) => { diff --git a/client/coral-admin/src/translations.json b/client/coral-admin/src/translations.json index da859c49c..c093f7ae0 100644 --- a/client/coral-admin/src/translations.json +++ b/client/coral-admin/src/translations.json @@ -79,7 +79,12 @@ "closed": "Closed", "newest": "Newest", "oldest": "Oldest", - "sort-by": "Sort By" + "sort-by": "Sort By", + "open": "Open", + "closed": "Closed", + "article": "Article", + "pubdate": "Publication Date", + "status": "Status" } }, "es": { @@ -150,7 +155,12 @@ "closed": "", "newest": "", "oldest": "", - "sort-by": "" + "sort-by": "", + "open": "", + "closed": "", + "article": "", + "pubdate": "", + "status": "" } } } From 86eef68b163e9412a24a478a0e2dd3644cebf0b4 Mon Sep 17 00:00:00 2001 From: David Jay Date: Thu, 15 Dec 2016 12:25:43 -0500 Subject: [PATCH 10/54] Adding status menu. --- client/coral-admin/src/actions/assets.js | 13 +++++ client/coral-admin/src/constants/assets.js | 5 +- .../src/containers/Streams/Streams.css | 24 ++++++++ .../src/containers/Streams/Streams.js | 55 ++++++++++++++----- client/coral-admin/src/reducers/assets.js | 3 +- .../coral-admin/src/services/talk-adapter.js | 6 +- 6 files changed, 88 insertions(+), 18 deletions(-) create mode 100644 client/coral-admin/src/actions/assets.js diff --git a/client/coral-admin/src/actions/assets.js b/client/coral-admin/src/actions/assets.js new file mode 100644 index 000000000..1e5f2716d --- /dev/null +++ b/client/coral-admin/src/actions/assets.js @@ -0,0 +1,13 @@ +import {FETCH_ASSETS, UPDATE_ASSET} from '../constants/assets'; + +/** + * Action disptacher related to assets + */ + +export const updateAsset = (id, property, value) => (dispatch) => { + dispatch({type: UPDATE_ASSET, id, property, value}); +}; + +export const fetchAssets = (skip, limit, search, sort) => (dispatch) => { + dispatch({type: FETCH_ASSETS, skip, limit, search, sort}); +}; diff --git a/client/coral-admin/src/constants/assets.js b/client/coral-admin/src/constants/assets.js index 59b76390f..b13534726 100644 --- a/client/coral-admin/src/constants/assets.js +++ b/client/coral-admin/src/constants/assets.js @@ -1 +1,4 @@ -export const ASSETS_FETCH = 'ASSETS_FETCH'; +export const FETCH_ASSETS = 'FETCH_ASSETS'; +export const FETCH_ASSETS_SUCCESS = 'FETCH_ASSETS_SUCCESS'; +export const FETCH_ASSETS_FAILED = 'FETCH_ASSETS_FAILED'; +export const UPDATE_ASSET = 'UPDATE_ASSET'; diff --git a/client/coral-admin/src/containers/Streams/Streams.css b/client/coral-admin/src/containers/Streams/Streams.css index 5ad875b35..c64de7974 100644 --- a/client/coral-admin/src/containers/Streams/Streams.css +++ b/client/coral-admin/src/containers/Streams/Streams.css @@ -54,6 +54,30 @@ 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 index 7b1778012..078f7c98a 100644 --- a/client/coral-admin/src/containers/Streams/Streams.js +++ b/client/coral-admin/src/containers/Streams/Streams.js @@ -2,7 +2,7 @@ import React, {Component} from 'react'; import styles from './Streams.css'; import {connect} from 'react-redux'; import I18n from 'coral-framework/modules/i18n/i18n'; -import {ASSETS_FETCH} from '../../constants/assets'; +import {fetchAssets, updateAsset} from '../../actions/assets'; import translations from '../../translations.json'; import { RadioGroup, @@ -17,7 +17,8 @@ class Streams extends Component { state = { searchTerm: '', sortBy: 'newest', - statusFilter: 'all' + statusFilter: 'all', + statusMenus: {} } componentDidMount () { @@ -33,9 +34,40 @@ class Streams extends Component { return `${d.getMonth() + 1}/${d.getDate()}/${d.getFullYear()}`; } - renderStatus = (closedAt) => { - const status = closedAt && new Date(closedAt) < Date.now ? lang.t('streams.closed') : lang.t('streams.open'); - return
    {status}
    ; + onStatusClick = (closeStream, id, statusMenuOpen) => () => { + if (statusMenuOpen) { + this.setState(prev => { + prev.statusMenus[id] = false; + return prev; + }); + this.props.updateAsset(id, 'closedAt', closeStream ? Date.now() : null); + } else { + this.setState(prev => { + prev.statusMenus[id] = true; + return prev; + }); + } + } + + renderStatus = (closedAt, id) => { + const closed = closedAt && new Date(closedAt) < 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')} +
    + } +
    ; } render () { @@ -101,14 +133,11 @@ const mapStateToProps = ({assets}) => { }; const mapDispatchToProps = (dispatch) => { return { - fetchAssets: (skip, limit, search, sort) => { - dispatch({ - type: ASSETS_FETCH, - skip, - limit, - search, - sort - }); + fetchAssets: (...args) => { + dispatch(fetchAssets.apply(this, args)); + }, + updateAsset: (...args) => { + dispatch(updateAsset.apply(this, args)); } }; }; diff --git a/client/coral-admin/src/reducers/assets.js b/client/coral-admin/src/reducers/assets.js index bdd1bcaf9..a1ca4c831 100644 --- a/client/coral-admin/src/reducers/assets.js +++ b/client/coral-admin/src/reducers/assets.js @@ -1,4 +1,5 @@ import {Map, List, fromJS} from 'immutable'; +import {FETCH_ASSETS_SUCCESS} from '../constants/assets'; const initialState = Map({ byId: Map(), @@ -7,7 +8,7 @@ const initialState = Map({ export default (state = initialState, action) => { switch (action.type) { - case 'ASSETS_FETCH_SUCCESS': return replaceAssets(action, state); + case FETCH_ASSETS_SUCCESS: return replaceAssets(action, state); default: return state; } }; diff --git a/client/coral-admin/src/services/talk-adapter.js b/client/coral-admin/src/services/talk-adapter.js index 6dc4d5827..1b824c13e 100644 --- a/client/coral-admin/src/services/talk-adapter.js +++ b/client/coral-admin/src/services/talk-adapter.js @@ -22,7 +22,7 @@ export default store => next => action => { return createComment(store, action.name, action.body); case 'USER_BAN': return userStatusUpdate(store, action.status, action.userId, action.commentId); - case 'ASSETS_FETCH': + case 'FETCH_ASSETS': return fetchAssets(store, action); } }; @@ -97,9 +97,9 @@ const fetchAssets = (store, action) => { return coralApi(`/assets?skip=${skip}&limit=${limit}&search=${search}`) .then(({result, count}) => /* Post comments and users to redux store. Actions will be posted when they are needed. */ - store.dispatch({type: 'ASSETS_FETCH_SUCCESS', + store.dispatch({type: 'FETCH_ASSETS_SUCCESS', assets: result, count })) - .catch(error => store.dispatch({type: 'ASSETS_FETCH_FAILED', error})); + .catch(error => store.dispatch({type: 'FETCH_ASSETS_FAILED', error})); }; From c7b232f0b3e89aafbe64531826138a323479581e Mon Sep 17 00:00:00 2001 From: David Jay Date: Thu, 15 Dec 2016 13:10:23 -0500 Subject: [PATCH 11/54] Adding action to upate asset state. --- client/coral-admin/src/actions/assets.js | 6 +- client/coral-admin/src/constants/assets.js | 4 +- .../coral-admin/src/services/talk-adapter.js | 28 ++++++- tests/client/coral-admin/reducers/assets.js | 4 +- .../coral-admin/services/talk-adapter.js | 73 ++++++++++++++----- 5 files changed, 87 insertions(+), 28 deletions(-) diff --git a/client/coral-admin/src/actions/assets.js b/client/coral-admin/src/actions/assets.js index 1e5f2716d..5918d1df8 100644 --- a/client/coral-admin/src/actions/assets.js +++ b/client/coral-admin/src/actions/assets.js @@ -1,11 +1,11 @@ -import {FETCH_ASSETS, UPDATE_ASSET} from '../constants/assets'; +import {FETCH_ASSETS, UPDATE_ASSET_STATE} from '../constants/assets'; /** * Action disptacher related to assets */ -export const updateAsset = (id, property, value) => (dispatch) => { - dispatch({type: UPDATE_ASSET, id, property, value}); +export const updateAssetState = (id, property, value) => (dispatch) => { + dispatch({type: UPDATE_ASSET_STATE, id, property, value}); }; export const fetchAssets = (skip, limit, search, sort) => (dispatch) => { diff --git a/client/coral-admin/src/constants/assets.js b/client/coral-admin/src/constants/assets.js index b13534726..8927b351d 100644 --- a/client/coral-admin/src/constants/assets.js +++ b/client/coral-admin/src/constants/assets.js @@ -1,4 +1,6 @@ export const FETCH_ASSETS = 'FETCH_ASSETS'; export const FETCH_ASSETS_SUCCESS = 'FETCH_ASSETS_SUCCESS'; export const FETCH_ASSETS_FAILED = 'FETCH_ASSETS_FAILED'; -export const UPDATE_ASSET = 'UPDATE_ASSET'; +export const UPDATE_ASSET_STATE = 'UPDATE_ASSET_STATE'; +export const UPDATE_ASSET_STATE_SUCCESS = 'UPDATE_ASSET_STATE_SUCCESS'; +export const UPDATE_ASSET_STATE_FAILED = 'UPDATE_ASSET_STATE_FAILED'; diff --git a/client/coral-admin/src/services/talk-adapter.js b/client/coral-admin/src/services/talk-adapter.js index 1b824c13e..dea4d7cb2 100644 --- a/client/coral-admin/src/services/talk-adapter.js +++ b/client/coral-admin/src/services/talk-adapter.js @@ -1,5 +1,12 @@ import coralApi from '../../../coral-framework/helpers/response'; - +import { + FETCH_ASSETS, + FETCH_ASSETS_FAILED, + FETCH_ASSETS_SUCCESS, + UPDATE_ASSET_STATE, + UPDATE_ASSET_STATE_SUCCESS, + UPDATE_ASSET_STATE_FAILED, +} from '../constants/assets'; /** * The adapter is a redux middleware that interecepts the actions that need * to interface with the backend, do the job and return the results. @@ -22,8 +29,10 @@ export default store => next => action => { return createComment(store, action.name, action.body); case 'USER_BAN': return userStatusUpdate(store, action.status, action.userId, action.commentId); - case 'FETCH_ASSETS': + case FETCH_ASSETS: return fetchAssets(store, action); + case UPDATE_ASSET_STATE: + return updateAssetState(store, action); } }; @@ -97,9 +106,20 @@ const fetchAssets = (store, action) => { return coralApi(`/assets?skip=${skip}&limit=${limit}&search=${search}`) .then(({result, count}) => /* Post comments and users to redux store. Actions will be posted when they are needed. */ - store.dispatch({type: 'FETCH_ASSETS_SUCCESS', + store.dispatch({type: FETCH_ASSETS_SUCCESS, assets: result, count })) - .catch(error => store.dispatch({type: 'FETCH_ASSETS_FAILED', error})); + .catch(error => store.dispatch({type: FETCH_ASSETS_FAILED, error})); +}; + +// Update an asset state +// Get comments to fill each of the three lists on the mod queue +const updateAssetState = (store, action) => { + const {id, closedAt} = action; + return coralApi(`/assets/${id}/status`, {method: 'PUT', body: {closedAt}}) + .then(() => + /* Post comments and users to redux store. Actions will be posted when they are needed. */ + store.dispatch({type: UPDATE_ASSET_STATE_SUCCESS, closedAt})) + .catch(error => store.dispatch({type: UPDATE_ASSET_STATE_FAILED, error})); }; diff --git a/tests/client/coral-admin/reducers/assets.js b/tests/client/coral-admin/reducers/assets.js index cf3e0dac4..2a3c15b45 100644 --- a/tests/client/coral-admin/reducers/assets.js +++ b/tests/client/coral-admin/reducers/assets.js @@ -3,10 +3,10 @@ import {expect} from 'chai'; import assetsReducer from '../../../../client/coral-admin/src/reducers/assets'; describe ('assetsReducer', () => { - describe('ASSETS_FETCH_SUCCESS', () => { + describe('FETCH_ASSETS_SUCCESS', () => { it('should replace the existing assets', () => { const action = { - type: 'ASSETS_FETCH_SUCCESS', + type: 'FETCH_ASSETS_SUCCESS', count: 200, assets: [ { diff --git a/tests/client/coral-admin/services/talk-adapter.js b/tests/client/coral-admin/services/talk-adapter.js index 27ae8efcb..b30ae3905 100644 --- a/tests/client/coral-admin/services/talk-adapter.js +++ b/tests/client/coral-admin/services/talk-adapter.js @@ -12,30 +12,30 @@ const mockStore = configureStore(); describe('talk-adapter.js', () => { let store; + const assets = [ + { + url: 'http://test.com', + id: '123', + status: 'closed' + }, + { + url: 'http://test.org', + id: '456', + status: 'open' + } + ]; + beforeEach(() => { store = mockStore(new Map({})); fetchMock.restore(); }); - describe('ASSETS_FETCH', () => { - - const assets = [ - { - url: 'http://test.com', - id: '123', - status: 'closed' - }, - { - url: 'http://test.org', - id: '456', - status: 'open' - } - ]; + describe('FETCH_ASSETS', () => { it('should fetch a list of assets', () => { const action = { - type: 'ASSETS_FETCH', + type: 'FETCH_ASSETS', skip: 2, limit: 20, search: '' @@ -48,7 +48,7 @@ describe('talk-adapter.js', () => { return adapter(store)(()=>{})(action) .then(() => { - expect(store.getActions()[0]).to.have.property('type', 'ASSETS_FETCH_SUCCESS'); + expect(store.getActions()[0]).to.have.property('type', 'FETCH_ASSETS_SUCCESS'); expect(store.getActions()[0]).to.have.property('count', 2); expect(store.getActions()[0]).to.have.property('assets'). and.to.deep.equal(assets); @@ -58,7 +58,7 @@ describe('talk-adapter.js', () => { it('should return an error appropriatly', () => { const action = { - type: 'ASSETS_FETCH', + type: 'FETCH_ASSETS', skip: 2, limit: 20, search: '' @@ -68,7 +68,44 @@ describe('talk-adapter.js', () => { return adapter(store)(()=>{})(action) .then(() => { - expect(store.getActions()[0]).to.have.property('type', 'ASSETS_FETCH_FAILED'); + expect(store.getActions()[0]).to.have.property('type', 'FETCH_ASSETS_FAILED'); + }); + }); + }); + + describe('UPDATE_ASSET_STATE', () => { + + it('should update an asset', () => { + const action = { + type: 'UPDATE_ASSET_STATE', + id: '123', + property: 'closedAt', + value: Date.now() + }; + + fetchMock.put('*', JSON.stringify(assets[0])); + + return adapter(store)(()=>{})(action) + .then(() => { + expect(store.getActions()[0]).to.have.property('type', 'UPDATE_ASSET_STATE_SUCCESS'); + }); + + }); + + it('should return an error appropriately', () => { + + const action = { + type: 'UPDATE_ASSET_STATE', + id: '123', + property: 'closedAt', + value: Date.now() + }; + + fetchMock.put('*', 404); + + return adapter(store)(()=>{})(action) + .then(() => { + expect(store.getActions()[0]).to.have.property('type', 'UPDATE_ASSET_STATE_FAILED'); }); }); }); From 328eaad8e521a35e7fce4dfa1207f6e6adbf8937 Mon Sep 17 00:00:00 2001 From: David Jay Date: Thu, 15 Dec 2016 14:05:10 -0500 Subject: [PATCH 12/54] Adding update state to reducer. --- client/coral-admin/src/reducers/assets.js | 7 +++-- tests/client/coral-admin/reducers/assets.js | 29 +++++++++++++++++++-- 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/client/coral-admin/src/reducers/assets.js b/client/coral-admin/src/reducers/assets.js index a1ca4c831..f18aaec33 100644 --- a/client/coral-admin/src/reducers/assets.js +++ b/client/coral-admin/src/reducers/assets.js @@ -1,5 +1,5 @@ import {Map, List, fromJS} from 'immutable'; -import {FETCH_ASSETS_SUCCESS} from '../constants/assets'; +import {FETCH_ASSETS_SUCCESS, UPDATE_ASSET_STATE} from '../constants/assets'; const initialState = Map({ byId: Map(), @@ -8,7 +8,10 @@ const initialState = Map({ export default (state = initialState, action) => { switch (action.type) { - case FETCH_ASSETS_SUCCESS: return replaceAssets(action, state); + case FETCH_ASSETS_SUCCESS: + return replaceAssets(action, state); + case UPDATE_ASSET_STATE: + return state.setIn(['byId', action.id, 'closedAt'], action.closedAt); default: return state; } }; diff --git a/tests/client/coral-admin/reducers/assets.js b/tests/client/coral-admin/reducers/assets.js index 2a3c15b45..60972f47a 100644 --- a/tests/client/coral-admin/reducers/assets.js +++ b/tests/client/coral-admin/reducers/assets.js @@ -1,4 +1,4 @@ -import {Map} from 'immutable'; +import {Map, fromJS} from 'immutable'; import {expect} from 'chai'; import assetsReducer from '../../../../client/coral-admin/src/reducers/assets'; @@ -23,7 +23,6 @@ describe ('assetsReducer', () => { }; const store = new Map({}); const result = assetsReducer(store, action); - console.log(result.getIn(['byId', '123']).toJS()); expect(result.getIn(['byId', '123']).toJS()).to.deep.equal({ url: 'http://test.com', closedAt: 'tomorrow', @@ -36,4 +35,30 @@ describe ('assetsReducer', () => { expect(result.getIn(['count'])).to.equal(200); }); }); + + describe('UPDATE_ASSET_STATE', () => { + it('should update the state of a particular asset', () => { + const action = { + type: 'UPDATE_ASSET_STATE', + id: '123', + closedAt: null + }; + const store = new fromJS({ + byId: { + '123': { + id: '123', + url: 'http://test.com', + closedAt: Date.now() + }, + '456': { + id: '456', + url: 'http://test2.com', + closedAt: 'thursday' + } + } + }); + const result = assetsReducer(store, action); + expect(result.getIn(['byId', '123', 'closedAt'])).to.equal.null; + }); + }); }); From 1154b65c9166b86370e5e070faeb859cad606fd0 Mon Sep 17 00:00:00 2001 From: David Jay Date: Thu, 15 Dec 2016 14:19:44 -0500 Subject: [PATCH 13/54] Updating state from menu. --- client/coral-admin/src/actions/assets.js | 4 ++-- client/coral-admin/src/containers/Streams/Streams.js | 12 ++++++------ client/coral-admin/src/services/talk-adapter.js | 2 +- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/client/coral-admin/src/actions/assets.js b/client/coral-admin/src/actions/assets.js index 5918d1df8..b13454a21 100644 --- a/client/coral-admin/src/actions/assets.js +++ b/client/coral-admin/src/actions/assets.js @@ -4,8 +4,8 @@ import {FETCH_ASSETS, UPDATE_ASSET_STATE} from '../constants/assets'; * Action disptacher related to assets */ -export const updateAssetState = (id, property, value) => (dispatch) => { - dispatch({type: UPDATE_ASSET_STATE, id, property, value}); +export const updateAssetState = (id, closedAt) => (dispatch) => { + dispatch({type: UPDATE_ASSET_STATE, id, closedAt}); }; export const fetchAssets = (skip, limit, search, sort) => (dispatch) => { diff --git a/client/coral-admin/src/containers/Streams/Streams.js b/client/coral-admin/src/containers/Streams/Streams.js index 078f7c98a..53db75c89 100644 --- a/client/coral-admin/src/containers/Streams/Streams.js +++ b/client/coral-admin/src/containers/Streams/Streams.js @@ -2,7 +2,7 @@ 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, updateAsset} from '../../actions/assets'; +import {fetchAssets, updateAssetState} from '../../actions/assets'; import translations from '../../translations.json'; import { RadioGroup, @@ -40,7 +40,7 @@ class Streams extends Component { prev.statusMenus[id] = false; return prev; }); - this.props.updateAsset(id, 'closedAt', closeStream ? Date.now() : null); + this.props.updateAssetState(id, closeStream ? Date.now() : null); } else { this.setState(prev => { prev.statusMenus[id] = true; @@ -49,8 +49,8 @@ class Streams extends Component { } } - renderStatus = (closedAt, id) => { - const closed = closedAt && new Date(closedAt) < Date.now; + renderStatus = (closedAt, {id}) => { + const closed = closedAt && new Date(closedAt).getTime() < Date.now(); const statusMenuOpen = this.state.statusMenus[id]; return
    { fetchAssets: (...args) => { dispatch(fetchAssets.apply(this, args)); }, - updateAsset: (...args) => { - dispatch(updateAsset.apply(this, args)); + updateAssetState: (...args) => { + dispatch(updateAssetState.apply(this, args)); } }; }; diff --git a/client/coral-admin/src/services/talk-adapter.js b/client/coral-admin/src/services/talk-adapter.js index dea4d7cb2..67835e30d 100644 --- a/client/coral-admin/src/services/talk-adapter.js +++ b/client/coral-admin/src/services/talk-adapter.js @@ -120,6 +120,6 @@ const updateAssetState = (store, action) => { return coralApi(`/assets/${id}/status`, {method: 'PUT', body: {closedAt}}) .then(() => /* Post comments and users to redux store. Actions will be posted when they are needed. */ - store.dispatch({type: UPDATE_ASSET_STATE_SUCCESS, closedAt})) + store.dispatch({type: UPDATE_ASSET_STATE_SUCCESS})) .catch(error => store.dispatch({type: UPDATE_ASSET_STATE_FAILED, error})); }; From d4880ff183423306cf925e12d8f9816f69a7f00c Mon Sep 17 00:00:00 2001 From: David Jay Date: Fri, 16 Dec 2016 15:35:15 -0500 Subject: [PATCH 14/54] Adding asset sorting to endpoint. --- .../src/containers/Streams/Streams.js | 13 ++++--- routes/api/assets/index.js | 28 +++++++++++--- tests/routes/api/assets/index.js | 38 ++++++++++++++++++- 3 files changed, 67 insertions(+), 12 deletions(-) diff --git a/client/coral-admin/src/containers/Streams/Streams.js b/client/coral-admin/src/containers/Streams/Streams.js index 53db75c89..5d8508f33 100644 --- a/client/coral-admin/src/containers/Streams/Streams.js +++ b/client/coral-admin/src/containers/Streams/Streams.js @@ -12,21 +12,24 @@ import { TableHeader } from 'react-mdl'; +const limit = 25; + class Streams extends Component { state = { - searchTerm: '', - sortBy: 'newest', + search: '', + sort: 'desc', statusFilter: 'all', statusMenus: {} } componentDidMount () { - this.props.fetchAssets(0, 25, '', 'desc'); + this.props.fetchAssets(0, limit, '', this.state.sortBy); } onSettingChange = (setting) => (e) => { this.setState({[setting]: e.target.value}); + this.props.fetchAssets(0, limit, this.state.search, this.state.sort); } renderDate = (date) => { @@ -104,8 +107,8 @@ class Streams extends Component { value={sortBy} childContainer='div' onChange={this.onSettingChange('sortBy')}> - {lang.t('streams.newest')} - {lang.t('streams.oldest')} + {lang.t('streams.newest')} + {lang.t('streams.oldest')}
    diff --git a/routes/api/assets/index.js b/routes/api/assets/index.js index a602d7fd0..a661d64e8 100644 --- a/routes/api/assets/index.js +++ b/routes/api/assets/index.js @@ -12,22 +12,40 @@ router.get('/', (req, res, next) => { skip = 0, sort = 'asc', field = 'created_at', + filter = 'all', search = '' } = req.query; + const assets = (filter, search) => { + switch(filter) { + case 'open': + return Asset.search(search) + .find({ + $or: [ + {closedAt: null}, + {closedAt: {$gt: Date.now()}} + ] + }); + case 'closed': + return Asset.search(search) + .find({ + closedAt: {$lt: Date.now()} + }); + default: + return Asset.search(search); + } + }; + // Find all the assets. Promise.all([ - Asset - .search(search) + assets(filter, search) .sort({[field]: (sort === 'asc') ? 1 : -1}) .skip(parseInt(skip)) .limit(parseInt(limit)), - Asset - .search(search) + assets(filter, search) .count() ]) .then(([result, count]) => { - // Send back the asset data. res.json({ result, diff --git a/tests/routes/api/assets/index.js b/tests/routes/api/assets/index.js index 8e94c81fc..da011bf7b 100644 --- a/tests/routes/api/assets/index.js +++ b/tests/routes/api/assets/index.js @@ -18,12 +18,14 @@ describe('/api/v1/assets', () => { url: 'https://coralproject.net/news/asset1', title: 'Asset 1', description: 'term1', - id: '1' + id: '1', + closedAt: Date.now() }, { url: 'https://coralproject.net/news/asset2', title: 'Asset 2', - description: 'term2' + description: 'term2', + closedAt: null } ]); }); @@ -81,6 +83,38 @@ describe('/api/v1/assets', () => { }); }); + it('should return only closed assets', () => { + return chai.request(app) + .get('/api/v1/assets?filter=closed') + .set(passport.inject({roles: ['admin']})) + .then((res) => { + const body = res.body; + + expect(body).to.have.property('count', 1); + expect(body).to.have.property('result'); + + const assets = body.result; + + expect(assets[0]).to.have.property('title', 'Asset 1'); + }); + }); + + it('should return only opened assets', () => { + return chai.request(app) + .get('/api/v1/assets?filter=open') + .set(passport.inject({roles: ['admin']})) + .then((res) => { + const body = res.body; + + expect(body).to.have.property('count', 1); + expect(body).to.have.property('result'); + + const assets = body.result; + + expect(assets[0]).to.have.property('title', 'Asset 2'); + }); + }); + }); describe('#put', () => { From 979b54166bbb3060b3944d612f4bf66e332a6ebc Mon Sep 17 00:00:00 2001 From: David Jay Date: Fri, 16 Dec 2016 16:24:32 -0500 Subject: [PATCH 15/54] Updating search with delay. --- client/coral-admin/src/actions/assets.js | 4 +-- .../src/containers/Streams/Streams.js | 35 +++++++++++++------ .../coral-admin/src/services/talk-adapter.js | 4 +-- 3 files changed, 29 insertions(+), 14 deletions(-) diff --git a/client/coral-admin/src/actions/assets.js b/client/coral-admin/src/actions/assets.js index b13454a21..df2e1bd60 100644 --- a/client/coral-admin/src/actions/assets.js +++ b/client/coral-admin/src/actions/assets.js @@ -8,6 +8,6 @@ export const updateAssetState = (id, closedAt) => (dispatch) => { dispatch({type: UPDATE_ASSET_STATE, id, closedAt}); }; -export const fetchAssets = (skip, limit, search, sort) => (dispatch) => { - dispatch({type: FETCH_ASSETS, skip, limit, search, sort}); +export const fetchAssets = (skip, limit, search, sort, filter) => (dispatch) => { + dispatch({type: FETCH_ASSETS, skip, limit, search, sort, filter}); }; diff --git a/client/coral-admin/src/containers/Streams/Streams.js b/client/coral-admin/src/containers/Streams/Streams.js index 5d8508f33..58c1c9b61 100644 --- a/client/coral-admin/src/containers/Streams/Streams.js +++ b/client/coral-admin/src/containers/Streams/Streams.js @@ -19,8 +19,9 @@ class Streams extends Component { state = { search: '', sort: 'desc', - statusFilter: 'all', - statusMenus: {} + filter: 'all', + statusMenus: {}, + timer: null } componentDidMount () { @@ -28,8 +29,22 @@ class Streams extends Component { } onSettingChange = (setting) => (e) => { + let options = this.state; this.setState({[setting]: e.target.value}); - this.props.fetchAssets(0, limit, this.state.search, this.state.sort); + options[setting] = e.target.value; + this.props.fetchAssets(0, limit, options.search, options.sort, options.filter); + } + + onSearchChange = (e) => { + this.setState({search: e.target.value}); + this.setState((prevState) => { + clearTimeout(prevState.timer); + const fetchAssets = this.props.fetchAssets; + prevState.timer = setTimeout(() => { + fetchAssets(0, limit, this.state.search, this.state.sort, this.state.filter); + }, 350); + return prevState; + }); } renderDate = (date) => { @@ -74,7 +89,7 @@ class Streams extends Component { } render () { - const {searchTerm, sortBy, statusFilter} = this.state; + const {search, sort, filter} = this.state; const {assets} = this.props; return
    @@ -84,9 +99,9 @@ class Streams extends Component {
    @@ -94,9 +109,9 @@ class Streams extends Component {
    {lang.t('streams.stream-status')}
    + onChange={this.onSettingChange('filter')}> {lang.t('streams.all')} {lang.t('streams.open')} {lang.t('streams.closed')} @@ -104,9 +119,9 @@ class Streams extends Component {
    {lang.t('streams.sort-by')}
    + onChange={this.onSettingChange('sort')}> {lang.t('streams.newest')} {lang.t('streams.oldest')} diff --git a/client/coral-admin/src/services/talk-adapter.js b/client/coral-admin/src/services/talk-adapter.js index 67835e30d..fcc334953 100644 --- a/client/coral-admin/src/services/talk-adapter.js +++ b/client/coral-admin/src/services/talk-adapter.js @@ -102,8 +102,8 @@ const userStatusUpdate = (store, status, userId, commentId) => { // Fetch a page of assets // Get comments to fill each of the three lists on the mod queue const fetchAssets = (store, action) => { - const {skip, limit, search} = action; - return coralApi(`/assets?skip=${skip}&limit=${limit}&search=${search}`) + const {skip, limit, sort, search, filter} = action; + return coralApi(`/assets?skip=${skip}&limit=${limit}&sort=${sort}&search=${search}&filter=${filter}`) .then(({result, count}) => /* Post comments and users to redux store. Actions will be posted when they are needed. */ store.dispatch({type: FETCH_ASSETS_SUCCESS, From 0d2d6b52a5d0468d020f672745844dfacb474ed8 Mon Sep 17 00:00:00 2001 From: David Jay Date: Fri, 16 Dec 2016 17:07:59 -0500 Subject: [PATCH 16/54] Adding pagination. --- .../src/containers/Community/Community.js | 2 +- .../src/containers/Streams/Streams.js | 19 +++++++++++++++++-- .../components}/Pager.css | 0 .../components}/Pager.js | 0 4 files changed, 18 insertions(+), 3 deletions(-) rename client/{coral-admin/src/containers/Community => coral-ui/components}/Pager.css (100%) rename client/{coral-admin/src/containers/Community => coral-ui/components}/Pager.js (100%) 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.js b/client/coral-admin/src/containers/Streams/Streams.js index 58c1c9b61..4841d2a1b 100644 --- a/client/coral-admin/src/containers/Streams/Streams.js +++ b/client/coral-admin/src/containers/Streams/Streams.js @@ -11,8 +11,9 @@ import { DataTable, TableHeader } from 'react-mdl'; +import Pager from 'coral-ui/components/Pager'; -const limit = 25; +const limit = 10; class Streams extends Component { @@ -21,7 +22,8 @@ class Streams extends Component { sort: 'desc', filter: 'all', statusMenus: {}, - timer: null + timer: null, + page: 0 } componentDidMount () { @@ -88,10 +90,18 @@ class Streams extends Component {
    ; } + 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; + console.log(assets); + return
    @@ -139,6 +149,11 @@ class Streams extends Component { {lang.t('streams.status')} +
    ; } diff --git a/client/coral-admin/src/containers/Community/Pager.css b/client/coral-ui/components/Pager.css similarity index 100% rename from client/coral-admin/src/containers/Community/Pager.css rename to client/coral-ui/components/Pager.css diff --git a/client/coral-admin/src/containers/Community/Pager.js b/client/coral-ui/components/Pager.js similarity index 100% rename from client/coral-admin/src/containers/Community/Pager.js rename to client/coral-ui/components/Pager.js From c34bf5036e10348796485af8643a52004a176437 Mon Sep 17 00:00:00 2001 From: David Jay Date: Fri, 16 Dec 2016 17:20:31 -0500 Subject: [PATCH 17/54] Updating page count. --- client/coral-admin/src/containers/Streams/Streams.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/coral-admin/src/containers/Streams/Streams.js b/client/coral-admin/src/containers/Streams/Streams.js index 4841d2a1b..55fe10fc2 100644 --- a/client/coral-admin/src/containers/Streams/Streams.js +++ b/client/coral-admin/src/containers/Streams/Streams.js @@ -13,7 +13,7 @@ import { } from 'react-mdl'; import Pager from 'coral-ui/components/Pager'; -const limit = 10; +const limit = 25; class Streams extends Component { From 871836f636bcba97805b302b3034394a384c729c Mon Sep 17 00:00:00 2001 From: David Jay Date: Fri, 16 Dec 2016 18:19:05 -0500 Subject: [PATCH 18/54] Adding description field to flags. --- client/coral-embed-stream/style/default.css | 2 ++ client/coral-plugin-flags/FlagButton.js | 27 ++++++++++----------- client/coral-plugin-flags/FlagComment.js | 3 ++- client/coral-plugin-flags/translations.json | 6 +++-- models/action.js | 1 + 5 files changed, 22 insertions(+), 17 deletions(-) diff --git a/client/coral-embed-stream/style/default.css b/client/coral-embed-stream/style/default.css index 463c8dc68..bc5ce27bc 100644 --- a/client/coral-embed-stream/style/default.css +++ b/client/coral-embed-stream/style/default.css @@ -241,6 +241,7 @@ hr { .coral-plugin-flags-popup-radio-label { margin:5px; font-size: 14px; + font-weight: 700; } .coral-plugin-flags-popup-counter { @@ -256,6 +257,7 @@ hr { .coral-plugin-flags-other-text { margin-left: 20px; + margin-top: 5px; width: 75%; } diff --git a/client/coral-plugin-flags/FlagButton.js b/client/coral-plugin-flags/FlagButton.js index d0b9cb6b4..57b3d1946 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: '', + description: '', 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, detail, step, description, 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) { @@ -42,7 +41,6 @@ class FlagButton extends Component { // If itemType and detail are both set, post the action if (itemType && detail && !posted) { // Set the text from the "other" field if it exists. - const updatedDetail = otherText || detail; let item_id; switch(itemType) { case 'comments': @@ -55,7 +53,8 @@ class FlagButton extends Component { const action = { action_type: 'flag', field, - detail: updatedDetail + detail, + description }; postAction(item_id, itemType, action) .then((action) => { @@ -92,7 +91,7 @@ class FlagButton extends Component { } onOtherTextChange = (e) => { - this.setState({otherText: e.target.value}); + this.setState({description: e.target.value}); } handleClickOutside () { @@ -141,16 +140,16 @@ class FlagButton extends Component { ) } { - this.state.showOther &&
    - +
    +