mirror of
https://github.com/wassname/talk.git
synced 2026-06-29 01:28:36 +08:00
Merge branch 'master' of github.com:coralproject/talk into action-focus-states
This commit is contained in:
@@ -1,6 +1,5 @@
|
||||
.layout {
|
||||
max-width: 1280px;
|
||||
margin: 0 auto;
|
||||
overflow: hidden;
|
||||
background-color: #FAFAFA;
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import NotFoundAsset from './NotFoundAsset';
|
||||
import ModerationKeysModal from '../../../components/ModerationKeysModal';
|
||||
import UserDetail from '../containers/UserDetail';
|
||||
import StorySearch from '../containers/StorySearch';
|
||||
import {Spinner} from 'coral-ui';
|
||||
|
||||
export default class Moderation extends Component {
|
||||
state = {
|
||||
@@ -102,18 +103,22 @@ export default class Moderation extends Component {
|
||||
}
|
||||
|
||||
render () {
|
||||
const {root, moderation, settings, assets, viewUserDetail, hideUserDetail, ...props} = this.props;
|
||||
const {root, moderation, settings, viewUserDetail, hideUserDetail, ...props} = this.props;
|
||||
const providedAssetId = this.props.params.id;
|
||||
const activeTab = this.props.route.path === ':id' ? 'premod' : this.props.route.path;
|
||||
|
||||
let asset;
|
||||
const {asset} = root;
|
||||
|
||||
if (providedAssetId) {
|
||||
asset = assets.find((asset) => asset.id === this.props.params.id);
|
||||
if (asset === null) {
|
||||
|
||||
if (!asset) {
|
||||
// Not found.
|
||||
return <NotFoundAsset assetId={providedAssetId} />;
|
||||
}
|
||||
if (asset === undefined || asset.id !== providedAssetId) {
|
||||
|
||||
// Still loading.
|
||||
return <Spinner />;
|
||||
}
|
||||
}
|
||||
|
||||
const comments = root[activeTab];
|
||||
|
||||
@@ -9,12 +9,12 @@ const formatDate = (date) => {
|
||||
const Story = ({author, title, createdAt, open, id, goToStory}) => {
|
||||
return (
|
||||
<li className={styles.story} onClick={() => goToStory(id)}>
|
||||
<p className={styles.title}>{title}</p>
|
||||
<p className={styles.meta}>
|
||||
<span className={styles.title}>{title}</span>
|
||||
<div className={styles.meta}>
|
||||
<span className={styles.author}>By {author}</span>
|
||||
<span className={styles.createdAt}>{formatDate(createdAt)}</span>
|
||||
<span className={styles.status}>{open ? 'Open' : 'Closed'}</span>
|
||||
</p>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
.container {
|
||||
position: absolute;
|
||||
background-color: white;
|
||||
top: 100px;
|
||||
top: 106px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
margin-left: auto;
|
||||
@@ -29,8 +29,7 @@
|
||||
|
||||
.headInput {
|
||||
background-color: #efefef;
|
||||
padding: 17px 56px;
|
||||
height: 80px;
|
||||
padding: 10px 27px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
@@ -41,12 +40,13 @@
|
||||
font-size: 16px;
|
||||
margin-right: 5px;
|
||||
position: relative;
|
||||
top: 2px;
|
||||
top: 3px;
|
||||
box-sizing: border-box;
|
||||
border-radius: 3px;
|
||||
border: solid 1px #dfdfdf;
|
||||
max-height: 45px;
|
||||
max-width: 600px;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.cta {
|
||||
@@ -59,20 +59,22 @@
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
padding: 12px 30px;
|
||||
letter-spacing: 0.7px;
|
||||
letter-spacing: 0.25px;
|
||||
}
|
||||
|
||||
.storyList {
|
||||
/*.storyList {
|
||||
border-top: 1px solid #ddd;
|
||||
}
|
||||
}*/
|
||||
|
||||
.story {
|
||||
padding: 10px 63px;
|
||||
padding: 7px 50px;
|
||||
border-bottom: 1px solid #ddd;
|
||||
cursor: pointer;
|
||||
display: block;
|
||||
text-decoration: none;
|
||||
height: 50px;
|
||||
box-sizing: border-box;
|
||||
transition: background-color 400ms;
|
||||
|
||||
&:hover {
|
||||
background-color: #efefef;
|
||||
@@ -86,7 +88,7 @@
|
||||
.title, .meta {
|
||||
margin: 0;
|
||||
color: black;
|
||||
font-size: 17px;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.author, .createdAt, .status {
|
||||
@@ -117,7 +119,8 @@
|
||||
}
|
||||
|
||||
.searchResults {
|
||||
padding: 10px 24px;
|
||||
padding: 7px 27px;
|
||||
background: #F5F5F5;
|
||||
}
|
||||
|
||||
.searchResults i {
|
||||
@@ -132,12 +135,11 @@
|
||||
.headlineRecent {
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.7px;
|
||||
letter-spacing: 0.25px;
|
||||
vertical-align: middle;
|
||||
margin-left: 10px;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.noResults {
|
||||
padding: 10px 24px 15px 49px;
|
||||
}
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ const StorySearch = (props) => {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className={styles.container} role='alertdialog'>
|
||||
<div className={styles.container} role='alertdialog' onKeyDown={props.handleEsc}>
|
||||
<div className={styles.positionShim}>
|
||||
<div className={styles.headInput}>
|
||||
<input
|
||||
|
||||
@@ -2,7 +2,6 @@ import React, {Component} from 'react';
|
||||
import {connect} from 'react-redux';
|
||||
import {bindActionCreators} from 'redux';
|
||||
import {compose, gql} from 'react-apollo';
|
||||
import isEqual from 'lodash/isEqual';
|
||||
import withQuery from 'coral-framework/hocs/withQuery';
|
||||
import {getDefinitionName} from 'coral-framework/utils';
|
||||
import * as notification from 'coral-admin/src/services/notification';
|
||||
@@ -12,7 +11,6 @@ import update from 'immutability-helper';
|
||||
import {withSetUserStatus, withSuspendUser, withSetCommentStatus} from 'coral-framework/graphql/mutations';
|
||||
|
||||
import {fetchSettings} from 'actions/settings';
|
||||
import {updateAssets} from 'actions/assets';
|
||||
import {
|
||||
toggleModal,
|
||||
singleView,
|
||||
@@ -39,13 +37,6 @@ class ModerationContainer extends Component {
|
||||
this.props.fetchSettings();
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
const {updateAssets} = this.props;
|
||||
if(!isEqual(nextProps.root.assets, this.props.root.assets)) {
|
||||
updateAssets(nextProps.root.assets);
|
||||
}
|
||||
}
|
||||
|
||||
suspendUser = async (args) => {
|
||||
this.props.hideSuspendUserDialog();
|
||||
try {
|
||||
@@ -175,7 +166,7 @@ const commentConnectionFragment = gql`
|
||||
`;
|
||||
|
||||
const withModQueueQuery = withQuery(gql`
|
||||
query CoralAdmin_Moderation($asset_id: ID, $sort: SORT_ORDER) {
|
||||
query CoralAdmin_Moderation($asset_id: ID, $sort: SORT_ORDER, $allAssets: Boolean!) {
|
||||
all: comments(query: {
|
||||
statuses: [NONE, PREMOD, ACCEPTED, REJECTED],
|
||||
asset_id: $asset_id,
|
||||
@@ -212,7 +203,7 @@ const withModQueueQuery = withQuery(gql`
|
||||
}) {
|
||||
...CoralAdmin_Moderation_CommentConnection
|
||||
}
|
||||
assets: assets(query: {}) {
|
||||
asset(id: $asset_id) @skip(if: $allAssets) {
|
||||
id
|
||||
title
|
||||
url
|
||||
@@ -248,6 +239,7 @@ const withModQueueQuery = withQuery(gql`
|
||||
variables: {
|
||||
asset_id: id,
|
||||
sort: sortOrder,
|
||||
allAssets: id === null
|
||||
}
|
||||
};
|
||||
},
|
||||
@@ -291,14 +283,12 @@ const mapStateToProps = (state) => ({
|
||||
moderation: state.moderation.toJS(),
|
||||
settings: state.settings.toJS(),
|
||||
auth: state.auth.toJS(),
|
||||
assets: state.assets.get('assets')
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
...bindActionCreators({
|
||||
toggleModal,
|
||||
singleView,
|
||||
updateAssets,
|
||||
fetchSettings,
|
||||
showBanUserDialog,
|
||||
hideBanUserDialog,
|
||||
|
||||
@@ -20,6 +20,13 @@ class StorySearchContainer extends React.Component {
|
||||
});
|
||||
}
|
||||
|
||||
handleEsc = (e) => {
|
||||
if (e.key === 'Escape') {
|
||||
e.preventDefault();
|
||||
this.props.closeSearch();
|
||||
}
|
||||
}
|
||||
|
||||
handleEnter = (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
@@ -43,6 +50,7 @@ class StorySearchContainer extends React.Component {
|
||||
<StorySearch
|
||||
search={this.search}
|
||||
goToStory={this.goToStory}
|
||||
handleEsc={this.handleEsc}
|
||||
handleEnter={this.handleEnter}
|
||||
searchValue={this.state.searchValue}
|
||||
handleSearchChange={this.handleSearchChange}
|
||||
@@ -54,7 +62,7 @@ class StorySearchContainer extends React.Component {
|
||||
|
||||
export const withAssetSearchQuery = withQuery(gql`
|
||||
query SearchStories($value: String = "") {
|
||||
assets(query: {value: $value}) {
|
||||
assets(query: {value: $value, limit: 10}) {
|
||||
id
|
||||
title
|
||||
url
|
||||
|
||||
@@ -22,12 +22,11 @@ const genAssetsByID = (context, ids) => AssetModel.find({
|
||||
/**
|
||||
* [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
|
||||
* @param {Object} query the query
|
||||
* @return {Promise} resolves the assets
|
||||
*/
|
||||
const getAssetsByQuery = (context, value, limit) => {
|
||||
return AssetsService.search(value, null, limit);
|
||||
const getAssetsByQuery = (context, query) => {
|
||||
return AssetsService.search(query);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -76,7 +75,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),
|
||||
search: (query) => getAssetsByQuery(context, query),
|
||||
getByID: new DataLoader((ids) => genAssetsByID(context, ids)),
|
||||
getForMetrics: () => getAssetsForMetrics(context),
|
||||
getAll: new util.SingletonResolver(() => AssetModel.find({}))
|
||||
|
||||
@@ -11,9 +11,7 @@ const RootQuery = {
|
||||
return null;
|
||||
}
|
||||
|
||||
const {value = '', limit} = query;
|
||||
|
||||
return Assets.search(value, limit);
|
||||
return Assets.search(query);
|
||||
},
|
||||
asset(_, query, {loaders: {Assets}}) {
|
||||
if (query.id) {
|
||||
|
||||
@@ -634,7 +634,7 @@ type RootQuery {
|
||||
comment(id: ID!): Comment
|
||||
|
||||
# All assets. Requires the `ADMIN` role.
|
||||
assets(query: AssetsQuery!): [Asset]
|
||||
assets(query: AssetsQuery): [Asset]
|
||||
|
||||
# Find or create an asset by url, or just find with the ID.
|
||||
asset(id: ID, url: String): Asset
|
||||
|
||||
@@ -48,13 +48,13 @@ router.get('/', (req, res, next) => {
|
||||
Promise.all([
|
||||
|
||||
// Find the actuall assets.
|
||||
FilterOpenAssets(AssetsService.search(search), filter)
|
||||
FilterOpenAssets(AssetsService.search({value: search}), filter)
|
||||
.sort({[field]: (sort === 'asc') ? 1 : -1})
|
||||
.skip(parseInt(skip))
|
||||
.limit(parseInt(limit)),
|
||||
|
||||
// Get the count of actual assets.
|
||||
FilterOpenAssets(AssetsService.search(search), filter)
|
||||
FilterOpenAssets(AssetsService.search({value: search}), filter)
|
||||
.count()
|
||||
])
|
||||
.then(([result, count]) => {
|
||||
|
||||
+3
-4
@@ -104,13 +104,12 @@ module.exports = class AssetsService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds assets matching keywords on the model. If `value` is an empty string,
|
||||
* then it will not even perform a text search query.
|
||||
* Finds assets matching keywords on the model.
|
||||
* @param {String} value string to search by.
|
||||
* @return {Promise}
|
||||
*/
|
||||
static search(value = '', skip = null, limit = null) {
|
||||
if (value.length === 0) {
|
||||
static search({value, skip, limit} = {}) {
|
||||
if (!value) {
|
||||
return AssetsService.all(skip, limit);
|
||||
} else {
|
||||
return AssetModel
|
||||
|
||||
@@ -43,7 +43,7 @@ describe('/api/v1/assets', () => {
|
||||
.set(passport.inject({roles: ['ADMIN']}))
|
||||
.then((res) => {
|
||||
const body = res.body;
|
||||
|
||||
|
||||
expect(body).to.have.property('count', 2);
|
||||
expect(body).to.have.property('result');
|
||||
|
||||
|
||||
Reference in New Issue
Block a user