mirror of
https://github.com/wassname/talk.git
synced 2026-07-08 20:17:25 +08:00
show stories in dropdown
This commit is contained in:
@@ -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);
|
||||
@@ -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({}))
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user