From de665b3a0d85b8ecce41e9ef7e9f6fc0e4d106b4 Mon Sep 17 00:00:00 2001 From: Riley Davis Date: Fri, 9 Jun 2017 14:10:21 -0600 Subject: [PATCH] show stories in dropdown --- client/coral-admin/src/actions/moderation.js | 2 + .../coral-admin/src/constants/moderation.js | 1 + client/coral-admin/src/reducers/moderation.js | 2 + .../Moderation/components/Moderation.js | 2 + .../src/routes/Moderation/components/Story.js | 22 ++++++ .../Moderation/components/StorySearch.css | 69 +++++++++++++++++++ .../Moderation/components/StorySearch.js | 47 +++++++++++++ .../Moderation/containers/Moderation.js | 2 +- .../Moderation/containers/StorySearch.js | 60 ++++++++++++++++ graph/loaders/assets.js | 12 ++++ graph/resolvers/root_query.js | 6 +- graph/typeDefs.graphql | 12 +++- 12 files changed, 233 insertions(+), 4 deletions(-) create mode 100644 client/coral-admin/src/routes/Moderation/components/Story.js create mode 100644 client/coral-admin/src/routes/Moderation/components/StorySearch.css create mode 100644 client/coral-admin/src/routes/Moderation/components/StorySearch.js create mode 100644 client/coral-admin/src/routes/Moderation/containers/StorySearch.js diff --git a/client/coral-admin/src/actions/moderation.js b/client/coral-admin/src/actions/moderation.js index 9f1c40c94..0833c856e 100644 --- a/client/coral-admin/src/actions/moderation.js +++ b/client/coral-admin/src/actions/moderation.js @@ -53,3 +53,5 @@ export const toggleSelectCommentInUserDetail = (id, active) => { }; export const toggleStorySearch = (active) => ({type: active ? actions.SHOW_STORY_SEARCH : actions.HIDE_STORY_SEARCH}); + +export const storySearchChange = (e) => ({type: actions.CHANGE_ASSET_SEARCH_STRING, value: e.target.value}); diff --git a/client/coral-admin/src/constants/moderation.js b/client/coral-admin/src/constants/moderation.js index 29cf03143..37f048e23 100644 --- a/client/coral-admin/src/constants/moderation.js +++ b/client/coral-admin/src/constants/moderation.js @@ -14,3 +14,4 @@ export const UNSELECT_USER_DETAIL_COMMENT = 'UNSELECT_USER_DETAIL_COMMENT'; export const CLEAR_USER_DETAIL_SELECTIONS = 'CLEAR_USER_DETAIL_SELECTIONS'; export const SHOW_STORY_SEARCH = 'SHOW_STORY_SEARCH'; export const HIDE_STORY_SEARCH = 'HIDE_STORY_SEARCH'; +export const CHANGE_ASSET_SEARCH_STRING = 'CHANGE_ASSET_SEARCH_STRING'; diff --git a/client/coral-admin/src/reducers/moderation.js b/client/coral-admin/src/reducers/moderation.js index 4bab9eda1..349ff517a 100644 --- a/client/coral-admin/src/reducers/moderation.js +++ b/client/coral-admin/src/reducers/moderation.js @@ -86,6 +86,8 @@ export default function moderation (state = initialState, action) { return state.set('storySearchVisible', true); case actions.HIDE_STORY_SEARCH: return state.set('storySearchVisible', false); + case actions.CHANGE_ASSET_SEARCH_STRING: + return state.set('storySearchString', action.value); case actions.SET_SORT_ORDER: return state.set('sortOrder', action.order); default : diff --git a/client/coral-admin/src/routes/Moderation/components/Moderation.js b/client/coral-admin/src/routes/Moderation/components/Moderation.js index f7338cf6b..7582d10d1 100644 --- a/client/coral-admin/src/routes/Moderation/components/Moderation.js +++ b/client/coral-admin/src/routes/Moderation/components/Moderation.js @@ -10,6 +10,7 @@ import ModerationHeader from './ModerationHeader'; import NotFoundAsset from './NotFoundAsset'; import ModerationKeysModal from '../../../components/ModerationKeysModal'; import UserDetail from '../containers/UserDetail'; +import StorySearch from '../containers/StorySearch'; export default class Moderation extends Component { state = { @@ -209,6 +210,7 @@ export default class Moderation extends Component { acceptComment={props.acceptComment} rejectComment={props.rejectComment} /> )} + ); } diff --git a/client/coral-admin/src/routes/Moderation/components/Story.js b/client/coral-admin/src/routes/Moderation/components/Story.js new file mode 100644 index 000000000..ee9d462d3 --- /dev/null +++ b/client/coral-admin/src/routes/Moderation/components/Story.js @@ -0,0 +1,22 @@ +import React, {PropTypes} from 'react'; +import styles from './StorySearch.css'; + +const Story = ({author, title, createdAt, open}) => { + return ( +
  • +

    {title}

    +

    + By {author}{createdAt}{open ? 'Open' : 'Closed'} +

    +
  • + ); +}; + +Story.propTypes = { + author: PropTypes.string.isRequired, + title: PropTypes.string.isRequired, + createdAt: PropTypes.string.isRequired, + open: PropTypes.bool.isRequired +}; + +export default Story; diff --git a/client/coral-admin/src/routes/Moderation/components/StorySearch.css b/client/coral-admin/src/routes/Moderation/components/StorySearch.css new file mode 100644 index 000000000..e5b6e4a93 --- /dev/null +++ b/client/coral-admin/src/routes/Moderation/components/StorySearch.css @@ -0,0 +1,69 @@ +.container { + position: fixed; + background-color: white; + top: 100px; + left: 0; + right: 0; + margin-left: auto; + margin-right: auto; + width: 50%; + box-shadow: 0px 3px 5px 0px rgba(0,0,0,0.15); +} + +.positionShim { + left: -50%; +} + +.headInput { + background-color: #efefef; + padding: 10px 30px; +} + +.searchInput { + width: calc(100% - 100px); + padding: 8px; + height: 100%; + font-size: 16px; + margin-right: 10px; + position: relative; + top: 2px; +} + +.cta { + padding: 15px; + font-weight: bold; + font-size: 14px; + margin: 0; +} + +.storyList { + padding: 10px; + margin: 0; + list-style: none; + border-top: 1px solid #ddd; +} + +.story { + padding: 10px; + border-bottom: 1px solid #ddd; +} + +.title, .meta { + margin: 0; +} + +.author, .createdAt, .status { + display: inline-block; + font-size: .8em; + color: #aaa; +} + +.author { + display: inline-block; + width: 200px; +} + +.createdAt { + display: inline-block; + width: 200px; +} diff --git a/client/coral-admin/src/routes/Moderation/components/StorySearch.js b/client/coral-admin/src/routes/Moderation/components/StorySearch.js new file mode 100644 index 000000000..ce16e5b15 --- /dev/null +++ b/client/coral-admin/src/routes/Moderation/components/StorySearch.js @@ -0,0 +1,47 @@ +import React, {PropTypes} from 'react'; +import styles from './StorySearch.css'; +import {Button, Spinner} from 'coral-ui'; +import Story from './Story'; + +const StorySearch = (props) => { + + const { + root: {assets = []}, + data: {loading} + } = props; + + return ( +
    +
    +
    + + +
    +
    +

    Moderate comments on All Stories

    +
      + { + loading + ? + : assets.map((story, i) => { + const storyOpen = story.closedAt === null || new Date(story.closedAt) > new Date(); + return ; + }) + } +
    +
    +
    +
    + ); +}; + +StorySearch.propTypes = { + storySearchChange: PropTypes.func.isRequired +}; + +export default StorySearch; diff --git a/client/coral-admin/src/routes/Moderation/containers/Moderation.js b/client/coral-admin/src/routes/Moderation/containers/Moderation.js index 31ac05450..24a3d748d 100644 --- a/client/coral-admin/src/routes/Moderation/containers/Moderation.js +++ b/client/coral-admin/src/routes/Moderation/containers/Moderation.js @@ -209,7 +209,7 @@ const withModQueueQuery = withQuery(gql` }) { ...CoralAdmin_Moderation_CommentConnection } - assets: assets { + assets: assets(query: {}) { id title url diff --git a/client/coral-admin/src/routes/Moderation/containers/StorySearch.js b/client/coral-admin/src/routes/Moderation/containers/StorySearch.js new file mode 100644 index 000000000..cfe2a5bea --- /dev/null +++ b/client/coral-admin/src/routes/Moderation/containers/StorySearch.js @@ -0,0 +1,60 @@ +import React from 'react'; +import {connect} from 'react-redux'; +import {bindActionCreators} from 'redux'; +import {compose, gql} from 'react-apollo'; +import StorySearch from '../components/StorySearch'; +import withQuery from 'coral-framework/hocs/withQuery'; +import {storySearchChange} from 'coral-admin/src/actions/moderation'; + +class StorySearchContainer extends React.Component { + searchChange = (e) => { + this.props.storySearchChange(e.target.value); + this.props.data.refetch(); + } + + componentDidUpdate (prevProps) { + if (prevProps.moderation.storySearchString !== this.props.moderation.storySearchString) { + this.props.data.refetch(); + } + } + + render () { + return ; + } +} + +export const withAssetSearchQuery = withQuery(gql` + query SearchStories($value: String = "") { + assets(query: {value: $value}) { + id + title + url + created_at + closedAt + author + } + } +`, { + options: ({moderation: {storySearchString = ''}}) => { + return { + variables: { + value: storySearchString + } + }; + } +}); + +const mapStateToProps = (state) => ({ + moderation: state.moderation.toJS() +}); + +const mapDispatchToProps = (dispatch) => ({ + ...bindActionCreators({ + storySearchChange + }, dispatch) +}); + +export default compose( + connect(mapStateToProps, mapDispatchToProps), + withAssetSearchQuery +)(StorySearchContainer); diff --git a/graph/loaders/assets.js b/graph/loaders/assets.js index 11f77df60..b32b37b60 100644 --- a/graph/loaders/assets.js +++ b/graph/loaders/assets.js @@ -19,6 +19,17 @@ const genAssetsByID = (context, ids) => AssetModel.find({ } }).then(util.singleJoinBy(ids, 'id')); +/** + * [getAssetsByQuery description] + * @param {Object} context the context of the request + * @param {String} value text string to search agains the documents + * @param {Number} limit limit the number of results + * @return {Promise} resolves the assets + */ +const getAssetsByQuery = (context, value, limit) => { + return AssetsService.search(value, null, limit); +}; + /** * This endpoint find or creates an asset at the given url when it is loaded. * @param {Object} context the context of the request @@ -65,6 +76,7 @@ module.exports = (context) => ({ // this operation create a new asset if one isn't found. getByURL: (url) => findOrCreateAssetByURL(context, url), + search: (value, limit) => getAssetsByQuery(context, value, limit), getByID: new DataLoader((ids) => genAssetsByID(context, ids)), getForMetrics: () => getAssetsForMetrics(context), getAll: new util.SingletonResolver(() => AssetModel.find({})) diff --git a/graph/resolvers/root_query.js b/graph/resolvers/root_query.js index e2cd58828..637af9018 100644 --- a/graph/resolvers/root_query.js +++ b/graph/resolvers/root_query.js @@ -6,12 +6,14 @@ const { } = require('../../perms/constants'); const RootQuery = { - assets(_, args, {loaders: {Assets}, user}) { + assets(_, {query}, {loaders: {Assets}, user}) { if (user == null || !user.can(SEARCH_ASSETS)) { return null; } - return Assets.getAll.load(); + const {value = '', limit} = query; + + return Assets.search(value, limit); }, asset(_, query, {loaders: {Assets}}) { if (query.id) { diff --git a/graph/typeDefs.graphql b/graph/typeDefs.graphql index 187e5bba5..2da5dca12 100644 --- a/graph/typeDefs.graphql +++ b/graph/typeDefs.graphql @@ -112,6 +112,16 @@ input UsersQuery { sort: SORT_ORDER = REVERSE_CHRONOLOGICAL } +# AssetsQuery allows teh ability to query assets by specific fields +input AssetsQuery { + + # a search string to match against titles, authors, urls, etc. + value: String = "" + + # Limit the number of results to be returned + limit: Int = 10 +} + ################################################################################ ## Comments ################################################################################ @@ -586,7 +596,7 @@ type RootQuery { comment(id: ID!): Comment # All assets. Requires the `ADMIN` role. - assets: [Asset] + assets(query: AssetsQuery!): [Asset] # Find or create an asset by url, or just find with the ID. asset(id: ID, url: String): Asset