show stories in dropdown

This commit is contained in:
Riley Davis
2017-06-09 14:10:21 -06:00
parent 06da4d7d26
commit de665b3a0d
12 changed files with 233 additions and 4 deletions
@@ -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});
@@ -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';
@@ -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 :
@@ -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} />
)}
<StorySearch />
</div>
);
}
@@ -0,0 +1,22 @@
import React, {PropTypes} from 'react';
import styles from './StorySearch.css';
const Story = ({author, title, createdAt, open}) => {
return (
<li className={styles.story}>
<p className={styles.title}>{title}</p>
<p className={styles.meta}>
<span className={styles.author}>By {author}</span><span className={styles.createdAt}>{createdAt}</span><span className={styles.status}>{open ? 'Open' : 'Closed'}</span>
</p>
</li>
);
};
Story.propTypes = {
author: PropTypes.string.isRequired,
title: PropTypes.string.isRequired,
createdAt: PropTypes.string.isRequired,
open: PropTypes.bool.isRequired
};
export default Story;
@@ -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;
}
@@ -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 (
<div className={styles.container}>
<div className={styles.positionShim}>
<div className={styles.headInput}>
<input className={styles.searchInput} onChange={props.storySearchChange} />
<Button cStyle='facebook'>Search</Button>
</div>
<div className={styles.results}>
<p className={styles.cta}>Moderate comments on All Stories</p>
<ul className={styles.storyList}>
{
loading
? <Spinner />
: assets.map((story, i) => {
const storyOpen = story.closedAt === null || new Date(story.closedAt) > new Date();
return <Story
key={i}
title={story.title}
createdAt={new Date(story.created_at).toISOString()}
open={storyOpen}
author={story.author} />;
})
}
</ul>
</div>
</div>
</div>
);
};
StorySearch.propTypes = {
storySearchChange: PropTypes.func.isRequired
};
export default StorySearch;
@@ -209,7 +209,7 @@ const withModQueueQuery = withQuery(gql`
}) {
...CoralAdmin_Moderation_CommentConnection
}
assets: assets {
assets: assets(query: {}) {
id
title
url
@@ -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 <StorySearch searchChange={this.searchChange} {...this.props} />;
}
}
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);
+12
View File
@@ -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({}))
+4 -2
View File
@@ -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) {
+11 -1
View File
@@ -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