mirror of
https://github.com/astuto/astuto.git
synced 2025-12-16 11:47:56 +01:00
Add anonymous feedback (#380)
This commit is contained in:
committed by
GitHub
parent
7a37dae22d
commit
a49b5695f5
@@ -78,4 +78,20 @@ export const requestPosts = (
|
||||
} catch (e) {
|
||||
dispatch(postsRequestFailure(e));
|
||||
}
|
||||
}
|
||||
|
||||
// Used to get posts that require moderation (i.e. pending or rejected posts)
|
||||
export const requestPostsForModeration = (
|
||||
|
||||
): ThunkAction<void, State, null, Action<string>> => async (dispatch) => {
|
||||
dispatch(postsRequestStart());
|
||||
|
||||
try {
|
||||
const response = await fetch('/posts/moderation');
|
||||
const json = await response.json();
|
||||
|
||||
dispatch(postsRequestSuccess(json, 1));
|
||||
} catch (e) {
|
||||
dispatch(postsRequestFailure(e));
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import HttpStatus from "../../constants/http_status";
|
||||
import buildRequestHeaders from "../../helpers/buildRequestHeaders";
|
||||
import IPostJSON from "../../interfaces/json/IPost";
|
||||
import { State } from "../../reducers/rootReducer";
|
||||
import { PostApprovalStatus } from "../../interfaces/IPost";
|
||||
|
||||
export const POST_UPDATE_START = 'POST_UPDATE_START';
|
||||
interface PostUpdateStartAction {
|
||||
@@ -31,7 +32,7 @@ const postUpdateStart = (): PostUpdateStartAction => ({
|
||||
type: POST_UPDATE_START,
|
||||
});
|
||||
|
||||
const postUpdateSuccess = (
|
||||
export const postUpdateSuccess = (
|
||||
postJSON: IPostJSON,
|
||||
): PostUpdateSuccessAction => ({
|
||||
type: POST_UPDATE_SUCCESS,
|
||||
@@ -78,6 +79,39 @@ export const updatePost = (
|
||||
} catch (e) {
|
||||
dispatch(postUpdateFailure(e));
|
||||
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
};
|
||||
|
||||
export const updatePostApprovalStatus = (
|
||||
id: number,
|
||||
approvalStatus: PostApprovalStatus,
|
||||
authenticityToken: string,
|
||||
): ThunkAction<void, State, null, Action<string>> => async (dispatch) => {
|
||||
dispatch(postUpdateStart());
|
||||
|
||||
try {
|
||||
const res = await fetch(`/posts/${id}`, {
|
||||
method: 'PATCH',
|
||||
headers: buildRequestHeaders(authenticityToken),
|
||||
body: JSON.stringify({
|
||||
post: {
|
||||
approval_status: approvalStatus,
|
||||
}
|
||||
}),
|
||||
});
|
||||
const json = await res.json();
|
||||
|
||||
if (res.status === HttpStatus.OK) {
|
||||
dispatch(postUpdateSuccess(json));
|
||||
} else {
|
||||
dispatch(postUpdateFailure(json.error));
|
||||
}
|
||||
|
||||
return Promise.resolve(res);
|
||||
} catch (e) {
|
||||
dispatch(postUpdateFailure(e));
|
||||
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
};
|
||||
@@ -20,7 +20,9 @@ interface Props {
|
||||
board: IBoard;
|
||||
isLoggedIn: boolean;
|
||||
isPowerUser: boolean;
|
||||
currentUserFullName: string;
|
||||
tenantSetting: ITenantSetting;
|
||||
componentRenderedAt: number;
|
||||
authenticityToken: string;
|
||||
posts: PostsState;
|
||||
postStatuses: PostStatusesState;
|
||||
@@ -91,7 +93,9 @@ class BoardP extends React.Component<Props> {
|
||||
board,
|
||||
isLoggedIn,
|
||||
isPowerUser,
|
||||
currentUserFullName,
|
||||
tenantSetting,
|
||||
componentRenderedAt,
|
||||
authenticityToken,
|
||||
posts,
|
||||
postStatuses,
|
||||
@@ -110,8 +114,12 @@ class BoardP extends React.Component<Props> {
|
||||
<NewPost
|
||||
board={board}
|
||||
isLoggedIn={isLoggedIn}
|
||||
currentUserFullName={currentUserFullName}
|
||||
isAnonymousFeedbackAllowed={tenantSetting.allow_anonymous_feedback}
|
||||
componentRenderedAt={componentRenderedAt}
|
||||
authenticityToken={authenticityToken}
|
||||
/>
|
||||
|
||||
<div className="sidebarFilters">
|
||||
<SearchFilter
|
||||
searchQuery={filters.searchQuery}
|
||||
|
||||
@@ -13,11 +13,17 @@ import Button from '../common/Button';
|
||||
import IBoard from '../../interfaces/IBoard';
|
||||
import buildRequestHeaders from '../../helpers/buildRequestHeaders';
|
||||
import HttpStatus from '../../constants/http_status';
|
||||
import { POST_APPROVAL_STATUS_APPROVED } from '../../interfaces/IPost';
|
||||
|
||||
interface Props {
|
||||
board: IBoard;
|
||||
isLoggedIn: boolean;
|
||||
currentUserFullName: string;
|
||||
isAnonymousFeedbackAllowed: boolean;
|
||||
authenticityToken: string;
|
||||
|
||||
// Time check anti-spam measure
|
||||
componentRenderedAt: number;
|
||||
}
|
||||
|
||||
interface State {
|
||||
@@ -28,6 +34,13 @@ interface State {
|
||||
|
||||
title: string;
|
||||
description: string;
|
||||
isSubmissionAnonymous: boolean;
|
||||
|
||||
// Honeypot anti-spam measure
|
||||
// These fields are honeypots: they are not visibile and must not be filled
|
||||
// dnf = do not fill
|
||||
dnf1: string;
|
||||
dnf2: string;
|
||||
}
|
||||
|
||||
class NewPost extends React.Component<Props, State> {
|
||||
@@ -42,12 +55,19 @@ class NewPost extends React.Component<Props, State> {
|
||||
|
||||
title: '',
|
||||
description: '',
|
||||
isSubmissionAnonymous: false,
|
||||
|
||||
dnf1: '',
|
||||
dnf2: '',
|
||||
};
|
||||
|
||||
this.toggleForm = this.toggleForm.bind(this);
|
||||
this.onTitleChange = this.onTitleChange.bind(this);
|
||||
this.onDescriptionChange = this.onDescriptionChange.bind(this);
|
||||
this.submitForm = this.submitForm.bind(this);
|
||||
|
||||
this.onDnf1Change = this.onDnf1Change.bind(this)
|
||||
this.onDnf2Change = this.onDnf2Change.bind(this)
|
||||
}
|
||||
|
||||
toggleForm() {
|
||||
@@ -72,6 +92,18 @@ class NewPost extends React.Component<Props, State> {
|
||||
});
|
||||
}
|
||||
|
||||
onDnf1Change(dnf1: string) {
|
||||
this.setState({
|
||||
dnf1,
|
||||
});
|
||||
}
|
||||
|
||||
onDnf2Change(dnf2: string) {
|
||||
this.setState({
|
||||
dnf2,
|
||||
});
|
||||
}
|
||||
|
||||
async submitForm(e: React.FormEvent) {
|
||||
e.preventDefault();
|
||||
|
||||
@@ -82,8 +114,8 @@ class NewPost extends React.Component<Props, State> {
|
||||
});
|
||||
|
||||
const boardId = this.props.board.id;
|
||||
const { authenticityToken } = this.props;
|
||||
const { title, description } = this.state;
|
||||
const { authenticityToken, componentRenderedAt } = this.props;
|
||||
const { title, description, isSubmissionAnonymous, dnf1, dnf2 } = this.state;
|
||||
|
||||
if (title === '') {
|
||||
this.setState({
|
||||
@@ -102,6 +134,12 @@ class NewPost extends React.Component<Props, State> {
|
||||
title,
|
||||
description,
|
||||
board_id: boardId,
|
||||
|
||||
is_anonymous: isSubmissionAnonymous,
|
||||
|
||||
dnf1,
|
||||
dnf2,
|
||||
form_rendered_at: componentRenderedAt,
|
||||
},
|
||||
}),
|
||||
});
|
||||
@@ -109,16 +147,22 @@ class NewPost extends React.Component<Props, State> {
|
||||
this.setState({isLoading: false});
|
||||
|
||||
if (res.status === HttpStatus.Created) {
|
||||
this.setState({
|
||||
success: I18n.t('board.new_post.submit_success'),
|
||||
if (json.approval_status === POST_APPROVAL_STATUS_APPROVED) {
|
||||
this.setState({
|
||||
success: I18n.t('board.new_post.submit_success'),
|
||||
});
|
||||
|
||||
title: '',
|
||||
description: '',
|
||||
});
|
||||
|
||||
setTimeout(() => (
|
||||
window.location.href = `/posts/${json.slug || json.id}`
|
||||
), 1000);
|
||||
setTimeout(() => (
|
||||
window.location.href = `/posts/${json.slug || json.id}`
|
||||
), 1000);
|
||||
} else {
|
||||
this.setState({
|
||||
success: I18n.t('board.new_post.submit_pending'),
|
||||
title: '',
|
||||
description: '',
|
||||
showForm: false,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
this.setState({error: json.error});
|
||||
}
|
||||
@@ -131,7 +175,13 @@ class NewPost extends React.Component<Props, State> {
|
||||
}
|
||||
|
||||
render() {
|
||||
const { board, isLoggedIn } = this.props;
|
||||
const {
|
||||
board,
|
||||
isLoggedIn,
|
||||
currentUserFullName,
|
||||
isAnonymousFeedbackAllowed
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
showForm,
|
||||
error,
|
||||
@@ -139,7 +189,11 @@ class NewPost extends React.Component<Props, State> {
|
||||
isLoading,
|
||||
|
||||
title,
|
||||
description
|
||||
description,
|
||||
isSubmissionAnonymous,
|
||||
|
||||
dnf1,
|
||||
dnf2,
|
||||
} = this.state;
|
||||
|
||||
return (
|
||||
@@ -154,23 +208,47 @@ class NewPost extends React.Component<Props, State> {
|
||||
{board.description}
|
||||
</ReactMarkdown>
|
||||
|
||||
<Button
|
||||
onClick={() => {
|
||||
|
||||
if (showForm) {
|
||||
this.toggleForm();
|
||||
return;
|
||||
}
|
||||
|
||||
if (isLoggedIn) {
|
||||
this.toggleForm();
|
||||
this.setState({ isSubmissionAnonymous: false });
|
||||
} else {
|
||||
window.location.href = '/users/sign_in';
|
||||
}
|
||||
}}
|
||||
className="submitBtn"
|
||||
outline={showForm}
|
||||
>
|
||||
{
|
||||
showForm ?
|
||||
I18n.t('board.new_post.cancel_button')
|
||||
:
|
||||
I18n.t('board.new_post.submit_button')
|
||||
}
|
||||
</Button>
|
||||
|
||||
{
|
||||
isLoggedIn ?
|
||||
<Button
|
||||
onClick={this.toggleForm}
|
||||
className="submitBtn"
|
||||
outline={showForm}>
|
||||
{
|
||||
showForm ?
|
||||
I18n.t('board.new_post.cancel_button')
|
||||
:
|
||||
I18n.t('board.new_post.submit_button')
|
||||
}
|
||||
</Button>
|
||||
:
|
||||
<a href="/users/sign_in" className="btn btnPrimary">
|
||||
{I18n.t('board.new_post.login_button')}
|
||||
</a>
|
||||
(isAnonymousFeedbackAllowed && !showForm) &&
|
||||
<div className="anonymousFeedbackLink">
|
||||
{I18n.t('common.words.or')}
|
||||
|
||||
<a
|
||||
onClick={() => {
|
||||
this.toggleForm();
|
||||
this.setState({ isSubmissionAnonymous: true });
|
||||
}}
|
||||
className="link"
|
||||
>
|
||||
{I18n.t('board.new_post.submit_anonymous_button').toLowerCase()}
|
||||
</a>
|
||||
</div>
|
||||
}
|
||||
|
||||
{
|
||||
@@ -180,7 +258,16 @@ class NewPost extends React.Component<Props, State> {
|
||||
description={description}
|
||||
handleTitleChange={this.onTitleChange}
|
||||
handleDescriptionChange={this.onDescriptionChange}
|
||||
|
||||
handleSubmit={this.submitForm}
|
||||
|
||||
dnf1={dnf1}
|
||||
dnf2={dnf2}
|
||||
handleDnf1Change={this.onDnf1Change}
|
||||
handleDnf2Change={this.onDnf2Change}
|
||||
|
||||
currentUserFullName={currentUserFullName}
|
||||
isSubmissionAnonymous={isSubmissionAnonymous}
|
||||
/>
|
||||
:
|
||||
null
|
||||
|
||||
@@ -2,13 +2,23 @@ import * as React from 'react';
|
||||
import I18n from 'i18n-js';
|
||||
|
||||
import Button from '../common/Button';
|
||||
import { SmallMutedText } from '../common/CustomTexts';
|
||||
|
||||
interface Props {
|
||||
title: string;
|
||||
description: string;
|
||||
handleTitleChange(title: string): void;
|
||||
handleDescriptionChange(description: string): void;
|
||||
|
||||
handleSubmit(e: object): void;
|
||||
|
||||
dnf1: string;
|
||||
dnf2: string;
|
||||
handleDnf1Change(dnf1: string): void;
|
||||
handleDnf2Change(dnf2: string): void;
|
||||
|
||||
currentUserFullName: string;
|
||||
isSubmissionAnonymous: boolean;
|
||||
}
|
||||
|
||||
const NewPostForm = ({
|
||||
@@ -16,17 +26,26 @@ const NewPostForm = ({
|
||||
description,
|
||||
handleTitleChange,
|
||||
handleDescriptionChange,
|
||||
|
||||
handleSubmit,
|
||||
|
||||
dnf1,
|
||||
dnf2,
|
||||
handleDnf1Change,
|
||||
handleDnf2Change,
|
||||
|
||||
currentUserFullName,
|
||||
isSubmissionAnonymous,
|
||||
}: Props) => (
|
||||
<div className="newPostForm">
|
||||
<form>
|
||||
<div className="form-group">
|
||||
<label htmlFor="postTitle">{I18n.t('board.new_post.title')}</label>
|
||||
<input
|
||||
type="text"
|
||||
value={title}
|
||||
onChange={e => handleTitleChange(e.target.value)}
|
||||
maxLength={128}
|
||||
placeholder={I18n.t('board.new_post.title')}
|
||||
|
||||
id="postTitle"
|
||||
className="form-control"
|
||||
@@ -34,20 +53,60 @@ const NewPostForm = ({
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
|
||||
{ /* Honeypot field 1 */ }
|
||||
<div className="form-group form-group-dnf">
|
||||
<input
|
||||
type="text"
|
||||
value={dnf1}
|
||||
onChange={e => handleDnf1Change(e.target.value)}
|
||||
maxLength={128}
|
||||
placeholder="email"
|
||||
autoComplete="off"
|
||||
|
||||
id="email"
|
||||
className="form-control"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{ /* Honeypot field 2 */ }
|
||||
<input
|
||||
type="text"
|
||||
value={dnf2}
|
||||
onChange={e => handleDnf2Change(e.target.value)}
|
||||
maxLength={128}
|
||||
placeholder="name"
|
||||
autoComplete="off"
|
||||
tabIndex={-1}
|
||||
|
||||
id="name"
|
||||
className="form-control"
|
||||
/>
|
||||
|
||||
<div className="form-group">
|
||||
<label htmlFor="postDescription">{I18n.t('board.new_post.description')}</label>
|
||||
<textarea
|
||||
value={description}
|
||||
onChange={e => handleDescriptionChange(e.target.value)}
|
||||
rows={3}
|
||||
placeholder={I18n.t('board.new_post.description')}
|
||||
|
||||
className="form-control"
|
||||
id="postDescription"
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<Button onClick={e => handleSubmit(e)} className="submitBtn d-block mx-auto">
|
||||
{I18n.t('board.new_post.submit_button')}
|
||||
</Button>
|
||||
|
||||
<SmallMutedText>
|
||||
{
|
||||
isSubmissionAnonymous ?
|
||||
I18n.t('board.new_post.anonymous_submission_help')
|
||||
:
|
||||
I18n.t('board.new_post.non_anonymous_submission_help', { name: currentUserFullName })
|
||||
}
|
||||
</SmallMutedText>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -14,7 +14,9 @@ interface Props {
|
||||
board: IBoard;
|
||||
isLoggedIn: boolean;
|
||||
isPowerUser: boolean;
|
||||
currentUserFullName: string;
|
||||
tenantSetting: ITenantSetting;
|
||||
componentRenderedAt: number;
|
||||
authenticityToken: string;
|
||||
}
|
||||
|
||||
@@ -32,7 +34,9 @@ class BoardRoot extends React.Component<Props> {
|
||||
board,
|
||||
isLoggedIn,
|
||||
isPowerUser,
|
||||
currentUserFullName,
|
||||
tenantSetting,
|
||||
componentRenderedAt,
|
||||
authenticityToken,
|
||||
} = this.props;
|
||||
|
||||
@@ -42,7 +46,9 @@ class BoardRoot extends React.Component<Props> {
|
||||
board={board}
|
||||
isLoggedIn={isLoggedIn}
|
||||
isPowerUser={isPowerUser}
|
||||
currentUserFullName={currentUserFullName}
|
||||
tenantSetting={tenantSetting}
|
||||
componentRenderedAt={componentRenderedAt}
|
||||
authenticityToken={authenticityToken}
|
||||
/>
|
||||
</Provider>
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
import React from 'react';
|
||||
import I18n from 'i18n-js';
|
||||
import Gravatar from 'react-gravatar';
|
||||
|
||||
import IPost, { PostApprovalStatus } from '../../../interfaces/IPost';
|
||||
import { AnonymousIcon, ApproveIcon, RejectIcon } from '../../common/Icons';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import ActionLink from '../../common/ActionLink';
|
||||
|
||||
interface Props {
|
||||
post: IPost;
|
||||
|
||||
onUpdatePostApprovalStatus(
|
||||
id: number,
|
||||
approvalStatus: PostApprovalStatus,
|
||||
): Promise<any>;
|
||||
|
||||
hideRejectButton: boolean;
|
||||
}
|
||||
|
||||
const FeedbackListItem = ({ post, onUpdatePostApprovalStatus, hideRejectButton }: Props) => {
|
||||
return (
|
||||
<div className="feedbackListItem">
|
||||
<div className="feedbackListItemIconAndContent">
|
||||
<div className="feedbackListItemIcon">
|
||||
{
|
||||
post.userId ?
|
||||
<Gravatar email={post.userEmail} size={42} title={post.userEmail} className="gravatar userGravatar" />
|
||||
:
|
||||
<AnonymousIcon size={42} />
|
||||
}
|
||||
</div>
|
||||
|
||||
<div className="feedbackListItemContent">
|
||||
<p className="feedbackListItemTitle" onClick={() => window.location.href = `/posts/${post.slug || post.id}`}>
|
||||
{post.title}
|
||||
</p>
|
||||
|
||||
<ReactMarkdown className="feedbackListItemDescription" allowedTypes={['text']} unwrapDisallowed>
|
||||
{post.description.length > 200 ? `${post.description.slice(0, 200)}...` : post.description}
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="feedbackListItemActions">
|
||||
<ActionLink
|
||||
onClick={() => {
|
||||
onUpdatePostApprovalStatus(post.id, 'approved')
|
||||
}}
|
||||
icon={<ApproveIcon />}
|
||||
>
|
||||
{I18n.t('common.buttons.approve')}
|
||||
</ActionLink>
|
||||
|
||||
{!hideRejectButton &&
|
||||
<ActionLink
|
||||
onClick={() => {
|
||||
onUpdatePostApprovalStatus(post.id, 'rejected')
|
||||
}}
|
||||
icon={<RejectIcon />}
|
||||
>
|
||||
{I18n.t('common.buttons.reject')}
|
||||
</ActionLink>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default FeedbackListItem;
|
||||
@@ -0,0 +1,41 @@
|
||||
import React from 'react';
|
||||
import I18n from 'i18n-js';
|
||||
|
||||
import IPost, { PostApprovalStatus } from '../../../interfaces/IPost';
|
||||
import FeedbackListItem from './FeedbackListItem';
|
||||
import { MutedText } from '../../common/CustomTexts';
|
||||
|
||||
interface Props {
|
||||
posts: Array<IPost>;
|
||||
|
||||
onUpdatePostApprovalStatus(
|
||||
id: number,
|
||||
approvalStatus: PostApprovalStatus,
|
||||
): Promise<any>;
|
||||
|
||||
hideRejectButton?: boolean;
|
||||
}
|
||||
|
||||
const FeedbackModerationList = ({ posts, onUpdatePostApprovalStatus, hideRejectButton = false }: Props) => {
|
||||
return (
|
||||
<div className="feedbackModerationList">
|
||||
{
|
||||
(posts && posts.length > 0) ?
|
||||
posts.map((post, i) => (
|
||||
<FeedbackListItem
|
||||
key={i}
|
||||
post={post}
|
||||
onUpdatePostApprovalStatus={onUpdatePostApprovalStatus}
|
||||
hideRejectButton={hideRejectButton}
|
||||
/>
|
||||
))
|
||||
:
|
||||
<div className="emptyList">
|
||||
<MutedText>{I18n.t('board.posts_list.empty')}</MutedText>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default FeedbackModerationList;
|
||||
@@ -0,0 +1,157 @@
|
||||
import * as React from 'react';
|
||||
import I18n from 'i18n-js';
|
||||
|
||||
import Box from '../../common/Box';
|
||||
import SiteSettingsInfoBox from '../../common/SiteSettingsInfoBox';
|
||||
import { UserRoles } from '../../../interfaces/IUser';
|
||||
import { TenantSettingFeedbackApprovalPolicy } from '../../../interfaces/ITenantSetting';
|
||||
import Badge, { BADGE_TYPE_LIGHT } from '../../common/Badge';
|
||||
import ActionLink from '../../common/ActionLink';
|
||||
import { SettingsIcon } from '../../common/Icons';
|
||||
import IPost, { PostApprovalStatus } from '../../../interfaces/IPost';
|
||||
import FeedbackModerationList from './FeedbackModerationList';
|
||||
|
||||
interface Props {
|
||||
currentUserRole: UserRoles;
|
||||
changeFeedbackModerationSettingsUrl: string;
|
||||
tenantSettingAllowAnonymousFeedback: boolean;
|
||||
tenantSettingFeedbackApprovalPolicy: TenantSettingFeedbackApprovalPolicy;
|
||||
authenticityToken: string;
|
||||
|
||||
posts: Array<IPost>;
|
||||
areLoading: boolean;
|
||||
areUpdating: boolean;
|
||||
error: string;
|
||||
requestPostsForModeration(): void;
|
||||
updatePostApprovalStatus(
|
||||
id: number,
|
||||
approvalStatus: PostApprovalStatus,
|
||||
authenticityToken: string
|
||||
): Promise<any>;
|
||||
}
|
||||
|
||||
interface State {
|
||||
filter: 'pending' | 'rejected';
|
||||
}
|
||||
|
||||
class FeedbackModerationP extends React.Component<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
filter: 'pending',
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.props.requestPostsForModeration();
|
||||
}
|
||||
|
||||
getFeedbackPolicyString(tenantSettingFeedbackApprovalPolicy: TenantSettingFeedbackApprovalPolicy): string {
|
||||
switch (tenantSettingFeedbackApprovalPolicy) {
|
||||
case 'anonymous_require_approval':
|
||||
return I18n.t('site_settings.general.feedback_approval_policy_anonymous_require_approval');
|
||||
case 'never_require_approval':
|
||||
return I18n.t('site_settings.general.feedback_approval_policy_never_require_approval');
|
||||
case 'always_require_approval':
|
||||
return I18n.t('site_settings.general.feedback_approval_policy_always_require_approval');
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
currentUserRole,
|
||||
changeFeedbackModerationSettingsUrl,
|
||||
tenantSettingAllowAnonymousFeedback,
|
||||
tenantSettingFeedbackApprovalPolicy,
|
||||
|
||||
posts,
|
||||
areLoading,
|
||||
areUpdating,
|
||||
error,
|
||||
|
||||
updatePostApprovalStatus,
|
||||
|
||||
authenticityToken,
|
||||
} = this.props;
|
||||
|
||||
const { filter } = this.state;
|
||||
|
||||
const pendingPosts = posts.filter(post => post.approvalStatus === 'pending');
|
||||
const rejectedPosts = posts.filter(post => post.approvalStatus === 'rejected');
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box customClass="feedbackModerationContainer">
|
||||
<h2>{ I18n.t('moderation.menu.feedback') }</h2>
|
||||
|
||||
{
|
||||
(currentUserRole === 'admin' || currentUserRole === 'owner') &&
|
||||
<>
|
||||
<div className="badges">
|
||||
<Badge type={BADGE_TYPE_LIGHT}>
|
||||
{
|
||||
tenantSettingAllowAnonymousFeedback ?
|
||||
I18n.t('moderation.feedback.anonymous_feedback_allowed')
|
||||
:
|
||||
I18n.t('moderation.feedback.anonymous_feedback_not_allowed')
|
||||
}
|
||||
</Badge>
|
||||
|
||||
<Badge type={BADGE_TYPE_LIGHT}>
|
||||
{ this.getFeedbackPolicyString(tenantSettingFeedbackApprovalPolicy) }
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<ActionLink
|
||||
onClick={() => window.location.href = changeFeedbackModerationSettingsUrl} icon={<SettingsIcon />}
|
||||
customClass="changeFeedbackModerationSettingsLink"
|
||||
>
|
||||
{ I18n.t('moderation.feedback.change_feedback_moderation_settings') }
|
||||
</ActionLink>
|
||||
</>
|
||||
}
|
||||
|
||||
<ul className="filterModerationFeedbackNav">
|
||||
<li className="nav-item">
|
||||
<a onClick={() => this.setState({filter: 'pending'})} className={`nav-link${filter === 'pending' ? ' active' : ''}`}>
|
||||
{I18n.t('activerecord.attributes.post.approval_status_pending')}
|
||||
|
||||
({pendingPosts && pendingPosts.length})
|
||||
</a>
|
||||
</li>
|
||||
<li className="nav-item">
|
||||
<a onClick={() => this.setState({filter: 'rejected'})} className={`nav-link${filter === 'rejected' ? ' active' : ''}`}>
|
||||
{I18n.t('activerecord.attributes.post.approval_status_rejected')}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
{
|
||||
filter === 'pending' ?
|
||||
<FeedbackModerationList
|
||||
posts={pendingPosts}
|
||||
onUpdatePostApprovalStatus={
|
||||
(id: number, approvalStatus: PostApprovalStatus) =>
|
||||
updatePostApprovalStatus(id, approvalStatus, authenticityToken)
|
||||
}
|
||||
/>
|
||||
:
|
||||
<FeedbackModerationList
|
||||
posts={rejectedPosts}
|
||||
onUpdatePostApprovalStatus={
|
||||
(id: number, approvalStatus: PostApprovalStatus) =>
|
||||
updatePostApprovalStatus(id, approvalStatus, authenticityToken)
|
||||
}
|
||||
hideRejectButton
|
||||
/>
|
||||
}
|
||||
</Box>
|
||||
|
||||
<SiteSettingsInfoBox areUpdating={areLoading || areUpdating} error={error} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default FeedbackModerationP;
|
||||
44
app/javascript/components/Moderation/Feedback/index.tsx
Normal file
44
app/javascript/components/Moderation/Feedback/index.tsx
Normal file
@@ -0,0 +1,44 @@
|
||||
import * as React from 'react';
|
||||
import { Provider } from 'react-redux';
|
||||
import { Store } from 'redux';
|
||||
|
||||
import FeedbackModeration from '../../../containers/FeedbackModeration';
|
||||
|
||||
import createStoreHelper from '../../../helpers/createStore';
|
||||
import { State } from '../../../reducers/rootReducer';
|
||||
import { UserRoles } from '../../../interfaces/IUser';
|
||||
import { TenantSettingFeedbackApprovalPolicy } from '../../../interfaces/ITenantSetting';
|
||||
|
||||
interface Props {
|
||||
currentUserRole: UserRoles;
|
||||
changeFeedbackModerationSettingsUrl: string;
|
||||
tenantSettingAllowAnonymousFeedback: boolean;
|
||||
tenantSettingFeedbackApprovalPolicy: TenantSettingFeedbackApprovalPolicy;
|
||||
authenticityToken: string;
|
||||
}
|
||||
|
||||
class FeedbackModerationRoot extends React.Component<Props> {
|
||||
store: Store<State, any>;
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this.store = createStoreHelper();
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Provider store={this.store}>
|
||||
<FeedbackModeration
|
||||
currentUserRole={this.props.currentUserRole}
|
||||
changeFeedbackModerationSettingsUrl={this.props.changeFeedbackModerationSettingsUrl}
|
||||
tenantSettingAllowAnonymousFeedback={this.props.tenantSettingAllowAnonymousFeedback}
|
||||
tenantSettingFeedbackApprovalPolicy={this.props.tenantSettingFeedbackApprovalPolicy}
|
||||
authenticityToken={this.props.authenticityToken}
|
||||
/>
|
||||
</Provider>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default FeedbackModerationRoot;
|
||||
@@ -64,9 +64,9 @@ class UserEditable extends React.Component<Props, State> {
|
||||
|
||||
const confirmationMessage =
|
||||
newStatus === 'blocked' ?
|
||||
I18n.t('site_settings.users.block_confirmation', { name: user.fullName })
|
||||
I18n.t('moderation.users.block_confirmation', { name: user.fullName })
|
||||
:
|
||||
I18n.t('site_settings.users.unblock_confirmation', { name: user.fullName });
|
||||
I18n.t('moderation.users.unblock_confirmation', { name: user.fullName });
|
||||
|
||||
const confirmationResponse = confirm(confirmationMessage);
|
||||
|
||||
@@ -101,7 +101,7 @@ class UserEditable extends React.Component<Props, State> {
|
||||
|
||||
<div className="userRoleStatus">
|
||||
<span>
|
||||
<MutedText>{ I18n.t(`site_settings.users.role_${user.role}`) }</MutedText>
|
||||
<MutedText>{ I18n.t(`moderation.users.role_${user.role}`) }</MutedText>
|
||||
</span>
|
||||
|
||||
{
|
||||
@@ -109,7 +109,7 @@ class UserEditable extends React.Component<Props, State> {
|
||||
<>
|
||||
<Separator />
|
||||
<span className={`userStatus userStatus${user.status}`}>
|
||||
{ I18n.t(`site_settings.users.status_${user.status}`) }
|
||||
{ I18n.t(`moderation.users.status_${user.status}`) }
|
||||
</span>
|
||||
</>
|
||||
:
|
||||
@@ -137,9 +137,9 @@ class UserEditable extends React.Component<Props, State> {
|
||||
>
|
||||
{
|
||||
user.status !== USER_STATUS_BLOCKED ?
|
||||
I18n.t('site_settings.users.block')
|
||||
I18n.t('moderation.users.block')
|
||||
:
|
||||
I18n.t('site_settings.users.unblock')
|
||||
I18n.t('moderation.users.unblock')
|
||||
}
|
||||
</ActionLink>
|
||||
</div>
|
||||
@@ -30,9 +30,9 @@ class UserForm extends React.Component<Props, State> {
|
||||
|
||||
if (selectedRole !== currentRole) {
|
||||
if (selectedRole === 'moderator')
|
||||
confirmation = confirm(I18n.t('site_settings.users.role_to_moderator_confirmation', { name: user.fullName }));
|
||||
confirmation = confirm(I18n.t('moderation.users.role_to_moderator_confirmation', { name: user.fullName }));
|
||||
else if (selectedRole === 'admin')
|
||||
confirmation = confirm(I18n.t('site_settings.users.role_to_admin_confirmation', { name: user.fullName }));
|
||||
confirmation = confirm(I18n.t('moderation.users.role_to_admin_confirmation', { name: user.fullName }));
|
||||
}
|
||||
|
||||
if (confirmation) updateUserRole(selectedRole);
|
||||
@@ -60,13 +60,13 @@ class UserForm extends React.Component<Props, State> {
|
||||
>
|
||||
<optgroup label={getLabel('user', 'role')}>
|
||||
<option value={USER_ROLE_USER}>
|
||||
{ I18n.t(`site_settings.users.role_${USER_ROLE_USER}`) }
|
||||
{ I18n.t(`moderation.users.role_${USER_ROLE_USER}`) }
|
||||
</option>
|
||||
<option value={USER_ROLE_MODERATOR}>
|
||||
{ I18n.t(`site_settings.users.role_${USER_ROLE_MODERATOR}`) }
|
||||
{ I18n.t(`moderation.users.role_${USER_ROLE_MODERATOR}`) }
|
||||
</option>
|
||||
<option value={USER_ROLE_ADMIN}>
|
||||
{ I18n.t(`site_settings.users.role_${USER_ROLE_ADMIN}`) }
|
||||
{ I18n.t(`moderation.users.role_${USER_ROLE_ADMIN}`) }
|
||||
</option>
|
||||
</optgroup>
|
||||
</select>
|
||||
@@ -32,7 +32,7 @@ interface Props {
|
||||
authenticityToken: string;
|
||||
}
|
||||
|
||||
class UsersSiteSettingsP extends React.Component<Props> {
|
||||
class UsersModerationP extends React.Component<Props> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
@@ -81,14 +81,14 @@ class UsersSiteSettingsP extends React.Component<Props> {
|
||||
return (
|
||||
<>
|
||||
<Box>
|
||||
<h2>{ I18n.t('site_settings.users.title') }</h2>
|
||||
<h2>{ I18n.t('moderation.users.title') }</h2>
|
||||
|
||||
<p className="userCount">
|
||||
{numberOfUsers} {I18n.t('activerecord.models.user', {count: users.items.length})}
|
||||
(
|
||||
{numberOfActiveUsers} {I18n.t('site_settings.users.status_active')},
|
||||
{numberOfBlockedUsers} {I18n.t('site_settings.users.status_blocked')},
|
||||
{numberOfDeletedUsers} {I18n.t('site_settings.users.status_deleted')})
|
||||
{numberOfActiveUsers} {I18n.t('moderation.users.status_active')},
|
||||
{numberOfBlockedUsers} {I18n.t('moderation.users.status_blocked')},
|
||||
{numberOfDeletedUsers} {I18n.t('moderation.users.status_deleted')})
|
||||
</p>
|
||||
|
||||
<ul className="usersList">
|
||||
@@ -117,4 +117,4 @@ class UsersSiteSettingsP extends React.Component<Props> {
|
||||
}
|
||||
}
|
||||
|
||||
export default UsersSiteSettingsP;
|
||||
export default UsersModerationP;
|
||||
@@ -2,7 +2,7 @@ import * as React from 'react';
|
||||
import { Provider } from 'react-redux';
|
||||
import { Store } from 'redux';
|
||||
|
||||
import UsersSiteSettings from '../../../containers/UsersSiteSettings';
|
||||
import UsersModeration from '../../../containers/UsersModeration';
|
||||
|
||||
import createStoreHelper from '../../../helpers/createStore';
|
||||
import { UserRoles } from '../../../interfaces/IUser';
|
||||
@@ -14,7 +14,7 @@ interface Props {
|
||||
authenticityToken: string;
|
||||
}
|
||||
|
||||
class UsersSiteSettingsRoot extends React.Component<Props> {
|
||||
class UsersModerationRoot extends React.Component<Props> {
|
||||
store: Store<State, any>;
|
||||
|
||||
constructor(props: Props) {
|
||||
@@ -26,7 +26,7 @@ class UsersSiteSettingsRoot extends React.Component<Props> {
|
||||
render() {
|
||||
return (
|
||||
<Provider store={this.store}>
|
||||
<UsersSiteSettings
|
||||
<UsersModeration
|
||||
currentUserEmail={this.props.currentUserEmail}
|
||||
currentUserRole={this.props.currentUserRole}
|
||||
authenticityToken={this.props.authenticityToken}
|
||||
@@ -36,4 +36,4 @@ class UsersSiteSettingsRoot extends React.Component<Props> {
|
||||
}
|
||||
}
|
||||
|
||||
export default UsersSiteSettingsRoot;
|
||||
export default UsersModerationRoot;
|
||||
@@ -29,14 +29,23 @@ const PostFooter = ({
|
||||
}: Props) => (
|
||||
<div className="postFooter">
|
||||
<div className="postAuthor">
|
||||
<span>{I18n.t('post.published_by').toLowerCase()} </span>
|
||||
<Gravatar email={authorEmail} size={24} className="postAuthorAvatar" />
|
||||
{authorFullName}
|
||||
{
|
||||
authorEmail ?
|
||||
<>
|
||||
<span>{I18n.t('post.published_by').toLowerCase()} </span>
|
||||
<Gravatar email={authorEmail} size={24} className="postAuthorAvatar" />
|
||||
<span>{authorFullName}</span>
|
||||
</>
|
||||
:
|
||||
<span>{I18n.t('post.published_anonymously').toLowerCase()}</span>
|
||||
}
|
||||
|
||||
<Separator />
|
||||
{friendlyDate(createdAt)}
|
||||
|
||||
<span>{friendlyDate(createdAt)}</span>
|
||||
</div>
|
||||
{
|
||||
isPowerUser || authorEmail === currentUserEmail ?
|
||||
isPowerUser || (authorEmail && authorEmail === currentUserEmail) ?
|
||||
<div className="postFooterActions">
|
||||
<ActionLink onClick={toggleEditMode} icon={<EditIcon />} customClass='editAction'>
|
||||
{I18n.t('common.buttons.edit')}
|
||||
|
||||
@@ -2,7 +2,7 @@ import * as React from 'react';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import I18n from 'i18n-js';
|
||||
|
||||
import IPost from '../../interfaces/IPost';
|
||||
import IPost, { POST_APPROVAL_STATUS_APPROVED, POST_APPROVAL_STATUS_PENDING } from '../../interfaces/IPost';
|
||||
import IPostStatus from '../../interfaces/IPostStatus';
|
||||
import IBoard from '../../interfaces/IBoard';
|
||||
import ITenantSetting from '../../interfaces/ITenantSetting';
|
||||
@@ -28,6 +28,7 @@ import { fromRailsStringToJavascriptDate } from '../../helpers/datetime';
|
||||
import HttpStatus from '../../constants/http_status';
|
||||
import ActionLink from '../common/ActionLink';
|
||||
import { EditIcon } from '../common/Icons';
|
||||
import Badge, { BADGE_TYPE_DANGER, BADGE_TYPE_WARNING } from '../common/Badge';
|
||||
|
||||
interface Props {
|
||||
postId: number;
|
||||
@@ -249,6 +250,15 @@ class PostP extends React.Component<Props> {
|
||||
</ActionLink>
|
||||
}
|
||||
</div>
|
||||
|
||||
{
|
||||
(isPowerUser && post.approvalStatus !== POST_APPROVAL_STATUS_APPROVED) &&
|
||||
<div className="postInfo">
|
||||
<Badge type={post.approvalStatus === POST_APPROVAL_STATUS_PENDING ? BADGE_TYPE_WARNING : BADGE_TYPE_DANGER}>
|
||||
{ I18n.t(`activerecord.attributes.post.approval_status_${post.approvalStatus.toLowerCase()}`) }
|
||||
</Badge>
|
||||
</div>
|
||||
}
|
||||
|
||||
<ReactMarkdown
|
||||
className="postDescription"
|
||||
|
||||
@@ -25,13 +25,15 @@ export interface ISiteSettingsGeneralForm {
|
||||
siteLogo: string;
|
||||
brandDisplaySetting: string;
|
||||
locale: string;
|
||||
rootBoardId?: string;
|
||||
customDomain?: string;
|
||||
allowAnonymousFeedback: boolean;
|
||||
feedbackApprovalPolicy: string;
|
||||
showRoadmapInHeader: boolean;
|
||||
collapseBoardsInHeader: string;
|
||||
showVoteCount: boolean;
|
||||
showVoteButtonInBoard: boolean;
|
||||
showPoweredBy: boolean;
|
||||
rootBoardId?: string;
|
||||
customDomain?: string;
|
||||
showRoadmapInHeader: boolean;
|
||||
collapseBoardsInHeader: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
@@ -50,6 +52,8 @@ interface Props {
|
||||
locale: string,
|
||||
rootBoardId: number,
|
||||
customDomain: string,
|
||||
allowAnonymousFeedback: boolean,
|
||||
feedbackApprovalPolicy: string,
|
||||
showRoadmapInHeader: boolean,
|
||||
collapseBoardsInHeader: string,
|
||||
showVoteCount: boolean,
|
||||
@@ -80,13 +84,15 @@ const GeneralSiteSettingsP = ({
|
||||
siteLogo: originForm.siteLogo,
|
||||
brandDisplaySetting: originForm.brandDisplaySetting,
|
||||
locale: originForm.locale,
|
||||
rootBoardId: originForm.rootBoardId,
|
||||
customDomain: originForm.customDomain,
|
||||
allowAnonymousFeedback: originForm.allowAnonymousFeedback,
|
||||
feedbackApprovalPolicy: originForm.feedbackApprovalPolicy,
|
||||
showRoadmapInHeader: originForm.showRoadmapInHeader,
|
||||
collapseBoardsInHeader: originForm.collapseBoardsInHeader,
|
||||
showVoteCount: originForm.showVoteCount,
|
||||
showVoteButtonInBoard: originForm.showVoteButtonInBoard,
|
||||
showPoweredBy: originForm.showPoweredBy,
|
||||
rootBoardId: originForm.rootBoardId,
|
||||
customDomain: originForm.customDomain,
|
||||
showRoadmapInHeader: originForm.showRoadmapInHeader,
|
||||
collapseBoardsInHeader: originForm.collapseBoardsInHeader,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -98,6 +104,8 @@ const GeneralSiteSettingsP = ({
|
||||
data.locale,
|
||||
Number(data.rootBoardId),
|
||||
data.customDomain,
|
||||
data.allowAnonymousFeedback,
|
||||
data.feedbackApprovalPolicy,
|
||||
data.showRoadmapInHeader,
|
||||
data.collapseBoardsInHeader,
|
||||
data.showVoteCount,
|
||||
@@ -106,15 +114,35 @@ const GeneralSiteSettingsP = ({
|
||||
authenticityToken
|
||||
).then(res => {
|
||||
if (res?.status !== HttpStatus.OK) return;
|
||||
|
||||
const urlWithoutHash = window.location.href.split('#')[0];
|
||||
window.history.pushState({}, document.title, urlWithoutHash);
|
||||
window.location.reload();
|
||||
});
|
||||
};
|
||||
|
||||
const customDomain = watch('customDomain');
|
||||
|
||||
React.useEffect(() => {
|
||||
if (window.location.hash) {
|
||||
const anchor = window.location.hash.substring(1);
|
||||
const anchorElement = document.getElementById(anchor);
|
||||
|
||||
if (anchorElement) {
|
||||
anchorElement.classList.add('highlighted');
|
||||
|
||||
setTimeout( () => {
|
||||
anchorElement.scrollIntoView({
|
||||
behavior: 'smooth'
|
||||
})
|
||||
}, 50);
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box>
|
||||
<Box customClass="generalSiteSettingsContainer">
|
||||
<h2>{ I18n.t('site_settings.general.title') }</h2>
|
||||
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
@@ -226,64 +254,98 @@ const GeneralSiteSettingsP = ({
|
||||
</div>
|
||||
}
|
||||
|
||||
<br />
|
||||
<h4>{ I18n.t('site_settings.general.subtitle_header') }</h4>
|
||||
|
||||
<div className="formGroup">
|
||||
<div className="checkboxSwitch">
|
||||
<input {...register('showRoadmapInHeader')} type="checkbox" id="show_roadmap_in_header" />
|
||||
<label htmlFor="show_roadmap_in_header">{ getLabel('tenant_setting', 'show_roadmap_in_header') }</label>
|
||||
<div id="moderation" className="settingsGroup">
|
||||
<h4>{ I18n.t('site_settings.general.subtitle_moderation') }</h4>
|
||||
|
||||
<div className="formGroup">
|
||||
<div className="checkboxSwitch">
|
||||
<input {...register('allowAnonymousFeedback')} type="checkbox" id="allow_anonymous_feedback" />
|
||||
<label htmlFor="allow_anonymous_feedback">{ getLabel('tenant_setting', 'allow_anonymous_feedback') }</label>
|
||||
<SmallMutedText>
|
||||
{ I18n.t('site_settings.general.allow_anonymous_feedback_help') }
|
||||
</SmallMutedText>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<br />
|
||||
<div className="formGroup">
|
||||
<label htmlFor="collapseBoardsInHeader">{ getLabel('tenant_setting', 'collapse_boards_in_header') }</label>
|
||||
<select
|
||||
{...register('collapseBoardsInHeader')}
|
||||
id="collapseBoardsInHeader"
|
||||
className="selectPicker"
|
||||
>
|
||||
<option value={TENANT_SETTING_COLLAPSE_BOARDS_IN_HEADER_NO_COLLAPSE}>
|
||||
{ I18n.t('site_settings.general.collapse_boards_in_header_no_collapse') }
|
||||
</option>
|
||||
<option value={TENANT_SETTING_COLLAPSE_BOARDS_IN_HEADER_ALWAYS_COLLAPSE}>
|
||||
{ I18n.t('site_settings.general.collapse_boards_in_header_always_collapse') }
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<br />
|
||||
<h4>{ I18n.t('site_settings.general.subtitle_visibility') }</h4>
|
||||
|
||||
<div className="formGroup">
|
||||
<div className="checkboxSwitch">
|
||||
<input {...register('showVoteCount')} type="checkbox" id="show_vote_count_checkbox" />
|
||||
<label htmlFor="show_vote_count_checkbox">{ getLabel('tenant_setting', 'show_vote_count') }</label>
|
||||
<SmallMutedText>
|
||||
{ I18n.t('site_settings.general.show_vote_count_help') }
|
||||
</SmallMutedText>
|
||||
<div className="formGroup">
|
||||
<label htmlFor="feedbackApprovalPolicy">{ getLabel('tenant_setting', 'feedback_approval_policy') }</label>
|
||||
<select
|
||||
{...register('feedbackApprovalPolicy')}
|
||||
id="feedbackApprovalPolicy"
|
||||
className="selectPicker"
|
||||
>
|
||||
<option value="anonymous_require_approval">
|
||||
{ I18n.t('site_settings.general.feedback_approval_policy_anonymous_require_approval') }
|
||||
</option>
|
||||
<option value="never_require_approval">
|
||||
{ I18n.t('site_settings.general.feedback_approval_policy_never_require_approval') }
|
||||
</option>
|
||||
<option value="always_require_approval">
|
||||
{ I18n.t('site_settings.general.feedback_approval_policy_always_require_approval') }
|
||||
</option>
|
||||
</select>
|
||||
<SmallMutedText>
|
||||
{ I18n.t('site_settings.general.feedback_approval_policy_help') }
|
||||
</SmallMutedText>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<br />
|
||||
|
||||
<div className="formGroup">
|
||||
<div className="checkboxSwitch">
|
||||
<input {...register('showVoteButtonInBoard')} type="checkbox" id="show_vote_button_in_board_checkbox" />
|
||||
<label htmlFor="show_vote_button_in_board_checkbox">{ getLabel('tenant_setting', 'show_vote_button_in_board') }</label>
|
||||
<SmallMutedText>
|
||||
{ I18n.t('site_settings.general.show_vote_button_in_board_help') }
|
||||
</SmallMutedText>
|
||||
<div id="header" className="settingsGroup">
|
||||
<h4>{ I18n.t('site_settings.general.subtitle_header') }</h4>
|
||||
|
||||
<div className="formGroup">
|
||||
<div className="checkboxSwitch">
|
||||
<input {...register('showRoadmapInHeader')} type="checkbox" id="show_roadmap_in_header" />
|
||||
<label htmlFor="show_roadmap_in_header">{ getLabel('tenant_setting', 'show_roadmap_in_header') }</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="formGroup">
|
||||
<label htmlFor="collapseBoardsInHeader">{ getLabel('tenant_setting', 'collapse_boards_in_header') }</label>
|
||||
<select
|
||||
{...register('collapseBoardsInHeader')}
|
||||
id="collapseBoardsInHeader"
|
||||
className="selectPicker"
|
||||
>
|
||||
<option value={TENANT_SETTING_COLLAPSE_BOARDS_IN_HEADER_NO_COLLAPSE}>
|
||||
{ I18n.t('site_settings.general.collapse_boards_in_header_no_collapse') }
|
||||
</option>
|
||||
<option value={TENANT_SETTING_COLLAPSE_BOARDS_IN_HEADER_ALWAYS_COLLAPSE}>
|
||||
{ I18n.t('site_settings.general.collapse_boards_in_header_always_collapse') }
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<br />
|
||||
<div id="visibility" className="settingsGroup">
|
||||
<br />
|
||||
<h4>{ I18n.t('site_settings.general.subtitle_visibility') }</h4>
|
||||
|
||||
<div className="formGroup">
|
||||
<div className="checkboxSwitch">
|
||||
<input {...register('showPoweredBy')} type="checkbox" id="show_powered_by_checkbox" />
|
||||
<label htmlFor="show_powered_by_checkbox">{ getLabel('tenant_setting', 'show_powered_by') }</label>
|
||||
<div className="formGroup">
|
||||
<div className="checkboxSwitch">
|
||||
<input {...register('showVoteCount')} type="checkbox" id="show_vote_count_checkbox" />
|
||||
<label htmlFor="show_vote_count_checkbox">{ getLabel('tenant_setting', 'show_vote_count') }</label>
|
||||
<SmallMutedText>
|
||||
{ I18n.t('site_settings.general.show_vote_count_help') }
|
||||
</SmallMutedText>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="formGroup">
|
||||
<div className="checkboxSwitch">
|
||||
<input {...register('showVoteButtonInBoard')} type="checkbox" id="show_vote_button_in_board_checkbox" />
|
||||
<label htmlFor="show_vote_button_in_board_checkbox">{ getLabel('tenant_setting', 'show_vote_button_in_board') }</label>
|
||||
<SmallMutedText>
|
||||
{ I18n.t('site_settings.general.show_vote_button_in_board_help') }
|
||||
</SmallMutedText>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="formGroup">
|
||||
<div className="checkboxSwitch">
|
||||
<input {...register('showPoweredBy')} type="checkbox" id="show_powered_by_checkbox" />
|
||||
<label htmlFor="show_powered_by_checkbox">{ getLabel('tenant_setting', 'show_powered_by') }</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -43,7 +43,13 @@ const Tour = ({ userFullName }: Props) => {
|
||||
{
|
||||
target: '.siteSettingsDropdown',
|
||||
title: 'Site settings',
|
||||
content: 'Click "Site settings" to customize your feedback space. You can add custom boards and statuses, manage users, personalize appearance, and more.',
|
||||
content: 'Click "Site settings" to customize your feedback space. You can add custom boards and statuses, configure various settings, personalize appearance, and more.',
|
||||
disableBeacon: true,
|
||||
},
|
||||
{
|
||||
target: '.moderationDropdown',
|
||||
title: 'Moderation',
|
||||
content: 'Click "Moderation" to approve or reject submitted feedback and to manage users.',
|
||||
disableBeacon: true,
|
||||
},
|
||||
{
|
||||
@@ -107,8 +113,8 @@ const Tour = ({ userFullName }: Props) => {
|
||||
// Open profile navbar
|
||||
if (
|
||||
state.type === 'step:after' &&
|
||||
(((state.action === 'next' || state.action === 'close') && (state.step.target === '.sidebarFilters' || state.step.target === '.siteSettingsDropdown')) ||
|
||||
(state.action === 'prev' && state.step.target === '.tourDropdown'))
|
||||
(((state.action === 'next' || state.action === 'close') && (state.step.target === '.sidebarFilters' || state.step.target === '.siteSettingsDropdown' || state.step.target === '.moderationDropdown')) ||
|
||||
(state.action === 'prev' && (state.step.target === '.moderationDropdown' || state.step.target === '.tourDropdown')))
|
||||
) {
|
||||
if (vw < BOOTSTRAP_BREAKPOINT_SM) openBoardsNav();
|
||||
|
||||
|
||||
23
app/javascript/components/common/Badge.tsx
Normal file
23
app/javascript/components/common/Badge.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import * as React from 'react';
|
||||
|
||||
export const BADGE_TYPE_LIGHT = 'badgeLight';
|
||||
export const BADGE_TYPE_WARNING = 'badgeWarning';
|
||||
export const BADGE_TYPE_DANGER = 'badgeDanger';
|
||||
|
||||
export type BadgeTypes =
|
||||
typeof BADGE_TYPE_LIGHT |
|
||||
typeof BADGE_TYPE_WARNING |
|
||||
typeof BADGE_TYPE_DANGER;
|
||||
|
||||
interface Props {
|
||||
type: BadgeTypes;
|
||||
children: string;
|
||||
}
|
||||
|
||||
const Badge = ({ type, children }: Props) => (
|
||||
<span className={`badge ${type}`}>
|
||||
{children}
|
||||
</span>
|
||||
);
|
||||
|
||||
export default Badge;
|
||||
@@ -2,14 +2,21 @@ import * as React from 'react';
|
||||
import I18n from 'i18n-js';
|
||||
|
||||
import { BsReply } from 'react-icons/bs';
|
||||
import { FiEdit, FiDelete } from 'react-icons/fi';
|
||||
import { FiEdit, FiDelete, FiSettings } from 'react-icons/fi';
|
||||
import { ImCancelCircle } from 'react-icons/im';
|
||||
import { TbLock, TbLockOpen } from 'react-icons/tb';
|
||||
import { MdContentCopy, MdDone, MdOutlineArrowBack } from 'react-icons/md';
|
||||
import { GrTest, GrClearOption } from 'react-icons/gr';
|
||||
import { MdOutlineLibraryBooks } from "react-icons/md";
|
||||
import { MdVerified } from "react-icons/md";
|
||||
import { BiLike, BiSolidLike } from "react-icons/bi";
|
||||
import {
|
||||
MdContentCopy,
|
||||
MdDone,
|
||||
MdOutlineArrowBack,
|
||||
MdOutlineLibraryBooks,
|
||||
MdVerified,
|
||||
MdCheck,
|
||||
MdClear,
|
||||
} from 'react-icons/md';
|
||||
import { FaUserNinja } from "react-icons/fa";
|
||||
|
||||
export const EditIcon = () => <FiEdit />;
|
||||
|
||||
@@ -43,4 +50,12 @@ export const ClearIcon = () => <GrClearOption />;
|
||||
|
||||
export const LikeIcon = ({size = 32}) => <BiLike size={size} />;
|
||||
|
||||
export const SolidLikeIcon = ({size = 32}) => <BiSolidLike size={size} />;
|
||||
export const SolidLikeIcon = ({size = 32}) => <BiSolidLike size={size} />;
|
||||
|
||||
export const SettingsIcon = () => <FiSettings />;
|
||||
|
||||
export const AnonymousIcon = ({size = 32, title=I18n.t('defaults.user_full_name')}) => <FaUserNinja size={size} title={title} />;
|
||||
|
||||
export const ApproveIcon = () => <MdCheck />;
|
||||
|
||||
export const RejectIcon = () => <MdClear />;
|
||||
@@ -62,7 +62,6 @@ const mapDispatchToProps = (dispatch: any) => ({
|
||||
|
||||
deleteBoard(id: number, authenticityToken: string) {
|
||||
dispatch(deleteBoard(id, authenticityToken)).then(res => {
|
||||
console.log(res);
|
||||
if (res && res.status === HttpStatus.Accepted) {
|
||||
window.location.reload();
|
||||
}
|
||||
|
||||
34
app/javascript/containers/FeedbackModeration.tsx
Normal file
34
app/javascript/containers/FeedbackModeration.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import { connect } from "react-redux";
|
||||
|
||||
import FeedbackModerationP from "../components/Moderation/Feedback/FeedbackModerationP";
|
||||
|
||||
import { State } from "../reducers/rootReducer";
|
||||
import { requestPostsForModeration } from "../actions/Post/requestPosts";
|
||||
import { updatePostApprovalStatus } from "../actions/Post/updatePost";
|
||||
import { PostApprovalStatus } from "../interfaces/IPost";
|
||||
|
||||
const mapStateToProps = (state: State) => ({
|
||||
posts: state.moderation.feedback.posts,
|
||||
areLoading: state.moderation.feedback.areLoading,
|
||||
areUpdating: state.moderation.feedback.areUpdating,
|
||||
error: state.moderation.feedback.error,
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch: any) => ({
|
||||
requestPostsForModeration() {
|
||||
dispatch(requestPostsForModeration());
|
||||
},
|
||||
|
||||
updatePostApprovalStatus(
|
||||
id: number,
|
||||
approvalStatus: PostApprovalStatus,
|
||||
authenticityToken: string
|
||||
) {
|
||||
return dispatch(updatePostApprovalStatus(id, approvalStatus, authenticityToken));
|
||||
}
|
||||
});
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(FeedbackModerationP);
|
||||
@@ -3,7 +3,7 @@ import { connect } from "react-redux";
|
||||
import GeneralSiteSettingsP from "../components/SiteSettings/General/GeneralSiteSettingsP";
|
||||
import { updateTenant } from "../actions/Tenant/updateTenant";
|
||||
import { State } from "../reducers/rootReducer";
|
||||
import { TenantSettingBrandDisplay, TenantSettingCollapseBoardsInHeader } from "../interfaces/ITenantSetting";
|
||||
import { TenantSettingBrandDisplay, TenantSettingCollapseBoardsInHeader, TenantSettingFeedbackApprovalPolicy } from "../interfaces/ITenantSetting";
|
||||
|
||||
const mapStateToProps = (state: State) => ({
|
||||
areUpdating: state.siteSettings.general.areUpdating,
|
||||
@@ -18,6 +18,8 @@ const mapDispatchToProps = (dispatch: any) => ({
|
||||
locale: string,
|
||||
rootBoardId: number,
|
||||
customDomain: string,
|
||||
allowAnonymousFeedback: boolean,
|
||||
feedbackApprovalPolicy: TenantSettingFeedbackApprovalPolicy,
|
||||
showRoadmapInHeader: boolean,
|
||||
collapseBoardsInHeader: TenantSettingCollapseBoardsInHeader,
|
||||
showVoteCount: boolean,
|
||||
@@ -30,12 +32,14 @@ const mapDispatchToProps = (dispatch: any) => ({
|
||||
siteLogo,
|
||||
tenantSetting: {
|
||||
brand_display: brandDisplaySetting,
|
||||
root_board_id: rootBoardId,
|
||||
allow_anonymous_feedback: allowAnonymousFeedback,
|
||||
feedback_approval_policy: feedbackApprovalPolicy,
|
||||
show_roadmap_in_header: showRoadmapInHeader,
|
||||
collapse_boards_in_header: collapseBoardsInHeader,
|
||||
show_vote_count: showVoteCount,
|
||||
show_vote_button_in_board: showVoteButtonInBoard,
|
||||
show_powered_by: showPoweredBy,
|
||||
root_board_id: rootBoardId,
|
||||
show_roadmap_in_header: showRoadmapInHeader,
|
||||
collapse_boards_in_header: collapseBoardsInHeader,
|
||||
},
|
||||
locale,
|
||||
customDomain,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { connect } from "react-redux";
|
||||
|
||||
import UsersSiteSettingsP from "../components/SiteSettings/Users/UsersSiteSettingsP";
|
||||
import UsersModerationP from "../components/Moderation/Users/UsersModerationP";
|
||||
|
||||
import { requestUsers } from "../actions/User/requestUsers";
|
||||
import { updateUser } from "../actions/User/updateUser";
|
||||
@@ -9,8 +9,8 @@ import { State } from "../reducers/rootReducer";
|
||||
|
||||
const mapStateToProps = (state: State) => ({
|
||||
users: state.users,
|
||||
settingsAreUpdating: state.siteSettings.users.areUpdating,
|
||||
settingsError: state.siteSettings.users.error,
|
||||
settingsAreUpdating: state.moderation.users.areUpdating,
|
||||
settingsError: state.moderation.users.error,
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch: any) => ({
|
||||
@@ -46,4 +46,4 @@ const mapDispatchToProps = (dispatch: any) => ({
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(UsersSiteSettingsP);
|
||||
)(UsersModerationP);
|
||||
@@ -1,8 +1,19 @@
|
||||
// Approval status
|
||||
export const POST_APPROVAL_STATUS_APPROVED = 'approved';
|
||||
export const POST_APPROVAL_STATUS_PENDING = 'pending';
|
||||
export const POST_APPROVAL_STATUS_REJECTED = 'rejected';
|
||||
|
||||
export type PostApprovalStatus =
|
||||
typeof POST_APPROVAL_STATUS_APPROVED |
|
||||
typeof POST_APPROVAL_STATUS_PENDING |
|
||||
typeof POST_APPROVAL_STATUS_REJECTED;
|
||||
|
||||
interface IPost {
|
||||
id: number;
|
||||
title: string;
|
||||
slug?: string;
|
||||
description?: string;
|
||||
approvalStatus: PostApprovalStatus;
|
||||
boardId: number;
|
||||
postStatusId?: number;
|
||||
likeCount: number;
|
||||
|
||||
@@ -10,6 +10,16 @@ export type TenantSettingBrandDisplay =
|
||||
typeof TENANT_SETTING_BRAND_DISPLAY_LOGO_ONLY |
|
||||
typeof TENANT_SETTING_BRAND_DISPLAY_NONE;
|
||||
|
||||
// Feedback approval policy
|
||||
export const TENANT_SETTING_FEEDBACK_APPROVAL_POLICY_ANONYMOUS_REQUIRE_APPROVAL = 'anonymous_require_approval';
|
||||
export const TENANT_SETTING_FEEDBACK_APPROVAL_POLICY_NEVER_REQUIRE_APPROVAL = 'never_require_approval';
|
||||
export const TENANT_SETTING_FEEDBACK_APPROVAL_POLICY_ALWAYS_REQUIRE_APPROVAL = 'always_require_approval';
|
||||
|
||||
export type TenantSettingFeedbackApprovalPolicy =
|
||||
typeof TENANT_SETTING_FEEDBACK_APPROVAL_POLICY_ANONYMOUS_REQUIRE_APPROVAL |
|
||||
typeof TENANT_SETTING_FEEDBACK_APPROVAL_POLICY_NEVER_REQUIRE_APPROVAL |
|
||||
typeof TENANT_SETTING_FEEDBACK_APPROVAL_POLICY_ALWAYS_REQUIRE_APPROVAL;
|
||||
|
||||
// Collapse boards in header
|
||||
export const TENANT_SETTING_COLLAPSE_BOARDS_IN_HEADER_NO_COLLAPSE = 'no_collapse';
|
||||
export const TENANT_SETTING_COLLAPSE_BOARDS_IN_HEADER_ALWAYS_COLLAPSE = 'always_collapse';
|
||||
@@ -22,6 +32,8 @@ export type TenantSettingCollapseBoardsInHeader =
|
||||
interface ITenantSetting {
|
||||
brand_display?: TenantSettingBrandDisplay;
|
||||
root_board_id?: number;
|
||||
allow_anonymous_feedback?: boolean;
|
||||
feedback_approval_policy?: TenantSettingFeedbackApprovalPolicy;
|
||||
show_vote_count?: boolean;
|
||||
show_vote_button_in_board?: boolean;
|
||||
show_roadmap_in_header?: boolean;
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import { PostApprovalStatus } from "../IPost";
|
||||
|
||||
interface IPostJSON {
|
||||
id: number;
|
||||
title: string;
|
||||
slug?: string;
|
||||
description?: string;
|
||||
approval_status: PostApprovalStatus;
|
||||
board_id: number;
|
||||
post_status_id?: number;
|
||||
likes_count: number;
|
||||
|
||||
67
app/javascript/reducers/Moderation/feedbackReducer.ts
Normal file
67
app/javascript/reducers/Moderation/feedbackReducer.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import { postRequestSuccess } from "../../actions/Post/requestPost";
|
||||
import { POSTS_REQUEST_FAILURE, POSTS_REQUEST_START, POSTS_REQUEST_SUCCESS, PostsRequestActionTypes } from "../../actions/Post/requestPosts";
|
||||
import { POST_UPDATE_FAILURE, POST_UPDATE_START, POST_UPDATE_SUCCESS, PostUpdateActionTypes, postUpdateSuccess } from "../../actions/Post/updatePost";
|
||||
import IPost from "../../interfaces/IPost";
|
||||
import postReducer from "../postReducer";
|
||||
|
||||
export interface ModerationFeedbackState {
|
||||
posts: Array<IPost>;
|
||||
areLoading: boolean;
|
||||
areUpdating: boolean;
|
||||
error: string;
|
||||
}
|
||||
|
||||
const initialState: ModerationFeedbackState = {
|
||||
posts: [],
|
||||
areLoading: true,
|
||||
areUpdating: false,
|
||||
error: '',
|
||||
};
|
||||
|
||||
const moderationFeedbackReducer = (
|
||||
state = initialState,
|
||||
action: PostsRequestActionTypes | PostUpdateActionTypes,
|
||||
) => {
|
||||
switch (action.type) {
|
||||
case POSTS_REQUEST_START:
|
||||
return {
|
||||
...state,
|
||||
areLoading: true,
|
||||
};
|
||||
|
||||
case POST_UPDATE_START:
|
||||
return {
|
||||
...state,
|
||||
areUpdating: true,
|
||||
};
|
||||
|
||||
case POSTS_REQUEST_SUCCESS:
|
||||
return {
|
||||
...state,
|
||||
areLoading: false,
|
||||
error: '',
|
||||
posts: action.posts.map(post => postReducer(undefined, postRequestSuccess(post))),
|
||||
};
|
||||
|
||||
case POST_UPDATE_SUCCESS:
|
||||
return {
|
||||
...state,
|
||||
areUpdating: false,
|
||||
error: '',
|
||||
posts: state.posts.map(post => post.id === action.post.id ? postReducer(post, postUpdateSuccess(action.post)) : post),
|
||||
};
|
||||
|
||||
case POSTS_REQUEST_FAILURE:
|
||||
case POST_UPDATE_FAILURE:
|
||||
return {
|
||||
...state,
|
||||
areLoading: false,
|
||||
error: action.error,
|
||||
};
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
export default moderationFeedbackReducer;
|
||||
@@ -5,17 +5,17 @@ import {
|
||||
USER_UPDATE_FAILURE,
|
||||
} from '../../actions/User/updateUser';
|
||||
|
||||
export interface SiteSettingsUsersState {
|
||||
export interface ModerationUsersState {
|
||||
areUpdating: boolean;
|
||||
error: string;
|
||||
}
|
||||
|
||||
const initialState: SiteSettingsUsersState = {
|
||||
const initialState: ModerationUsersState = {
|
||||
areUpdating: false,
|
||||
error: '',
|
||||
};
|
||||
|
||||
const siteSettingsUsersReducer = (
|
||||
const moderationUsersReducer = (
|
||||
state = initialState,
|
||||
action: UserUpdateActionTypes,
|
||||
) => {
|
||||
@@ -45,4 +45,4 @@ const siteSettingsUsersReducer = (
|
||||
}
|
||||
}
|
||||
|
||||
export default siteSettingsUsersReducer;
|
||||
export default moderationUsersReducer;
|
||||
55
app/javascript/reducers/moderationReducer.ts
Normal file
55
app/javascript/reducers/moderationReducer.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import { POSTS_REQUEST_FAILURE, POSTS_REQUEST_START, POSTS_REQUEST_SUCCESS, PostsRequestActionTypes } from '../actions/Post/requestPosts';
|
||||
import { POST_UPDATE_FAILURE, POST_UPDATE_START, POST_UPDATE_SUCCESS, PostUpdateActionTypes } from '../actions/Post/updatePost';
|
||||
import {
|
||||
UserUpdateActionTypes,
|
||||
USER_UPDATE_START,
|
||||
USER_UPDATE_SUCCESS,
|
||||
USER_UPDATE_FAILURE,
|
||||
} from '../actions/User/updateUser';
|
||||
|
||||
import moderationFeedbackReducer, { ModerationFeedbackState } from './Moderation/feedbackReducer';
|
||||
import moderationUsersReducer, { ModerationUsersState } from './Moderation/usersReducer';
|
||||
|
||||
interface ModerationState {
|
||||
feedback: ModerationFeedbackState;
|
||||
users: ModerationUsersState;
|
||||
}
|
||||
|
||||
const initialState: ModerationState = {
|
||||
feedback: moderationFeedbackReducer(undefined, {} as any),
|
||||
users: moderationUsersReducer(undefined, {} as any),
|
||||
};
|
||||
|
||||
const moderationReducer = (
|
||||
state = initialState,
|
||||
action:
|
||||
PostsRequestActionTypes |
|
||||
PostUpdateActionTypes |
|
||||
UserUpdateActionTypes
|
||||
): ModerationState => {
|
||||
switch (action.type) {
|
||||
case POSTS_REQUEST_START:
|
||||
case POSTS_REQUEST_SUCCESS:
|
||||
case POSTS_REQUEST_FAILURE:
|
||||
case POST_UPDATE_START:
|
||||
case POST_UPDATE_SUCCESS:
|
||||
case POST_UPDATE_FAILURE:
|
||||
return {
|
||||
...state,
|
||||
feedback: moderationFeedbackReducer(state.feedback, action),
|
||||
};
|
||||
|
||||
case USER_UPDATE_START:
|
||||
case USER_UPDATE_SUCCESS:
|
||||
case USER_UPDATE_FAILURE:
|
||||
return {
|
||||
...state,
|
||||
users: moderationUsersReducer(state.users, action),
|
||||
};
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
export default moderationReducer;
|
||||
@@ -8,13 +8,14 @@ import {
|
||||
POST_UPDATE_SUCCESS,
|
||||
} from '../actions/Post/updatePost';
|
||||
|
||||
import IPost from '../interfaces/IPost';
|
||||
import IPost, { POST_APPROVAL_STATUS_APPROVED } from '../interfaces/IPost';
|
||||
|
||||
const initialState: IPost = {
|
||||
id: 0,
|
||||
title: '',
|
||||
slug: null,
|
||||
description: null,
|
||||
approvalStatus: POST_APPROVAL_STATUS_APPROVED,
|
||||
boardId: 0,
|
||||
postStatusId: null,
|
||||
likeCount: 0,
|
||||
@@ -40,6 +41,7 @@ const postReducer = (
|
||||
title: action.post.title,
|
||||
slug: action.post.slug,
|
||||
description: action.post.description,
|
||||
approvalStatus: action.post.approval_status,
|
||||
boardId: action.post.board_id,
|
||||
postStatusId: action.post.post_status_id,
|
||||
likeCount: action.post.likes_count,
|
||||
@@ -59,6 +61,7 @@ const postReducer = (
|
||||
description: action.post.description,
|
||||
boardId: action.post.board_id,
|
||||
postStatusId: action.post.post_status_id,
|
||||
approvalStatus: action.post.approval_status,
|
||||
};
|
||||
|
||||
default:
|
||||
|
||||
@@ -7,8 +7,9 @@ import boardsReducer from './boardsReducer';
|
||||
import postStatusesReducer from './postStatusesReducer';
|
||||
import usersReducer from './usersReducer';
|
||||
import currentPostReducer from './currentPostReducer';
|
||||
import siteSettingsReducer from './siteSettingsReducer';
|
||||
import oAuthsReducer from './oAuthsReducer';
|
||||
import siteSettingsReducer from './siteSettingsReducer';
|
||||
import moderationReducer from './moderationReducer';
|
||||
|
||||
const rootReducer = combineReducers({
|
||||
tenantSignUp: tenantSignUpReducer,
|
||||
@@ -18,8 +19,10 @@ const rootReducer = combineReducers({
|
||||
postStatuses: postStatusesReducer,
|
||||
users: usersReducer,
|
||||
currentPost: currentPostReducer,
|
||||
siteSettings: siteSettingsReducer,
|
||||
oAuths: oAuthsReducer,
|
||||
|
||||
siteSettings: siteSettingsReducer,
|
||||
moderation: moderationReducer,
|
||||
});
|
||||
|
||||
export type State = ReturnType<typeof rootReducer>
|
||||
|
||||
@@ -61,13 +61,6 @@ import {
|
||||
POSTSTATUS_UPDATE_FAILURE,
|
||||
} from '../actions/PostStatus/updatePostStatus';
|
||||
|
||||
import {
|
||||
UserUpdateActionTypes,
|
||||
USER_UPDATE_START,
|
||||
USER_UPDATE_SUCCESS,
|
||||
USER_UPDATE_FAILURE,
|
||||
} from '../actions/User/updateUser';
|
||||
|
||||
import {
|
||||
OAuthSubmitActionTypes,
|
||||
OAUTH_SUBMIT_START,
|
||||
@@ -93,7 +86,6 @@ import siteSettingsGeneralReducer, { SiteSettingsGeneralState } from './SiteSett
|
||||
import siteSettingsBoardsReducer, { SiteSettingsBoardsState } from './SiteSettings/boardsReducer';
|
||||
import siteSettingsPostStatusesReducer, { SiteSettingsPostStatusesState } from './SiteSettings/postStatusesReducer';
|
||||
import siteSettingsRoadmapReducer, { SiteSettingsRoadmapState } from './SiteSettings/roadmapReducer';
|
||||
import siteSettingsUsersReducer, { SiteSettingsUsersState } from './SiteSettings/usersReducer';
|
||||
import siteSettingsAuthenticationReducer, { SiteSettingsAuthenticationState } from './SiteSettings/authenticationReducer';
|
||||
import siteSettingsAppearanceReducer, { SiteSettingsAppearanceState } from './SiteSettings/appearanceReducer';
|
||||
|
||||
@@ -104,7 +96,6 @@ interface SiteSettingsState {
|
||||
postStatuses: SiteSettingsPostStatusesState;
|
||||
roadmap: SiteSettingsRoadmapState;
|
||||
appearance: SiteSettingsAppearanceState;
|
||||
users: SiteSettingsUsersState;
|
||||
}
|
||||
|
||||
const initialState: SiteSettingsState = {
|
||||
@@ -114,7 +105,6 @@ const initialState: SiteSettingsState = {
|
||||
postStatuses: siteSettingsPostStatusesReducer(undefined, {} as any),
|
||||
roadmap: siteSettingsRoadmapReducer(undefined, {} as any),
|
||||
appearance: siteSettingsAppearanceReducer(undefined, {} as any),
|
||||
users: siteSettingsUsersReducer(undefined, {} as any),
|
||||
};
|
||||
|
||||
const siteSettingsReducer = (
|
||||
@@ -131,8 +121,7 @@ const siteSettingsReducer = (
|
||||
PostStatusOrderUpdateActionTypes |
|
||||
PostStatusDeleteActionTypes |
|
||||
PostStatusSubmitActionTypes |
|
||||
PostStatusUpdateActionTypes |
|
||||
UserUpdateActionTypes
|
||||
PostStatusUpdateActionTypes
|
||||
): SiteSettingsState => {
|
||||
switch (action.type) {
|
||||
case TENANT_UPDATE_START:
|
||||
@@ -198,14 +187,6 @@ const siteSettingsReducer = (
|
||||
roadmap: siteSettingsRoadmapReducer(state.roadmap, action),
|
||||
};
|
||||
|
||||
case USER_UPDATE_START:
|
||||
case USER_UPDATE_SUCCESS:
|
||||
case USER_UPDATE_FAILURE:
|
||||
return {
|
||||
...state,
|
||||
users: siteSettingsUsersReducer(state.users, action),
|
||||
};
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user