First working version

This commit is contained in:
Chi Vinh Le
2017-08-25 19:21:57 +07:00
parent f0931b4dc6
commit 6ae68a4f90
35 changed files with 502 additions and 169 deletions
+1
View File
@@ -13,5 +13,6 @@ plugins/*
!plugins/talk-plugin-comment-content
!plugins/talk-plugin-permalink
!plugins/talk-plugin-featured-comments
!plugins/talk-plugin-sort-oldest
node_modules
+1
View File
@@ -29,5 +29,6 @@ plugins/*
!plugins/talk-plugin-comment-content
!plugins/talk-plugin-permalink
!plugins/talk-plugin-featured-comments
!plugins/talk-plugin-sort-oldest
**/node_modules/*
@@ -5,6 +5,8 @@ import queryString from 'query-string';
export const setActiveReplyBox = (id) => ({type: actions.SET_ACTIVE_REPLY_BOX, id});
export const setSort = ({order, by}) => ({type: actions.SET_SORT, order, by});
export const viewAllComments = () => {
const search = queryString.stringify({
+158 -107
View File
@@ -13,7 +13,7 @@ import t, {timeago} from 'coral-framework/services/i18n';
import CommentBox from 'talk-plugin-commentbox/CommentBox';
import QuestionBox from 'talk-plugin-questionbox/QuestionBox';
import {isCommentActive} from 'coral-framework/utils';
import {Button, Tab, TabCount, TabPane} from 'coral-ui';
import {Spinner, Button, Tab, TabCount, TabPane} from 'coral-ui';
import cn from 'classnames';
import {getTopLevelParent, attachCommentToParent} from '../graphql/utils';
@@ -23,6 +23,8 @@ import StreamTabPanel from '../containers/StreamTabPanel';
import styles from './Stream.css';
const SpinnerWhenLoading = ({loading, children}) => loading ? <Spinner /> : <div>{children}</div>;
class Stream extends React.Component {
constructor(props) {
@@ -49,15 +51,14 @@ class Stream extends React.Component {
);
};
render() {
renderHighlightedComment() {
const {
data,
root,
activeReplyBox,
setActiveReplyBox,
appendItemArray,
commentClassNames,
root: {asset, asset: {comment, comments, totalCommentCount}},
root: {asset, asset: {comment}},
postComment,
addNotification,
editComment,
@@ -65,23 +66,15 @@ class Stream extends React.Component {
postDontAgree,
deleteAction,
showSignInDialog,
updateItem,
ignoreUser,
activeStreamTab,
setActiveStreamTab,
loadNewReplies,
loadMoreComments,
viewAllComments,
auth: {loggedIn, user},
editName,
auth: {user},
emit,
} = this.props;
const {keepCommentBox} = this.state;
const open = !asset.isClosed;
// even though the permalinked comment is the highlighted one, we're displaying its parent + replies
let highlightedComment = comment && getTopLevelParent(comment);
if (highlightedComment) {
let topLevelComment = getTopLevelParent(comment);
if (topLevelComment) {
// Inactive comments can be viewed by moderators and admins (e.g. using permalinks).
const isInactive = !isCommentActive(comment.status);
@@ -89,10 +82,152 @@ class Stream extends React.Component {
// the highlighted comment is not active and as such not in the replies, so we
// attach it to the right parent.
highlightedComment = attachCommentToParent(highlightedComment, comment);
topLevelComment = attachCommentToParent(topLevelComment, comment);
}
}
return (
<div className={cn('talk-stream-highlighted-container', styles.highlightedContainer)}>
<Comment
data={data}
root={root}
commentClassNames={commentClassNames}
ignoreUser={ignoreUser}
setActiveReplyBox={setActiveReplyBox}
activeReplyBox={activeReplyBox}
addNotification={addNotification}
depth={0}
disableReply={!open}
postComment={postComment}
asset={asset}
currentUser={user}
highlighted={comment.id}
postFlag={postFlag}
postDontAgree={postDontAgree}
loadMore={loadNewReplies}
deleteAction={deleteAction}
showSignInDialog={showSignInDialog}
key={topLevelComment.id}
commentIsIgnored={this.commentIsIgnored}
comment={topLevelComment}
charCountEnable={asset.settings.charCountEnable}
maxCharCount={asset.settings.charCount}
editComment={editComment}
emit={emit}
liveUpdates={true}
/>
</div>
);
}
renderTabPanel() {
const {
data,
root,
activeReplyBox,
setActiveReplyBox,
commentClassNames,
root: {asset, asset: {comments, totalCommentCount}},
postComment,
addNotification,
editComment,
postFlag,
postDontAgree,
deleteAction,
showSignInDialog,
ignoreUser,
activeStreamTab,
setActiveStreamTab,
loadNewReplies,
loadMoreComments,
auth: {user},
emit,
sortOrder,
sortBy,
} = this.props;
const slotProps = {data};
const slotQueryData = {root, asset};
// `key` of `StreamTabPanel` depends on sorting so that we always reset
// the state when changing sorting.
return (
<div className={cn('talk-stream-tab-container', styles.tabContainer)}>
<div
className={cn('talk-stream-filter-wrapper', styles.filterWrapper)}
>
<Slot
fill="streamFilter"
queryData={slotQueryData}
{...slotProps}
/>
</div>
<StreamTabPanel
key={`${sortBy}_${sortOrder}`}
activeTab={activeStreamTab}
setActiveTab={setActiveStreamTab}
fallbackTab={'all'}
tabSlot={'streamTabs'}
tabPaneSlot={'streamTabPanes'}
slotProps={slotProps}
queryData={slotQueryData}
appendTabs={
<Tab tabId={'all'} key='all'>
All Comments <TabCount active={activeStreamTab === 'all'} sub>{totalCommentCount}</TabCount>
</Tab>
}
appendTabPanes={
<TabPane tabId={'all'} key='all'>
<AllCommentsPane
data={data}
root={root}
comments={comments}
commentClassNames={commentClassNames}
ignoreUser={ignoreUser}
setActiveReplyBox={setActiveReplyBox}
activeReplyBox={activeReplyBox}
addNotification={addNotification}
disableReply={!open}
postComment={postComment}
asset={asset}
currentUser={user}
postFlag={postFlag}
postDontAgree={postDontAgree}
loadMore={loadMoreComments}
loadNewReplies={loadNewReplies}
deleteAction={deleteAction}
showSignInDialog={showSignInDialog}
commentIsIgnored={this.commentIsIgnored}
charCountEnable={asset.settings.charCountEnable}
maxCharCount={asset.settings.charCount}
editComment={editComment}
emit={emit}
/>
</TabPane>
}
sub
/>
</div>
);
}
render() {
const {
data,
root,
appendItemArray,
root: {asset, asset: {comment: highlightedComment, comments}},
postComment,
addNotification,
updateItem,
viewAllComments,
auth: {loggedIn, user},
editName,
loading,
} = this.props;
const {keepCommentBox} = this.state;
const open = !asset.isClosed;
const banned = user && user.status === 'BANNED';
const temporarilySuspended =
user &&
@@ -103,7 +238,7 @@ class Stream extends React.Component {
const slotProps = {data};
const slotQueryData = {root, asset};
if (!comment && !comments) {
if (!highlightedComment && !comments) {
console.error('Talk: No comments came back from the graph given that query. Please, check the query params.');
return <StreamError />;
}
@@ -111,7 +246,7 @@ class Stream extends React.Component {
return (
<div id="stream" className={styles.root}>
<AutomaticAssetClosure assetId={asset.id} closedAt={asset.closedAt}/>
{comment &&
{highlightedComment &&
<Button
cStyle="darkGrey"
className={cn('talk-stream-show-all-comments-button', styles.viewAllButton)}
@@ -174,96 +309,12 @@ class Stream extends React.Component {
/>
)}
{/* the highlightedComment is isolated after the user followed a permalink */}
{highlightedComment
? (
<div className={cn('talk-stream-highlighted-container', styles.highlightedContainer)}>
<Comment
data={data}
root={root}
commentClassNames={commentClassNames}
ignoreUser={ignoreUser}
setActiveReplyBox={setActiveReplyBox}
activeReplyBox={activeReplyBox}
addNotification={addNotification}
depth={0}
disableReply={!open}
postComment={postComment}
asset={asset}
currentUser={user}
highlighted={comment.id}
postFlag={postFlag}
postDontAgree={postDontAgree}
loadMore={loadNewReplies}
deleteAction={deleteAction}
showSignInDialog={showSignInDialog}
key={highlightedComment.id}
commentIsIgnored={this.commentIsIgnored}
comment={highlightedComment}
charCountEnable={asset.settings.charCountEnable}
maxCharCount={asset.settings.charCount}
editComment={editComment}
emit={emit}
liveUpdates={true}
/>
</div>
)
: <div className={cn('talk-stream-tab-container', styles.tabContainer)}>
<div
className={cn('talk-stream-filter-wrapper', styles.filterWrapper)}
>
<Slot
fill="streamFilter"
queryData={slotQueryData}
{...slotProps}
/>
</div>
<StreamTabPanel
activeTab={activeStreamTab}
setActiveTab={setActiveStreamTab}
fallbackTab={'all'}
tabSlot={'streamTabs'}
tabPaneSlot={'streamTabPanes'}
slotProps={slotProps}
queryData={slotQueryData}
appendTabs={
<Tab tabId={'all'} key='all'>
All Comments <TabCount active={activeStreamTab === 'all'} sub>{totalCommentCount}</TabCount>
</Tab>
}
appendTabPanes={
<TabPane tabId={'all'} key='all'>
<AllCommentsPane
data={data}
root={root}
comments={comments}
commentClassNames={commentClassNames}
ignoreUser={ignoreUser}
setActiveReplyBox={setActiveReplyBox}
activeReplyBox={activeReplyBox}
addNotification={addNotification}
disableReply={!open}
postComment={postComment}
asset={asset}
currentUser={user}
postFlag={postFlag}
postDontAgree={postDontAgree}
loadMore={loadMoreComments}
loadNewReplies={loadNewReplies}
deleteAction={deleteAction}
showSignInDialog={showSignInDialog}
commentIsIgnored={this.commentIsIgnored}
charCountEnable={asset.settings.charCountEnable}
maxCharCount={asset.settings.charCount}
editComment={editComment}
emit={emit}
/>
</TabPane>
}
sub
/>
</div>
<SpinnerWhenLoading loading={loading}>
{highlightedComment
? this.renderHighlightedComment()
: this.renderTabPanel()
}
</SpinnerWhenLoading>
</div>
);
}
@@ -6,3 +6,4 @@ export const ADD_COMMENT_CLASSNAME = 'ADD_COMMENT_CLASSNAME';
export const REMOVE_COMMENT_CLASSNAME = 'REMOVE_COMMENT_CLASSNAME';
export const THREADING_LEVEL = process.env.TALK_THREADING_LEVEL;
export const SET_ACTIVE_TAB = 'CORAL_STREAM_SET_ACTIVE_TAB';
export const SET_SORT = 'CORAL_STREAM_SET_SORT';
@@ -158,7 +158,7 @@ const EMBED_QUERY = gql`
$hasComment: Boolean!,
$excludeIgnored: Boolean,
$sortBy: SORT_COMMENTS_BY!,
$sort: SORT_ORDER!,
$sortOrder: SORT_ORDER!,
) {
me {
id
@@ -171,7 +171,7 @@ const EMBED_QUERY = gql`
`;
export const withEmbedQuery = withQuery(EMBED_QUERY, {
options: ({auth, commentId, assetId, assetUrl, sortBy, sort}) => ({
options: ({auth, commentId, assetId, assetUrl, sortBy, sortOrder}) => ({
variables: {
assetId,
assetUrl,
@@ -179,7 +179,7 @@ export const withEmbedQuery = withQuery(EMBED_QUERY, {
hasComment: commentId !== '',
excludeIgnored: Boolean(auth && auth.user && auth.user.id),
sortBy,
sort,
sortOrder,
},
}),
});
@@ -191,7 +191,7 @@ const mapStateToProps = (state) => ({
assetUrl: state.stream.assetUrl,
activeTab: state.embed.activeTab,
config: state.config,
sort: state.stream.sort,
sortOrder: state.stream.sortOrder,
sortBy: state.stream.sortBy,
});
@@ -102,7 +102,7 @@ class StreamContainer extends React.Component {
cursor: comment.replies.endCursor,
parent_id,
asset_id: this.props.root.asset.id,
sort: 'ASC',
sortOrder: 'ASC',
excludeIgnored: this.props.data.variables.excludeIgnored,
},
updateQuery: (prev, {fetchMoreResult:{comments}}) => {
@@ -119,7 +119,7 @@ class StreamContainer extends React.Component {
cursor: this.props.root.asset.comments.endCursor,
parent_id: null,
asset_id: this.props.root.asset.id,
sort: this.props.data.variables.sort,
sortOrder: this.props.data.variables.sortOrder,
sortBy: this.props.data.variables.sortBy,
excludeIgnored: this.props.data.variables.excludeIgnored,
},
@@ -141,24 +141,43 @@ class StreamContainer extends React.Component {
clearInterval(this.countPoll);
}
componentWillReceiveProps(nextProps) {
if (nextProps.sortBy === 'CREATED_AT' && nextProps.sortOrder === 'DESC') {
// When switching to 'Newest first' we refetch and subscribe so that we always have the newest comments.
if (this.props.sortOrder !== nextProps.sortOrder || this.props.sortBy !== nextProps.sortBy) {
nextProps.data.refetch();
this.subscribeToUpdates();
}
} else {
// TODO: only unsubscribe from posting comments.
// We only subscribe to new comments during 'Newest first'.
this.unsubscribe();
}
}
userIsDegraged({auth: {user}} = this.props) {
return !can(user, 'INTERACT_WITH_COMMUNITY');
}
render() {
if (this.props.refetching
|| !this.props.root.asset
if (!this.props.root.asset
|| !this.props.root.asset.comment
&& !this.props.root.asset.comments
) {
return <Spinner />;
}
const streamLoading = this.props.refetching || this.props.data.loading;
return <Stream
{...this.props}
loadMore={this.loadMore}
loadMoreComments={this.loadMoreComments}
loadNewReplies={this.loadNewReplies}
userIsDegraged={this.userIsDegraged()}
loading={streamLoading}
/>;
}
}
@@ -209,7 +228,7 @@ const LOAD_MORE_QUERY = gql`
$cursor: Cursor
$parent_id: ID
$asset_id: ID
$sort: SORT_ORDER
$sortOrder: SORT_ORDER
$sortBy: SORT_COMMENTS_BY = CREATED_AT
$excludeIgnored: Boolean
) {
@@ -219,7 +238,7 @@ const LOAD_MORE_QUERY = gql`
cursor: $cursor
parent_id: $parent_id
asset_id: $asset_id
sort: $sort
sort: $sortOrder
sortBy: $sortBy
excludeIgnored: $excludeIgnored
}
@@ -274,7 +293,7 @@ const fragments = {
}
commentCount @skip(if: $hasComment)
totalCommentCount @skip(if: $hasComment)
comments(query: {limit: 10, excludeIgnored: $excludeIgnored, sort: $sort, sortBy: $sortBy}) @skip(if: $hasComment) {
comments(query: {limit: 10, excludeIgnored: $excludeIgnored, sort: $sortOrder, sortBy: $sortBy}) @skip(if: $hasComment) {
nodes {
...CoralEmbedStream_Stream_comment
}
@@ -317,6 +336,8 @@ const mapStateToProps = (state) => ({
previousStreamTab: state.stream.previousTab,
commentClassNames: state.stream.commentClassNames,
pluginConfig: state.config.plugin_config,
sortOrder: state.stream.sortOrder,
sortBy: state.stream.sortBy,
});
const mapDispatchToProps = (dispatch) =>
@@ -24,7 +24,7 @@ const initialState = {
activeTab: process.env.TALK_DEFAULT_STREAM_TAB,
previousTab: '',
sortBy: 'CREATED_AT',
sort: 'DESC',
sortOrder: 'DESC',
};
export default function stream(state = initialState, action) {
@@ -68,6 +68,12 @@ export default function stream(state = initialState, action) {
...state.commentClassNames.slice(action.idx + 1)
]
};
case actions.SET_SORT :
return {
...state,
sortOrder: action.order ? action.order : state.order,
sortBy: action.by ? action.by : state.by,
};
default:
return state;
}
+52 -6
View File
@@ -6,6 +6,7 @@ import {getSlotElements, getSlotComponentProps} from 'coral-framework/helpers/pl
import omit from 'lodash/omit';
import isEqual from 'lodash/isEqual';
import {getShallowChanges} from 'coral-framework/utils';
import PropTypes from 'prop-types';
const emptyConfig = {};
@@ -25,7 +26,18 @@ class Slot extends React.Component {
return changes.length !== 0;
}
getSlotProps({fill: _a, inline: _b, className: _c, reduxState: _d, defaultComponent_: _e, queryData: _f, ...rest} = this.props) {
getSlotProps(props = this.props) {
const {
fill: _a,
inline: _b,
className: _c,
reduxState: _d,
defaultComponent_: _e,
queryData: _f,
childFactory: _g,
component: _h,
...rest
} = props;
return rest;
}
@@ -34,26 +46,60 @@ class Slot extends React.Component {
}
render() {
const {inline = false, className, reduxState, defaultComponent: DefaultComponent, queryData} = this.props;
const {
inline = false,
className,
reduxState,
component: Component,
childFactory,
defaultComponent: DefaultComponent,
queryData,
} = this.props;
let children = this.getChildren();
const pluginConfig = reduxState.config.pluginConfig || emptyConfig;
if (children.length === 0 && DefaultComponent) {
children = <DefaultComponent {...getSlotComponentProps(DefaultComponent, reduxState, this.getSlotProps(this.props), queryData)} />;
}
if (childFactory) {
children = children.map(childFactory);
}
return (
<div className={cn({[styles.inline]: inline, [styles.debug]: pluginConfig.debug}, className)}>
<Component className={cn({[styles.inline]: inline, [styles.debug]: pluginConfig.debug}, className)}>
{children}
</div>
</Component>
);
}
}
Slot.defaultProps = {
component: 'div',
};
Slot.propTypes = {
fill: React.PropTypes.string.isRequired,
fill: PropTypes.string.isRequired,
/**
* You may specify the component to use as the root wrapper.
* Defaults to 'div'.
*/
component: PropTypes.any,
// props coming from graphql must be passed through this property.
queryData: React.PropTypes.object,
queryData: PropTypes.object,
/**
* You may need to apply reactive updates to a child as it is exiting.
* This is generally done by using `cloneElement` however in the case of an exiting
* child the element has already been removed and not accessible to the consumer.
*
* If you do need to update a child as it leaves you can provide a `childFactory`
* to wrap every child, even the ones that are leaving.
*
* @type Function(child: ReactElement) -> ReactElement
*/
childFactory: PropTypes.func,
};
const mapStateToProps = (state) => ({
+1
View File
@@ -2,6 +2,7 @@ import {gql} from 'react-apollo';
import t from 'coral-framework/services/i18n';
import union from 'lodash/union';
import {capitalize} from 'coral-framework/helpers/strings';
export * from 'coral-framework/helpers/strings';
export const getTotalActionCount = (type, comment) => {
return comment.action_summaries
+1
View File
@@ -0,0 +1 @@
export {setSort} from 'coral-embed-stream/src/actions/stream';
@@ -0,0 +1,8 @@
.input {
cursor: pointer;
}
.label {
cursor: pointer;
padding-left: 4px;
}
@@ -0,0 +1,15 @@
import React from 'react';
import styles from './SortOption.css';
export default class SortOption extends React.Component {
render() {
return (
<div className={styles.viewingOption}>
<input type="radio" onChange={this.props.setSort} checked={this.props.active} className={styles.input}/>
<label className={styles.label} onClick={this.props.setSort}>
{this.props.label}
</label>
</div>
);
}
}
@@ -1,3 +1,5 @@
export {Slot} from 'coral-framework/components';
export {default as ClickOutside} from 'coral-framework/components/ClickOutside';
export {default as IfSlotIsEmpty} from 'coral-framework/components/IfSlotIsEmpty';
export {default as IfSlotIsNotEmpty} from 'coral-framework/components/IfSlotIsNotEmpty';
export {default as CommentAuthorName} from 'coral-framework/components/CommentAuthorName';
+1
View File
@@ -1,5 +1,6 @@
export {default as withReaction} from './withReaction';
export {default as withTags} from './withTags';
export {default as withSortOption} from './withSortOption';
export {default as withFragments} from 'coral-framework/hocs/withFragments';
export {default as excludeIf} from 'coral-framework/hocs/excludeIf';
export {default as connect} from 'coral-framework/hocs/connect';
@@ -0,0 +1,40 @@
import React from 'react';
import {connect} from 'plugin-api/beta/client/hocs';
import {bindActionCreators} from 'redux';
import {sortOrderSelector, sortBySelector} from 'plugin-api/beta/client/selectors/stream';
import {setSort} from 'plugin-api/beta/client/actions/stream';
import hoistStatics from 'recompose/hoistStatics';
const mapStateToProps = (state) => ({
sortOrder: sortOrderSelector(state),
sortBy: sortBySelector(state),
});
const mapDispatchToProps = (dispatch) =>
bindActionCreators(
{
setSort
},
dispatch
);
export default ({by = 'created_at', order = 'DESC', label}) => hoistStatics((WrappedComponent) => {
class WithSortOption extends React.Component {
setSort = () => {
this.props.setSort({by, order});
}
render() {
const active = this.props.sortOrder === order && this.props.sortBy === by;
const resolvedLabel = typeof label === 'function' ? label() : label;
return (
<WrappedComponent
active={active}
setSort={this.setSort}
label={resolvedLabel}
/>
);
}
}
return connect(mapStateToProps, mapDispatchToProps)(WithSortOption);
});
@@ -0,0 +1,2 @@
export const sortOrderSelector = (state) => state.stream.sortOrder;
export const sortBySelector = (state) => state.stream.sortBy;
+1
View File
@@ -3,5 +3,6 @@ export {
insertCommentsSorted,
getSlotFragmentSpreads,
forEachError,
capitalize,
getDefinitionName,
} from 'coral-framework/utils';
@@ -18,7 +18,7 @@ class TabPaneContainer extends React.Component {
limit: 5,
cursor: this.props.asset.featuredComments.endCursor,
asset_id: this.props.asset.id,
sort: this.props.data.variables.sort,
sortOrder: this.props.data.variables.sortOrder,
sortBy: this.props.data.variables.sortBy,
excludeIgnored: this.props.data.variables.excludeIgnored,
},
@@ -52,7 +52,7 @@ const LOAD_MORE_QUERY = gql`
$limit: Int = 5
$cursor: Cursor
$asset_id: ID
$sort: SORT_ORDER
$sortOrder: SORT_ORDER
$sortBy: SORT_COMMENTS_BY
$excludeIgnored: Boolean
) {
@@ -62,7 +62,7 @@ const LOAD_MORE_QUERY = gql`
cursor: $cursor
tags: ["FEATURED"]
asset_id: $asset_id,
sort: $sort
sort: $sortOrder
sortBy: $sortBy
excludeIgnored: $excludeIgnored
}
@@ -100,7 +100,7 @@ const enhance = compose(
featuredComments: comments(
query: {
tags: ["FEATURED"]
sort: $sort
sort: $sortOrder
sortBy: $sortBy
}
deep: true
@@ -19,7 +19,6 @@
}
.viewingOption {
padding: 5px 0;
}
:global(.talk-plugin-off-topic-comment) {
+2 -2
View File
@@ -6,7 +6,7 @@ import reducer from './reducer';
/**
* talk-plugin-offtopic depends on talk-plugin-viewing-options
* in other to display filter and use the streamViewingOptions slot
* in other to display filter.
*/
export default {
@@ -15,6 +15,6 @@ export default {
slots: {
commentInputDetailArea: [OffTopicCheckbox],
commentInfoBar: [OffTopicTag],
viewingOptions: [OffTopicFilter]
viewingOptionsFilter: [OffTopicFilter]
}
};
@@ -0,0 +1,14 @@
{
"presets": [
"es2015"
],
"plugins": [
"add-module-exports",
"transform-class-properties",
"transform-decorators-legacy",
"transform-object-assign",
"transform-object-rest-spread",
"transform-async-to-generator",
"transform-react-jsx"
]
}
@@ -0,0 +1,23 @@
{
"env": {
"browser": true,
"es6": true,
"mocha": true
},
"parserOptions": {
"sourceType": "module",
"ecmaFeatures": {
"experimentalObjectRestSpread": true,
"jsx": true
}
},
"parser": "babel-eslint",
"plugins": [
"react"
],
"rules": {
"react/jsx-uses-react": "error",
"react/jsx-uses-vars": "error",
"no-console": ["warn", { "allow": ["warn", "error"] }]
}
}
@@ -0,0 +1,9 @@
.input {
cursor: pointer;
}
.label {
cursor: pointer;
padding-left: 4px;
}
@@ -0,0 +1,15 @@
import React from 'react';
import styles from './SortOption.css';
export default class SortOption extends React.Component {
render() {
return (
<div className={styles.viewingOption}>
<input type="radio" onChange={this.props.setSort} checked={this.props.active} className={styles.input}/>
<label className={styles.label} onClick={this.props.setSort}>
{this.props.label}
</label>
</div>
);
}
}
@@ -0,0 +1,4 @@
import {withSortOption} from 'plugin-api/beta/client/hocs';
import SortOption from '../components/SortOption';
export default withSortOption({by: 'CREATED_AT', order: 'ASC', label: 'Oldest first'})(SortOption);
@@ -0,0 +1,4 @@
import {withSortOption} from 'plugin-api/beta/client/hocs';
import SortOption from '../components/SortOption';
export default withSortOption({by: 'CREATED_AT', order: 'DESC', label: 'Newest first'})(SortOption);
@@ -0,0 +1,15 @@
import translations from './translations.yml';
import SortOption from './containers/SortOption';
import SortOptionNewest from './containers/SortOptionNewest';
/**
* talk-plugin-sort-oldest depends on talk-plugin-viewing-options
* in other to display sort option.
*/
export default {
translations,
slots: {
viewingOptionsSort: [SortOptionNewest, SortOption]
}
};
@@ -0,0 +1,4 @@
en:
talk-plugin-sort-oldest:
es:
talk-plugin-sort-oldest:
+2
View File
@@ -0,0 +1,2 @@
module.exports = {
};
@@ -0,0 +1,15 @@
.title {
font-weight: bold;
padding: 12px 8px 0px 8px;
}
.list {
padding: 0;
margin: 0;
}
.listItem {
padding: 10px;
list-style: none;
white-space: nowrap;
}
@@ -0,0 +1,16 @@
import React from 'react';
import styles from './Category.css';
import {Slot, IfSlotIsNotEmpty} from 'plugin-api/beta/client/components';
const childFactory = (child) => <li className={styles.listItem} key={child.key}>{child}</li>;
const ViewingOptions = ({slot, title}) => {
return (
<IfSlotIsNotEmpty slot={slot}>
<div className={styles.title}>{title}</div>
<Slot fill={slot} childFactory={childFactory} className={styles.list} component={'ul'}/>
</IfSlotIsNotEmpty>
);
};
export default ViewingOptions;
@@ -9,13 +9,15 @@
cursor: pointer;
}
.list {
.menu {
background: white;
position: absolute;
box-shadow: 0px 3px 5px 0px rgba(0,0,0,0.15);
right: 0px;
top: 20px;
z-index: 10;
min-height: 32px;
min-width: 64px;
}
.list > ul, .list > ul > li {
@@ -3,51 +3,60 @@ import cn from 'classnames';
import styles from './ViewingOptions.css';
import {t} from 'plugin-api/beta/client/services';
import {Icon} from 'plugin-api/beta/client/components/ui';
import {Slot, ClickOutside} from 'plugin-api/beta/client/components';
import {ClickOutside} from 'plugin-api/beta/client/components';
import {capitalize} from 'plugin-api/beta/client/utils';
import Category from './Category';
const ViewingOptions = (props) => {
const toggleOpen = () => {
if (!props.open) {
props.openMenu();
class ViewingOptions extends React.Component {
categories = {
sort: t('talk-plugin-viewing-options.sort'),
filter: t('talk-plugin-viewing-options.filter'),
};
toggleOpen = () => {
const {open, openMenu, closeMenu} = this.props;
if (!open) {
openMenu();
} else {
props.closeMenu();
closeMenu();
}
};
const handleClickOutside = () => {
if (props.open) {
props.closeMenu();
handleClickOutside = () => {
const {open, closeMenu} = this.props;
if (open) {
closeMenu();
}
};
return (
<ClickOutside onClickOutside={handleClickOutside}>
<div className={cn([styles.root, 'talk-plugin-viewing-options'])}>
<div>
<button className={styles.button} onClick={toggleOpen}>
<Icon className={styles.filterIcon} name="filter_list" />
<span className={styles.filterText}>{t('talk-plugin-viewing-options.viewing_options')}</span>
{props.open ? <Icon name="arrow_drop_up" className={styles.icon}/> : <Icon name="arrow_drop_down" className={styles.icon}/>}
</button>
</div>
{
props.open ? (
<div className={cn([styles.list, 'talk-plugin-viewing-options-list'])}>
<ul>
render() {
const {open} = this.props;
return (
<ClickOutside onClickOutside={this.handleClickOutside}>
<div className={cn([styles.root, 'talk-plugin-viewing-options'])}>
<div>
<button className={styles.button} onClick={this.toggleOpen}>
<Icon className={styles.filterIcon} name="filter_list" />
<span className={styles.filterText}>{t('talk-plugin-viewing-options.viewing_options')}</span>
{open ? <Icon name="arrow_drop_up" className={styles.icon}/> : <Icon name="arrow_drop_down" className={styles.icon}/>}
</button>
</div>
{
open ? (
<div className={cn([styles.menu, 'talk-plugin-viewing-options-menu'])}>
{
React.Children.map(<Slot fill="viewingOptions" />, (component) => {
return React.createElement('li', {
className: 'talk-plugin-viewing-options-item'
}, component);
})
Object.keys(this.categories).map((category) =>
<Category key={category} slot={`viewingOptions${capitalize(category)}`} title={this.categories[category]} />
)
}
</ul>
</div>
) : null
}
</div>
</ClickOutside>
);
};
</div>
) : null
}
</div>
</ClickOutside>
);
}
}
export default ViewingOptions;
@@ -1,6 +1,8 @@
en:
talk-plugin-viewing-options:
viewing_options: "Viewing Options"
sort: Sort
filter: Filter
es:
talk-plugin-viewing-options:
viewing_options: "Opciones de visualización"