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