Add post status administration (#105)

This commit is contained in:
Riccardo Graziosi
2022-05-01 18:00:38 +02:00
committed by GitHub
parent c5148147e3
commit 5256ea911a
47 changed files with 1580 additions and 32 deletions

View File

@@ -6,22 +6,10 @@
# you're free to overwrite the RESTful controller actions.
module Admin
class ApplicationController < Administrate::ApplicationController
include ApplicationHelper
before_action :authenticate_admin
def authenticate_admin
unless user_signed_in?
flash[:alert] = 'You must be logged in to access this page.'
redirect_to new_user_session_path
return
end
unless current_user.moderator? || current_user.admin?
flash[:alert] = 'You do not have the privilegies to access this page.'
redirect_to root_path
return
end
end
# Override this value to specify the number of elements to display at a time
# on index pages. Defaults to 20.
# def records_per_page

View File

@@ -1,7 +1,75 @@
class PostStatusesController < ApplicationController
include ApplicationHelper
before_action :authenticate_admin, only: [:create, :update, :update_order, :destroy]
def index
post_statuses = PostStatus.order(order: :asc)
render json: post_statuses
end
end
def create
post_status = PostStatus.new(post_status_params)
if post_status.save
render json: post_status, status: :created
else
render json: {
error: I18n.t('errors.post_status.create', message: post_status.errors.full_messages)
}, status: :unprocessable_entity
end
end
def update
post_status = PostStatus.find(params[:id])
post_status.assign_attributes(post_status_params)
if post_status.save
render json: post_status, status: :ok
else
render json: {
error: I18n.t('errors.post_status.update', message: post_status.errors.full_messages)
}, status: :unprocessable_entity
end
end
def destroy
post_status = PostStatus.find(params[:id])
if post_status.destroy
render json: {
id: params[:id]
}, status: :accepted
else
render json: {
error: I18n.t('errors.post_statuses.destroy', message: post_status.errors.full_messages)
}, status: :unprocessable_entity
end
end
def update_order
workflow_output = ReorderWorkflow.new(
entity_classname: PostStatus,
column_name: 'order',
entity_id: params[:post_status][:id],
src_index: params[:post_status][:src_index],
dst_index: params[:post_status][:dst_index]
).run
if workflow_output
render json: workflow_output
else
render json: {
error: I18n.t("errors.post_status.update_order")
}, status: :unprocessable_entity
end
end
private
def post_status_params
params
.require(:post_status)
.permit(:name, :color)
end
end

View File

@@ -0,0 +1,11 @@
class SiteSettingsController < ApplicationController
include ApplicationHelper
before_action :authenticate_admin
def general
end
def post_statuses
end
end

View File

@@ -1,2 +1,15 @@
module ApplicationHelper
def authenticate_admin
unless user_signed_in?
flash[:alert] = 'You must be logged in to access this page.'
redirect_to new_user_session_path
return
end
unless current_user.moderator? || current_user.admin?
flash[:alert] = 'You do not have the privilegies to access this page.'
redirect_to root_path
return
end
end
end

View File

@@ -0,0 +1,62 @@
import { Action } from "redux";
import { ThunkAction } from "redux-thunk";
import buildRequestHeaders from "../helpers/buildRequestHeaders";
import { State } from "../reducers/rootReducer";
export const POST_STATUS_DELETE_START = 'POST_STATUS_DELETE_START';
interface PostStatusDeleteStartAction {
type: typeof POST_STATUS_DELETE_START;
}
export const POST_STATUS_DELETE_SUCCESS = 'POST_STATUS_DELETE_SUCCESS';
interface PostStatusDeleteSuccessAction {
type: typeof POST_STATUS_DELETE_SUCCESS;
id: number;
}
export const POST_STATUS_DELETE_FAILURE = 'POST_STATUS_DELETE_FAILURE';
interface PostStatusDeleteFailureAction {
type: typeof POST_STATUS_DELETE_FAILURE;
error: string;
}
export type PostStatusDeleteActionTypes =
PostStatusDeleteStartAction |
PostStatusDeleteSuccessAction |
PostStatusDeleteFailureAction;
const postStatusDeleteStart = (): PostStatusDeleteStartAction => ({
type: POST_STATUS_DELETE_START,
});
const postStatusDeleteSuccess = (
id: number,
): PostStatusDeleteSuccessAction => ({
type: POST_STATUS_DELETE_SUCCESS,
id,
});
const postStatusDeleteFailure = (error: string): PostStatusDeleteFailureAction => ({
type: POST_STATUS_DELETE_FAILURE,
error,
});
export const deletePostStatus = (
id: number,
authenticityToken: string,
): ThunkAction<void, State, null, Action<string>> => (
async (dispatch) => {
dispatch(postStatusDeleteStart());
try {
const response = await fetch(`/post_statuses/${id}`, {
method: 'DELETE',
headers: buildRequestHeaders(authenticityToken),
});
const json = await response.json();
dispatch(postStatusDeleteSuccess(id));
} catch (e) {
dispatch(postStatusDeleteFailure(e));
}
}
);

View File

@@ -4,6 +4,7 @@ import { State } from '../reducers/rootReducer';
import ICommentJSON from '../interfaces/json/IComment';
import buildRequestHeaders from '../helpers/buildRequestHeaders';
import HttpStatus from '../constants/http_status';
export const COMMENT_SUBMIT_START = 'COMMENT_SUBMIT_START';
interface CommentSubmitStartAction {
@@ -68,7 +69,7 @@ export const submitComment = (
});
const json = await res.json();
if (res.status === 201) {
if (res.status === HttpStatus.Created) {
dispatch(commentSubmitSuccess(json));
} else {
dispatch(commentSubmitFailure(parentId, json.error));

View File

@@ -4,6 +4,7 @@ import { ThunkAction } from "redux-thunk";
import { State } from "../reducers/rootReducer";
import ILikeJSON from "../interfaces/json/ILike";
import buildRequestHeaders from "../helpers/buildRequestHeaders";
import HttpStatus from "../constants/http_status";
export const LIKE_SUBMIT_SUCCESS = 'LIKE_SUBMIT_SUCCESS';
interface LikeSubmitSuccessAction {
@@ -38,7 +39,7 @@ export const submitLike = (
});
const json = await res.json();
if (res.status === 201 || res.status === 202)
if (res.status === HttpStatus.Created || res.status === HttpStatus.Accepted)
dispatch(likeSubmitSuccess(postId, isLike, json));
} catch (e) {
console.log('An error occurred while liking a post');

View File

@@ -0,0 +1,78 @@
import { Action } from "redux";
import { ThunkAction } from "redux-thunk";
import HttpStatus from "../constants/http_status";
import buildRequestHeaders from "../helpers/buildRequestHeaders";
import IPostStatusJSON from "../interfaces/json/IPostStatus";
import { State } from "../reducers/rootReducer";
export const POSTSTATUS_SUBMIT_START = 'POSTSTATUS_SUBMIT_START';
interface PostStatusSubmitStartAction {
type: typeof POSTSTATUS_SUBMIT_START;
}
export const POSTSTATUS_SUBMIT_SUCCESS = 'POSTSTATUS_SUBMIT_SUCCESS';
interface PostStatusSubmitSuccessAction {
type: typeof POSTSTATUS_SUBMIT_SUCCESS;
postStatus: IPostStatusJSON;
}
export const POSTSTATUS_SUBMIT_FAILURE = 'POSTSTATUS_SUBMIT_FAILURE';
interface PostStatusSubmitFailureAction {
type: typeof POSTSTATUS_SUBMIT_FAILURE;
error: string;
}
export type PostStatusSubmitActionTypes =
PostStatusSubmitStartAction |
PostStatusSubmitSuccessAction |
PostStatusSubmitFailureAction;
const postStatusSubmitStart = (): PostStatusSubmitStartAction => ({
type: POSTSTATUS_SUBMIT_START,
});
const postStatusSubmitSuccess = (
postStatusJSON: IPostStatusJSON,
): PostStatusSubmitSuccessAction => ({
type: POSTSTATUS_SUBMIT_SUCCESS,
postStatus: postStatusJSON,
});
const postStatusSubmitFailure = (error: string): PostStatusSubmitFailureAction => ({
type: POSTSTATUS_SUBMIT_FAILURE,
error,
});
export const submitPostStatus = (
name: string,
color: string,
authenticityToken: string,
): ThunkAction<void, State, null, Action<string>> => async (dispatch) => {
dispatch(postStatusSubmitStart());
try {
const res = await fetch(`/post_statuses`, {
method: 'POST',
headers: buildRequestHeaders(authenticityToken),
body: JSON.stringify({
post_status: {
name,
color,
},
}),
});
const json = await res.json();
if (res.status === HttpStatus.Created) {
dispatch(postStatusSubmitSuccess(json));
} else {
dispatch(postStatusSubmitFailure(json.error));
}
return Promise.resolve(res);
} catch (e) {
dispatch(postStatusSubmitFailure(e));
return Promise.resolve(null);
}
};

View File

@@ -0,0 +1,79 @@
import { Action } from "redux";
import { ThunkAction } from "redux-thunk";
import HttpStatus from "../constants/http_status";
import buildRequestHeaders from "../helpers/buildRequestHeaders";
import IPostStatusJSON from "../interfaces/json/IPostStatus";
import { State } from "../reducers/rootReducer";
export const POSTSTATUS_UPDATE_START = 'POSTSTATUS_UPDATE_START';
interface PostStatusUpdateStartAction {
type: typeof POSTSTATUS_UPDATE_START;
}
export const POSTSTATUS_UPDATE_SUCCESS = 'POSTSTATUS_UPDATE_SUCCESS';
interface PostStatusUpdateSuccessAction {
type: typeof POSTSTATUS_UPDATE_SUCCESS;
postStatus: IPostStatusJSON;
}
export const POSTSTATUS_UPDATE_FAILURE = 'POSTSTATUS_UPDATE_FAILURE';
interface PostStatusUpdateFailureAction {
type: typeof POSTSTATUS_UPDATE_FAILURE;
error: string;
}
export type PostStatusUpdateActionTypes =
PostStatusUpdateStartAction |
PostStatusUpdateSuccessAction |
PostStatusUpdateFailureAction;
const postStatusUpdateStart = (): PostStatusUpdateStartAction => ({
type: POSTSTATUS_UPDATE_START,
});
const postStatusUpdateSuccess = (
postStatusJSON: IPostStatusJSON,
): PostStatusUpdateSuccessAction => ({
type: POSTSTATUS_UPDATE_SUCCESS,
postStatus: postStatusJSON,
});
const postStatusUpdateFailure = (error: string): PostStatusUpdateFailureAction => ({
type: POSTSTATUS_UPDATE_FAILURE,
error,
});
export const updatePostStatus = (
id: number,
name: string,
color: string,
authenticityToken: string,
): ThunkAction<void, State, null, Action<string>> => async (dispatch) => {
dispatch(postStatusUpdateStart());
try {
const res = await fetch(`/post_statuses/${id}`, {
method: 'PATCH',
headers: buildRequestHeaders(authenticityToken),
body: JSON.stringify({
post_status: {
name,
color,
},
}),
});
const json = await res.json();
if (res.status === HttpStatus.OK) {
dispatch(postStatusUpdateSuccess(json));
} else {
dispatch(postStatusUpdateFailure(json.error));
}
return Promise.resolve(res);
} catch (e) {
dispatch(postStatusUpdateFailure(e));
return Promise.resolve(null);
}
};

View File

@@ -0,0 +1,96 @@
import { Action } from "redux";
import { ThunkAction } from "redux-thunk";
import HttpStatus from "../constants/http_status";
import buildRequestHeaders from "../helpers/buildRequestHeaders";
import IPostStatus from "../interfaces/IPostStatus";
import { State } from "../reducers/rootReducer";
export const POSTSTATUS_ORDER_UPDATE_START = 'POSTSTATUS_ORDER_UPDATE_START';
interface PostStatusOrderUpdateStartAction {
type: typeof POSTSTATUS_ORDER_UPDATE_START;
newOrder: Array<IPostStatus>;
}
export const POSTSTATUS_ORDER_UPDATE_SUCCESS = 'POSTSTATUS_ORDER_UPDATE_SUCCESS';
interface PostStatusOrderUpdateSuccessAction {
type: typeof POSTSTATUS_ORDER_UPDATE_SUCCESS;
}
export const POSTSTATUS_ORDER_UPDATE_FAILURE = 'POSTSTATUS_ORDER_UPDATE_FAILURE';
interface PostStatusOrderUpdateFailureAction {
type: typeof POSTSTATUS_ORDER_UPDATE_FAILURE;
error: string;
}
export type PostStatusOrderUpdateActionTypes =
PostStatusOrderUpdateStartAction |
PostStatusOrderUpdateSuccessAction |
PostStatusOrderUpdateFailureAction;
const postStatusOrderUpdateStart = (
newOrder: Array<IPostStatus>
): PostStatusOrderUpdateStartAction => ({
type: POSTSTATUS_ORDER_UPDATE_START,
newOrder,
});
const postStatusOrderUpdateSuccess = (): PostStatusOrderUpdateSuccessAction => ({
type: POSTSTATUS_ORDER_UPDATE_SUCCESS,
});
const postStatusOrderUpdateFailure = (
error: string
): PostStatusOrderUpdateFailureAction => ({
type: POSTSTATUS_ORDER_UPDATE_FAILURE,
error,
});
export const updatePostStatusOrder = (
id: number,
postStatuses: Array<IPostStatus>,
sourceIndex: number,
destinationIndex: number,
authenticityToken: string,
): ThunkAction<void, State, null, Action<string>> => async (dispatch) => {
let newOrder = createNewOrder(postStatuses, sourceIndex, destinationIndex);
dispatch(postStatusOrderUpdateStart(newOrder));
try {
const res = await fetch(`/post_statuses/update_order`, {
method: 'PATCH',
headers: buildRequestHeaders(authenticityToken),
body: JSON.stringify({
post_status: {
id: id,
src_index: sourceIndex,
dst_index: destinationIndex,
},
}),
});
const json = await res.json();
if (res.status === HttpStatus.OK) {
dispatch(postStatusOrderUpdateSuccess());
} else {
dispatch(postStatusOrderUpdateFailure(json.error));
}
} catch (e) {
dispatch(postStatusOrderUpdateFailure(e));
}
};
function createNewOrder(
oldOrder: Array<IPostStatus>,
sourceIndex: number,
destinationIndex: number
) {
let newOrder = JSON.parse(JSON.stringify(oldOrder));
const [reorderedItem] = newOrder.splice(sourceIndex, 1);
newOrder.splice(destinationIndex, 0, reorderedItem);
return newOrder;
}

View File

@@ -11,6 +11,7 @@ import Button from '../shared/Button';
import IBoard from '../../interfaces/IBoard';
import buildRequestHeaders from '../../helpers/buildRequestHeaders';
import HttpStatus from '../../constants/http_status';
interface Props {
board: IBoard;
@@ -106,7 +107,7 @@ class NewPost extends React.Component<Props, State> {
const json = await res.json();
this.setState({isLoading: false});
if (res.status === 201) {
if (res.status === HttpStatus.Created) {
this.setState({
success: 'Post published! You will be redirected soon...',

View File

@@ -0,0 +1,113 @@
import * as React from 'react';
import { Draggable } from 'react-beautiful-dnd';
import PostStatusLabel from "../../shared/PostStatusLabel";
import DragZone from '../../shared/DragZone';
import Separator from '../../shared/Separator';
import PostStatusForm from './PostStatusForm';
interface Props {
id: number;
name: string;
color: string;
index: number;
settingsAreUpdating: boolean;
handleUpdate(
id: number,
name: string,
color: string,
onSuccess: Function,
): void;
handleDelete(id: number): void;
}
interface State {
editMode: boolean;
}
class PostStatusEditable extends React.Component<Props, State> {
constructor(props) {
super(props);
this.state = {
editMode: false,
};
this.toggleEditMode = this.toggleEditMode.bind(this);
this.handleUpdate = this.handleUpdate.bind(this);
}
toggleEditMode() {
this.setState({editMode: !this.state.editMode});
}
handleUpdate(id: number, name: string, color: string) {
this.props.handleUpdate(
id,
name,
color,
() => this.setState({editMode: false}),
);
}
render() {
const {
id,
name,
color,
index,
settingsAreUpdating,
handleDelete
} = this.props;
const {editMode} = this.state;
return (
<Draggable key={id} draggableId={id.toString()} index={index} isDragDisabled={settingsAreUpdating}>
{provided => (
<li className="postStatusEditable" ref={provided.innerRef} {...provided.draggableProps}>
<DragZone dndProvided={provided} isDragDisabled={settingsAreUpdating} />
{ editMode === false ?
<React.Fragment>
<PostStatusLabel name={name} color={color} />
<div className="postStatusEditableActions">
<a onClick={this.toggleEditMode}>Edit</a>
<Separator />
<a
onClick={() => handleDelete(id)}
data-confirm="Are you sure?"
>
Delete
</a>
</div>
</React.Fragment>
:
<React.Fragment>
<PostStatusForm
mode='update'
id={id}
name={name}
color={color}
handleUpdate={this.handleUpdate}
/>
<a
className="postStatusFormCancelButton"
onClick={this.toggleEditMode}>
Cancel
</a>
</React.Fragment>
}
</li>
)}
</Draggable>
);
}
}
export default PostStatusEditable;

View File

@@ -0,0 +1,115 @@
import * as React from 'react';
import Button from '../../shared/Button';
interface Props {
mode: 'create' | 'update';
id?: number;
name?: string;
color?: string;
handleSubmit?(
name: string,
color: string,
onSuccess: Function,
): void;
handleUpdate?(
id: number,
name: string,
color: string,
): void;
}
interface State {
name: string;
color: string;
}
class PostStatusForm extends React.Component<Props, State> {
initialState: State = {
name: '',
color: this.getRandomColor(),
};
constructor(props: Props) {
super(props);
this.state = this.props.mode === 'create' ?
this.initialState
:
{
name: this.props.name,
color: this.props.color,
};
this.onSubmit = this.onSubmit.bind(this);
}
getRandomColor() {
return '#' + (Math.random() * 0xFFFFFF << 0).toString(16).padStart(6, '0');
}
isFormValid() {
return this.state.name && this.state.name.length > 0 &&
this.state.color && this.state.color.length === 7;
}
onNameChange(nameText: string) {
this.setState({
name: nameText,
});
}
onColorChange(colorText: string) {
this.setState({
color: colorText,
});
}
onSubmit() {
if (this.props.mode === 'create') {
this.props.handleSubmit(
this.state.name,
this.state.color,
() => this.setState({...this.initialState, color: this.getRandomColor()}),
);
} else {
this.props.handleUpdate(this.props.id, this.state.name, this.state.color);
}
}
render() {
const {mode} = this.props;
const {name, color} = this.state;
return (
<div className="postStatusForm">
<input
type="text"
placeholder="Name"
value={name}
onChange={e => this.onNameChange(e.target.value)}
className="form-control"
/>
<input
type="color"
value={color}
onChange={e => this.onColorChange(e.target.value)}
className="form-control"
/>
<Button
onClick={this.onSubmit}
className="newPostStatusButton"
disabled={!this.isFormValid()}
>
{mode === 'create' ? 'Create' : 'Save'}
</Button>
</div>
);
}
}
export default PostStatusForm;

View File

@@ -0,0 +1,135 @@
import * as React from 'react';
import { DragDropContext, Droppable } from 'react-beautiful-dnd';
import IPostStatus from '../../../interfaces/IPostStatus';
import { PostStatusesState } from "../../../reducers/postStatusesReducer";
import { CenteredMutedText } from '../../shared/CustomTexts';
import SiteSettingsInfoBox from '../../shared/SiteSettingsInfoBox';
import PostStatusForm from './PostStatusForm';
import PostStatusEditable from './PostStatusEditable';
import Spinner from '../../shared/Spinner';
interface Props {
authenticityToken: string;
postStatuses: PostStatusesState;
settingsAreUpdating: boolean;
settingsError: string;
requestPostStatuses(): void;
submitPostStatus(
name: string,
color: string,
onSuccess: Function,
authenticityToken: string,
): void;
updatePostStatus(
id: number,
name: string,
color: string,
onSuccess: Function,
authenticityToken: string,
): void;
updatePostStatusOrder(
id: number,
postStatuses: Array<IPostStatus>,
sourceIndex: number,
destinationIndex: number,
authenticityToken: string,
): void;
deletePostStatus(id: number, authenticityToken: string): void;
}
class PostStatusesSiteSettingsP extends React.Component<Props> {
constructor(props: Props) {
super(props);
this.handleSubmit = this.handleSubmit.bind(this);
this.handleUpdate = this.handleUpdate.bind(this);
this.handleDragEnd = this.handleDragEnd.bind(this);
this.handleDelete = this.handleDelete.bind(this);
}
componentDidMount() {
this.props.requestPostStatuses();
}
handleSubmit(name: string, color: string, onSuccess: Function) {
this.props.submitPostStatus(name, color, onSuccess, this.props.authenticityToken);
}
handleUpdate(id: number, name: string, color: string, onSuccess: Function) {
this.props.updatePostStatus(id, name, color, onSuccess, this.props.authenticityToken);
}
handleDragEnd(result) {
if (result.destination == null || result.source.index === result.destination.index)
return;
this.props.updatePostStatusOrder(
parseInt(result.draggableId),
this.props.postStatuses.items,
result.source.index,
result.destination.index,
this.props.authenticityToken,
);
}
handleDelete(id: number) {
this.props.deletePostStatus(id, this.props.authenticityToken);
}
render() {
const { postStatuses, settingsAreUpdating, settingsError } = this.props;
return (
<React.Fragment>
<div className="content">
<h2>Post statuses</h2>
{
postStatuses.items.length > 0 ?
<DragDropContext onDragEnd={this.handleDragEnd}>
<Droppable droppableId="postStatuses">
{provided => (
<ul ref={provided.innerRef} {...provided.droppableProps} className="postStatusList">
{postStatuses.items.map((postStatus, i) => (
<PostStatusEditable
id={postStatus.id}
name={postStatus.name}
color={postStatus.color}
index={i}
settingsAreUpdating={settingsAreUpdating}
handleUpdate={this.handleUpdate}
handleDelete={this.handleDelete}
key={postStatus.id}
/>
))}
{provided.placeholder}
</ul>
)}
</Droppable>
</DragDropContext>
:
postStatuses.areLoading ?
<Spinner />
:
<CenteredMutedText>There are no post statuses. Create one below!</CenteredMutedText>
}
</div>
<div className="content">
<h2>New</h2>
<PostStatusForm mode='create' handleSubmit={this.handleSubmit} />
</div>
<SiteSettingsInfoBox areUpdating={settingsAreUpdating || postStatuses.areLoading} error={settingsError} />
</React.Fragment>
);
}
}
export default PostStatusesSiteSettingsP;

View File

@@ -0,0 +1,33 @@
import * as React from 'react';
import { Provider } from 'react-redux';
import { Store } from 'redux';
import PostStatusesSiteSettings from '../../../containers/PostStatusesSiteSettings';
import createStoreHelper from '../../../helpers/createStore';
import { State } from '../../../reducers/rootReducer';
interface Props {
authenticityToken: string;
}
class PostStatusesSiteSettingsRoot extends React.Component<Props> {
store: Store<State, any>;
constructor(props: Props) {
super(props);
this.store = createStoreHelper();
}
render() {
return (
<Provider store={this.store}>
<PostStatusesSiteSettings
authenticityToken={this.props.authenticityToken}
/>
</Provider>
);
}
}
export default PostStatusesSiteSettingsRoot;

View File

@@ -5,12 +5,14 @@ interface Props {
onClick(e: React.FormEvent): void;
className?: string;
outline?: boolean;
disabled?: boolean;
}
const Button = ({ children, onClick, className = '', outline = false}: Props) => (
const Button = ({ children, onClick, className = '', outline = false, disabled = false}: Props) => (
<button
onClick={onClick}
className={`${className} btn btn-${outline ? 'outline-' : ''}dark`}
disabled={disabled}
>
{children}
</button>

View File

@@ -0,0 +1,12 @@
import * as React from 'react';
const DragZone = ({dndProvided, isDragDisabled}) => (
<span
className={`drag-zone${isDragDisabled ? ' drag-zone-disabled' : ''}`}
{...dndProvided.dragHandleProps}
>
<span className="drag-icon"></span>
</span>
);
export default DragZone;

View File

@@ -0,0 +1,24 @@
import * as React from 'react';
import Spinner from './Spinner';
interface Props {
areUpdating: boolean;
error: string;
}
const SiteSettingsInfoBox = ({ areUpdating, error }: Props) => (
<div className="content siteSettingsInfo">
{
areUpdating ?
<Spinner />
:
error ?
<span className="error">An error occurred: {error}</span>
:
<span>Everything up to date</span>
}
</div>
);
export default SiteSettingsInfoBox;

View File

@@ -0,0 +1,7 @@
enum HttpStatus {
OK = 200,
Created = 201,
Accepted = 202,
}
export default HttpStatus;

View File

@@ -0,0 +1,64 @@
import { connect } from "react-redux";
import { deletePostStatus } from "../actions/deletePostStatus";
import { requestPostStatuses } from "../actions/requestPostStatuses";
import { submitPostStatus } from "../actions/submitPostStatus";
import { updatePostStatus } from "../actions/updatePostStatus";
import { updatePostStatusOrder } from "../actions/updatePostStatusOrder";
import PostStatusesSiteSettingsP from "../components/SiteSettings/PostStatuses/PostStatusesSiteSettingsP";
import HttpStatus from "../constants/http_status";
import IPostStatus from "../interfaces/IPostStatus";
import { State } from "../reducers/rootReducer";
const mapStateToProps = (state: State) => ({
postStatuses: state.postStatuses,
settingsAreUpdating: state.siteSettings.postStatuses.areUpdating,
settingsError: state.siteSettings.postStatuses.error,
});
const mapDispatchToProps = (dispatch: any) => ({
requestPostStatuses() {
dispatch(requestPostStatuses());
},
submitPostStatus(
name: string,
color: string,
onSuccess: Function,
authenticityToken: string
) {
dispatch(submitPostStatus(name, color, authenticityToken)).then(res => {
if (res && res.status === HttpStatus.Created) onSuccess();
});
},
updatePostStatus(
id: number,
name: string,
color: string,
onSuccess: Function,
authenticityToken: string,
) {
dispatch(updatePostStatus(id, name, color, authenticityToken)).then(res => {
if (res && res.status === HttpStatus.OK) onSuccess();
});
},
updatePostStatusOrder(
id: number,
postStatuses: Array<IPostStatus>,
sourceIndex: number,
destinationIndex: number,
authenticityToken: string) {
dispatch(updatePostStatusOrder(id, postStatuses, sourceIndex, destinationIndex, authenticityToken));
},
deletePostStatus(id: number, authenticityToken: string) {
dispatch(deletePostStatus(id, authenticityToken));
},
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(PostStatusesSiteSettingsP);

View File

@@ -0,0 +1,7 @@
interface IPostStatusJSON {
id: number;
name: string;
color: string;
}
export default IPostStatusJSON;

View File

@@ -0,0 +1,81 @@
import {
PostStatusOrderUpdateActionTypes,
POSTSTATUS_ORDER_UPDATE_START,
POSTSTATUS_ORDER_UPDATE_SUCCESS,
POSTSTATUS_ORDER_UPDATE_FAILURE,
} from '../../actions/updatePostStatusOrder';
import {
PostStatusDeleteActionTypes,
POST_STATUS_DELETE_START,
POST_STATUS_DELETE_SUCCESS,
POST_STATUS_DELETE_FAILURE,
} from '../../actions/deletePostStatus';
import {
PostStatusSubmitActionTypes,
POSTSTATUS_SUBMIT_START,
POSTSTATUS_SUBMIT_SUCCESS,
POSTSTATUS_SUBMIT_FAILURE,
} from '../../actions/submitPostStatus';
import {
PostStatusUpdateActionTypes,
POSTSTATUS_UPDATE_START,
POSTSTATUS_UPDATE_SUCCESS,
POSTSTATUS_UPDATE_FAILURE,
} from '../../actions/updatePostStatus';
export interface SiteSettingsPostStatusesState {
areUpdating: boolean;
error: string;
}
const initialState: SiteSettingsPostStatusesState = {
areUpdating: false,
error: '',
};
const siteSettingsPostStatusesReducer = (
state = initialState,
action: PostStatusOrderUpdateActionTypes |
PostStatusDeleteActionTypes |
PostStatusSubmitActionTypes |
PostStatusUpdateActionTypes
): SiteSettingsPostStatusesState => {
switch (action.type) {
case POSTSTATUS_SUBMIT_START:
case POSTSTATUS_UPDATE_START:
case POSTSTATUS_ORDER_UPDATE_START:
case POST_STATUS_DELETE_START:
return {
...state,
areUpdating: true,
};
case POSTSTATUS_SUBMIT_SUCCESS:
case POSTSTATUS_UPDATE_SUCCESS:
case POSTSTATUS_ORDER_UPDATE_SUCCESS:
case POST_STATUS_DELETE_SUCCESS:
return {
...state,
areUpdating: false,
error: '',
};
case POSTSTATUS_SUBMIT_FAILURE:
case POSTSTATUS_UPDATE_FAILURE:
case POSTSTATUS_ORDER_UPDATE_FAILURE:
case POST_STATUS_DELETE_FAILURE:
return {
...state,
areUpdating: false,
error: action.error,
};
default:
return state;
}
};
export default siteSettingsPostStatusesReducer;

View File

@@ -5,6 +5,26 @@ import {
POST_STATUSES_REQUEST_FAILURE,
} from '../actions/requestPostStatuses';
import {
PostStatusOrderUpdateActionTypes,
POSTSTATUS_ORDER_UPDATE_START,
} from '../actions/updatePostStatusOrder';
import {
PostStatusDeleteActionTypes,
POST_STATUS_DELETE_SUCCESS,
} from '../actions/deletePostStatus';
import {
PostStatusSubmitActionTypes,
POSTSTATUS_SUBMIT_SUCCESS,
} from '../actions/submitPostStatus';
import {
PostStatusUpdateActionTypes,
POSTSTATUS_UPDATE_SUCCESS,
} from '../actions/updatePostStatus';
import IPostStatus from '../interfaces/IPostStatus';
export interface PostStatusesState {
@@ -21,7 +41,11 @@ const initialState: PostStatusesState = {
const postStatusesReducer = (
state = initialState,
action: PostStatusesRequestActionTypes,
action: PostStatusesRequestActionTypes |
PostStatusOrderUpdateActionTypes |
PostStatusDeleteActionTypes |
PostStatusSubmitActionTypes |
PostStatusUpdateActionTypes
) => {
switch (action.type) {
case POST_STATUSES_REQUEST_START:
@@ -49,6 +73,33 @@ const postStatusesReducer = (
error: action.error,
};
case POSTSTATUS_SUBMIT_SUCCESS:
return {
...state,
items: [...state.items, action.postStatus],
};
case POSTSTATUS_UPDATE_SUCCESS:
return {
...state,
items: state.items.map(postStatus => {
if (postStatus.id !== action.postStatus.id) return postStatus;
return {...postStatus, name: action.postStatus.name, color: action.postStatus.color};
}),
};
case POST_STATUS_DELETE_SUCCESS:
return {
...state,
items: state.items.filter(postStatus => postStatus.id !== action.id),
};
case POSTSTATUS_ORDER_UPDATE_START:
return {
...state,
items: action.newOrder,
};
default:
return state;
}

View File

@@ -18,7 +18,6 @@ import {
COMMENT_SUBMIT_FAILURE,
} from '../actions/submitComment';
import replyFormReducer, { ReplyFormState } from './replyFormReducer';
import ICommentJSON from '../interfaces/json/IComment';

View File

@@ -3,11 +3,13 @@ import { combineReducers } from 'redux';
import postsReducer from './postsReducer';
import postStatusesReducer from './postStatusesReducer';
import currentPostReducer from './currentPostReducer';
import siteSettingsReducer from './siteSettingsReducer';
const rootReducer = combineReducers({
posts: postsReducer,
postStatuses: postStatusesReducer,
currentPost: currentPostReducer,
siteSettings: siteSettingsReducer,
});
export type State = ReturnType<typeof rootReducer>

View File

@@ -0,0 +1,68 @@
import {
PostStatusOrderUpdateActionTypes,
POSTSTATUS_ORDER_UPDATE_START,
POSTSTATUS_ORDER_UPDATE_SUCCESS,
POSTSTATUS_ORDER_UPDATE_FAILURE,
} from '../actions/updatePostStatusOrder';
import {
PostStatusDeleteActionTypes,
POST_STATUS_DELETE_START,
POST_STATUS_DELETE_SUCCESS,
POST_STATUS_DELETE_FAILURE,
} from '../actions/deletePostStatus';
import {
PostStatusSubmitActionTypes,
POSTSTATUS_SUBMIT_START,
POSTSTATUS_SUBMIT_SUCCESS,
POSTSTATUS_SUBMIT_FAILURE,
} from '../actions/submitPostStatus';
import {
PostStatusUpdateActionTypes,
POSTSTATUS_UPDATE_START,
POSTSTATUS_UPDATE_SUCCESS,
POSTSTATUS_UPDATE_FAILURE,
} from '../actions/updatePostStatus';
import siteSettingsPostStatusesReducer, { SiteSettingsPostStatusesState } from './SiteSettings/postStatusesReducer';
interface SiteSettingsState {
postStatuses: SiteSettingsPostStatusesState;
}
const initialState: SiteSettingsState = {
postStatuses: siteSettingsPostStatusesReducer(undefined, {} as any),
};
const siteSettingsReducer = (
state = initialState,
action: PostStatusOrderUpdateActionTypes |
PostStatusDeleteActionTypes |
PostStatusSubmitActionTypes |
PostStatusUpdateActionTypes
): SiteSettingsState => {
switch (action.type) {
case POSTSTATUS_ORDER_UPDATE_START:
case POSTSTATUS_ORDER_UPDATE_SUCCESS:
case POSTSTATUS_ORDER_UPDATE_FAILURE:
case POST_STATUS_DELETE_START:
case POST_STATUS_DELETE_SUCCESS:
case POST_STATUS_DELETE_FAILURE:
case POSTSTATUS_SUBMIT_START:
case POSTSTATUS_SUBMIT_SUCCESS:
case POSTSTATUS_SUBMIT_FAILURE:
case POSTSTATUS_UPDATE_START:
case POSTSTATUS_UPDATE_SUCCESS:
case POSTSTATUS_UPDATE_FAILURE:
return {
postStatuses: siteSettingsPostStatusesReducer(state.postStatuses, action)
};
default:
return state;
}
};
export default siteSettingsReducer;

View File

@@ -0,0 +1,32 @@
.postStatusList {
@extend
.p-0,
.m-0;
list-style: none;
.postStatusEditable {
@extend
.d-flex,
.justify-content-between,
.p-3;
a {
cursor: pointer;
&:hover { text-decoration: underline; }
}
.postStatusFormCancelButton {
align-self: center;
}
}
}
.postStatusForm {
@extend
.d-flex,
.m-2;
column-gap: 16px;
}

View File

@@ -0,0 +1,7 @@
.siteSettingsInfo {
text-align: center;
.error {
color: red;
}
}

View File

@@ -27,6 +27,35 @@ a {
}
}
.multiColumnContainer {
@extend
.d-flex,
.justify-content-between,
.align-items-start;
flex-direction: row;
@include media-breakpoint-down(sm) {
flex-direction: column;
.postAndCommentsContainer { width: 100%; }
}
}
.multiRowContent {
@extend
.flex-grow-1,
.w-100;
}
.content {
@extend
.card,
.flex-grow-1,
.p-3,
.mb-3;
}
.sidebar {
position: sticky;
top: 79px;
@@ -48,6 +77,39 @@ a {
}
}
.verticalNavigation {
@extend
.nav,
.flex-column,
.nav-pills,
.text-center,
.align-self-stretch;
.nav-link.active {
background-color: $astuto-black;
}
}
.drag-zone {
@extend
.align-self-center,
.pl-4,
.pr-4,
.pt-1,
.pb-1;
cursor: grab;
&.drag-zone-disabled {
cursor: not-allowed;
&:active { cursor: not-allowed; }
}
&:active { cursor: grabbing; }
}
@include media-breakpoint-down(sm) {
.sidebar {
position: relative;

View File

@@ -0,0 +1,23 @@
/* Code taken from: https://gist.github.com/JakeSidSmith/83c324dbe7e4d91ee8c52525b1d504d9 */
/* Thanks JakeSidSmith */
span.drag-icon {
display: inline-block;
width: 16px;
height: 8px;
}
span.drag-icon,
span.drag-icon::before {
background-image: radial-gradient(black 40%, transparent 40%);
background-size: 4px 4px;
background-position: 0 100%;
background-repeat: repeat-x;
}
span.drag-icon::before {
content: '';
display: block;
width: 100%;
height: 33%;
}

View File

@@ -13,4 +13,11 @@
@import 'components/Comments';
@import 'components/LikeButton';
@import 'components/Post';
@import 'components/Roadmap';
@import 'components/Roadmap';
/* Site Settings Components */
@import 'components/SiteSettings';
@import 'components/SiteSettings/PostStatuses';
/* Icons */
@import 'icons/drag_icon';

View File

@@ -2,10 +2,11 @@ class PostStatus < ApplicationRecord
has_many :posts, dependent: :nullify
after_initialize :set_random_color, :set_order_to_last
after_destroy :ensure_coherent_order
validates :name, presence: true, uniqueness: true
validates :color, format: { with: /\A#(?:[0-9a-fA-F]{3}){1,2}\z/ }
validates :order, numericality: { only_integer: true, greater_than: 0 }
validates :order, presence: true, numericality: { only_integer: true, greater_than_or_equal_to: 0 }
class << self
def find_roadmap
@@ -25,7 +26,14 @@ class PostStatus < ApplicationRecord
return unless new_record?
return unless order.nil?
order_last = PostStatus.maximum(:order) || 0
order_last = PostStatus.maximum(:order) || -1
self.order = order_last + 1
end
def ensure_coherent_order
EnsureCoherentOrderingWorkflow.new(
entity_classname: PostStatus,
column_name: 'order'
).run
end
end

View File

@@ -32,4 +32,8 @@ class User < ApplicationRecord
def power_user?
role == 'admin' || role == 'moderator'
end
def admin?
role == 'admin'
end
end

View File

@@ -18,17 +18,18 @@
</ul>
<ul class="navbar-nav ml-auto">
<% if user_signed_in? %>
<% if current_user.power_user? %>
<li class="nav-item">
<%= link_to 'Admin Panel', admin_root_path, class: 'nav-link', 'data-turbolinks': 'false' %>
</li>
<% end %>
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<%= image_tag(current_user.gravatar_url, class: 'gravatar', alt: current_user.full_name, size: 24) %>
<span class="fullname"><%= current_user.full_name %></span>
</a>
<div class="dropdown-menu" aria-labelledby="navbarDropdown">
<% if current_user.power_user? %>
<%= link_to 'Site settings', site_settings_general_path, class: 'dropdown-item' %>
<%= link_to 'Admin Panel', admin_root_path, class: 'dropdown-item', 'data-turbolinks': 'false' %>
<div class="dropdown-divider"></div>
<% end %>
<%= link_to 'Profile settings', edit_user_registration_path, class: 'dropdown-item' %>
<div class="dropdown-divider"></div>
<%= link_to 'Sign out', destroy_user_session_path, method: :delete, class: 'dropdown-item' %>

View File

@@ -0,0 +1,11 @@
<div class="sidebar">
<div class="sidebarCard">
<span class="boxTitleText">Site Settings</span>
<div class="verticalNavigation" role="tablist" aria-orientation="vertical">
<%= render 'menu_link', label: 'General', path: site_settings_general_path %>
<%= render 'menu_link', label: 'Appearance', path: '#' %>
<%= render 'menu_link', label: 'Post statuses', path: site_settings_post_statuses_path %>
<%= render 'menu_link', label: 'Widgets', path: '#' %>
</div>
</div>
</div>

View File

@@ -0,0 +1,8 @@
<a
href="<%= path %>"
class="nav-link<%= current_page?(path) ? ' active' : '' %>"
role="tabs"
aria-controls="v-pills-home"
>
<%= label %>
</a>

View File

@@ -0,0 +1,8 @@
<div class="multiColumnContainer">
<%= render 'menu' %>
<div class="content">
<h2>General</h2>
Nome sito: <input type="text" />
</div>
</div>

View File

@@ -0,0 +1,13 @@
<div class="multiColumnContainer">
<%= render 'menu' %>
<div class="multiRowContent">
<%=
react_component(
'SiteSettings/PostStatuses',
{
authenticityToken: form_authenticity_token
}
)
%>
</div>
</div>

View File

@@ -0,0 +1,22 @@
class EnsureCoherentOrderingWorkflow
attr_accessor :entity_classname, :column_name
def initialize(entity_classname: "", column_name: "")
@entity_classname = entity_classname
@column_name = column_name
end
def run
column_name_sanitized = ActiveRecord::Base.connection.quote_column_name(column_name)
entity_records = entity_classname.order("#{column_name_sanitized} ASC")
entity_records.each_with_index do |entity_record, order|
entity_record[column_name] = order
end
entity_classname.transaction do
entity_records.each(&:save!)
end
end
end

View File

@@ -0,0 +1,68 @@
class ReorderWorkflow
attr_accessor :entity_classname, :column_name
attr_accessor :entity_id, :src_index, :dst_index
# This workflow is used by entities which has a "order" column of sort
# Given:
# entity_classname: the entity class name, e.g. PostStatus
# column_name: the name of the db column that contains the order information, e.g. 'order'
# entity_id: the id of the entity being reordered
# src_index: the current order of the entity
# dst_index = the new order of the entity
#
# The workflow reorders the record with id <entity_id> of entity <entity_classname>
# from <src_index> to <dst_index> using <column_name> as the db column that keeps the ordering
#
# Returns:
# A collection of the reordered records, if successful
# nil, if unsuccessful
def initialize(entity_classname: "", column_name: "", entity_id: "", src_index: "", dst_index: "")
@entity_classname = entity_classname
@column_name = column_name
@entity_id = entity_id
@src_index = src_index
@dst_index = dst_index
end
def run
convert_indexes_to_i
if src_index == dst_index
return {}
end
lowest_index = src_index < dst_index ? src_index : dst_index
highest_index = src_index < dst_index ? dst_index : src_index
column_name_sanitized = ActiveRecord::Base.connection.quote_column_name(column_name)
entity_records = entity_classname.where("#{column_name_sanitized} BETWEEN #{lowest_index} AND #{highest_index}")
# reorder records
entity_records.each do |entity_record|
if entity_record.id == entity_id
entity_record[column_name] = dst_index
elsif src_index < dst_index
entity_record[column_name] -= 1
elsif src_index > dst_index
entity_record[column_name] += 1
end
end
# save all changes in a single transaction
entity_classname.transaction do
begin
entity_records.each(&:save!)
return entity_records
rescue
return nil
end
end
end
def convert_indexes_to_i
self.src_index = src_index.to_i
self.dst_index = dst_index.to_i
end
end

View File

@@ -20,5 +20,12 @@ Rails.application.routes.draw do
resources :comments, only: [:index, :create, :update]
end
resources :boards, only: [:show]
resources :post_statuses, only: [:index]
resources :post_statuses, only: [:index, :create, :update, :destroy] do
patch 'update_order', on: :collection
end
namespace :site_settings do
get 'general'
get 'post_statuses'
end
end

View File

@@ -15,6 +15,7 @@
"popper.js": "^1.15.0",
"prop-types": "^15.7.2",
"react": "^16.9.0",
"react-beautiful-dnd": "^13.1.0",
"react-dom": "^16.9.0",
"react-gravatar": "^2.6.3",
"react-infinite-scroller": "^1.2.4",

View File

@@ -0,0 +1 @@
rspec --exclude-pattern "spec/system/*_spec.rb"

View File

@@ -37,7 +37,7 @@ RSpec.describe PostStatus, type: :model do
expect(valid_color2).to be_valid
end
it 'must have a order of type integer and positive' do
it 'must have a order of type integer and non-negative' do
nil_order = FactoryBot.build(:post_status, order: nil)
empty_order = FactoryBot.build(:post_status, order: '')
decimal_order = FactoryBot.build(:post_status, order: 1.1)
@@ -48,7 +48,7 @@ RSpec.describe PostStatus, type: :model do
expect(empty_order).to be_invalid
expect(decimal_order).to be_invalid
expect(negative_order).to be_invalid
expect(zero_order).to be_invalid
expect(zero_order).to be_valid
end
it 'has a method that returns only post statuses that should show up in roadmap' do

View File

@@ -0,0 +1,33 @@
require 'rails_helper'
RSpec.describe EnsureCoherentOrderingWorkflow do
let(:workflow_creator) {
EnsureCoherentOrderingWorkflow.new(
entity_classname: PostStatus,
column_name: 'order'
)
}
it 'fills any gap in a column representing ordering' do
post_status1 = FactoryBot.create(:post_status, order: 4)
post_status2 = FactoryBot.create(:post_status, order: 10)
post_status3 = FactoryBot.create(:post_status, order: 133)
workflow_creator.run
expect(post_status1.reload.order).to eq(0)
expect(post_status2.reload.order).to eq(1)
expect(post_status3.reload.order).to eq(2)
post_status2.destroy
workflow_creator.run
expect(post_status1.reload.order).to eq(0)
expect(post_status3.reload.order).to eq(1)
post_status1.destroy
workflow_creator.run
expect(post_status3.reload.order).to eq(0)
end
end

View File

@@ -0,0 +1,35 @@
require 'rails_helper'
RSpec.describe ReorderWorkflow do
let(:workflow_creator) {
ReorderWorkflow.new(
entity_classname: entity_classname,
column_name: column_name,
entity_id: entity_id,
src_index: src_index,
dst_index: dst_index
)
}
let(:entity_classname) { PostStatus }
let(:column_name) { 'order' }
let(:entity_id) { post_status1.id }
let(:src_index) { post_status1.order }
let(:dst_index) { post_status3.order }
let!(:post_status0) { FactoryBot.create(:post_status, order: 0) }
let!(:post_status1) { FactoryBot.create(:post_status, order: 1) }
let!(:post_status2) { FactoryBot.create(:post_status, order: 2) }
let!(:post_status3) { FactoryBot.create(:post_status, order: 3) }
let!(:post_status4) { FactoryBot.create(:post_status, order: 4) }
it 'reorders entities after moving one of them' do
workflow_creator.run
expect(post_status0.reload.order).to eq(0)
expect(post_status1.reload.order).to eq(3)
expect(post_status3.reload.order).to eq(2)
expect(post_status2.reload.order).to eq(1)
expect(post_status4.reload.order).to eq(4)
end
end

View File

@@ -705,6 +705,13 @@
"@babel/plugin-transform-react-jsx-self" "^7.7.4"
"@babel/plugin-transform-react-jsx-source" "^7.7.4"
"@babel/runtime@^7.15.4", "@babel/runtime@^7.9.2":
version "7.17.9"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.17.9.tgz#d19fbf802d01a8cb6cf053a64e472d42c434ba72"
integrity sha512-lSiBBvodq29uShpWGNbgFdKYNiFDo5/HIYsaCEY9ff4sb10x9jizo2+pRrSyF4jKZCXqgzuqBOQKbUm90gQwJg==
dependencies:
regenerator-runtime "^0.13.4"
"@babel/runtime@^7.5.5", "@babel/runtime@^7.6.3", "@babel/runtime@^7.7.2":
version "7.7.7"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.7.7.tgz#194769ca8d6d7790ec23605af9ee3e42a0aa79cf"
@@ -865,6 +872,16 @@
dependencies:
"@types/react" "*"
"@types/react-redux@^7.1.20":
version "7.1.23"
resolved "https://registry.yarnpkg.com/@types/react-redux/-/react-redux-7.1.23.tgz#3c2bb1bcc698ae69d70735f33c5a8e95f41ac528"
integrity sha512-D02o3FPfqQlfu2WeEYwh3x2otYd2Dk1o8wAfsA0B1C2AJEFxE663Ozu7JzuWbznGgW248NaOF6wsqCGNq9d3qw==
dependencies:
"@types/hoist-non-react-statics" "^3.3.0"
"@types/react" "*"
hoist-non-react-statics "^3.3.0"
redux "^4.0.0"
"@types/react-redux@^7.1.3":
version "7.1.5"
resolved "https://registry.yarnpkg.com/@types/react-redux/-/react-redux-7.1.5.tgz#c7a528d538969250347aa53c52241051cf886bd3"
@@ -2125,6 +2142,13 @@ css-blank-pseudo@^0.1.4:
dependencies:
postcss "^7.0.5"
css-box-model@^1.2.0:
version "1.2.1"
resolved "https://registry.yarnpkg.com/css-box-model/-/css-box-model-1.2.1.tgz#59951d3b81fd6b2074a62d49444415b0d2b4d7c1"
integrity sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw==
dependencies:
tiny-invariant "^1.0.6"
css-color-names@0.0.4, css-color-names@^0.0.4:
version "0.0.4"
resolved "https://registry.yarnpkg.com/css-color-names/-/css-color-names-0.0.4.tgz#808adc2e79cf84738069b646cb20ec27beb629e0"
@@ -3326,6 +3350,13 @@ hoist-non-react-statics@^3.3.0:
dependencies:
react-is "^16.7.0"
hoist-non-react-statics@^3.3.2:
version "3.3.2"
resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45"
integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==
dependencies:
react-is "^16.7.0"
homedir-polyfill@^1.0.1:
version "1.0.3"
resolved "https://registry.yarnpkg.com/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz#743298cef4e5af3e194161fbadcc2151d3a058e8"
@@ -4249,6 +4280,11 @@ mem@^4.0.0:
mimic-fn "^2.0.0"
p-is-promise "^2.0.0"
memoize-one@^5.1.1:
version "5.2.1"
resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-5.2.1.tgz#8337aa3c4335581839ec01c3d594090cebe8f00e"
integrity sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==
memory-fs@^0.4.0, memory-fs@^0.4.1:
version "0.4.1"
resolved "https://registry.yarnpkg.com/memory-fs/-/memory-fs-0.4.1.tgz#3a9a20b8462523e447cfbc7e8bb80ed667bfc552"
@@ -5941,6 +5977,11 @@ querystringify@^2.1.1:
resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.2.0.tgz#3345941b4153cb9d082d8eee4cda2016a9aef7f6"
integrity sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==
raf-schd@^4.0.2:
version "4.0.3"
resolved "https://registry.yarnpkg.com/raf-schd/-/raf-schd-4.0.3.tgz#5d6c34ef46f8b2a0e880a8fcdb743efc5bfdbc1a"
integrity sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ==
randombytes@^2.0.0, randombytes@^2.0.1, randombytes@^2.0.5:
version "2.1.0"
resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a"
@@ -5971,6 +6012,19 @@ raw-body@2.4.0:
iconv-lite "0.4.24"
unpipe "1.0.0"
react-beautiful-dnd@^13.1.0:
version "13.1.0"
resolved "https://registry.yarnpkg.com/react-beautiful-dnd/-/react-beautiful-dnd-13.1.0.tgz#ec97c81093593526454b0de69852ae433783844d"
integrity sha512-aGvblPZTJowOWUNiwd6tNfEpgkX5OxmpqxHKNW/4VmvZTNTbeiq7bA3bn5T+QSF2uibXB0D1DmJsb1aC/+3cUA==
dependencies:
"@babel/runtime" "^7.9.2"
css-box-model "^1.2.0"
memoize-one "^5.1.1"
raf-schd "^4.0.2"
react-redux "^7.2.0"
redux "^4.0.4"
use-memo-one "^1.1.1"
react-dom@^16.9.0:
version "16.12.0"
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.12.0.tgz#0da4b714b8d13c2038c9396b54a92baea633fe11"
@@ -6002,6 +6056,11 @@ react-is@^16.7.0, react-is@^16.8.1, react-is@^16.9.0:
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.12.0.tgz#2cc0fe0fba742d97fd527c42a13bec4eeb06241c"
integrity sha512-rPCkf/mWBtKc97aLL9/txD8DZdemK0vkA3JMLShjlJB3Pj3s+lpf1KaBzMfQrAmhMQB0n1cU/SUGgKKBCe837Q==
react-is@^17.0.2:
version "17.0.2"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0"
integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==
react-redux@^7.1.1:
version "7.1.3"
resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-7.1.3.tgz#717a3d7bbe3a1b2d535c94885ce04cdc5a33fc79"
@@ -6014,6 +6073,18 @@ react-redux@^7.1.1:
prop-types "^15.7.2"
react-is "^16.9.0"
react-redux@^7.2.0:
version "7.2.8"
resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-7.2.8.tgz#a894068315e65de5b1b68899f9c6ee0923dd28de"
integrity sha512-6+uDjhs3PSIclqoCk0kd6iX74gzrGc3W5zcAjbrFgEdIjRSQObdIwfx80unTkVUYvbQ95Y8Av3OvFHq1w5EOUw==
dependencies:
"@babel/runtime" "^7.15.4"
"@types/react-redux" "^7.1.20"
hoist-non-react-statics "^3.3.2"
loose-envify "^1.4.0"
prop-types "^15.7.2"
react-is "^17.0.2"
react@^16.9.0:
version "16.12.0"
resolved "https://registry.yarnpkg.com/react/-/react-16.12.0.tgz#0c0a9c6a142429e3614834d5a778e18aa78a0b83"
@@ -6136,6 +6207,11 @@ regenerator-runtime@^0.13.2, regenerator-runtime@^0.13.3:
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.3.tgz#7cf6a77d8f5c6f60eb73c5fc1955b2ceb01e6bf5"
integrity sha512-naKIZz2GQ8JWh///G7L3X6LaQUAMp2lvb1rvwwsURe/VXwD6VMfr+/1NuNw3ag8v2kY1aQ/go5SNn79O9JU7yw==
regenerator-runtime@^0.13.4:
version "0.13.9"
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz#8925742a98ffd90814988d7566ad30ca3b263b52"
integrity sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==
regenerator-transform@^0.14.0:
version "0.14.1"
resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.14.1.tgz#3b2fce4e1ab7732c08f665dfdb314749c7ddd2fb"
@@ -7060,6 +7136,11 @@ timsort@^0.3.0:
resolved "https://registry.yarnpkg.com/timsort/-/timsort-0.3.0.tgz#405411a8e7e6339fe64db9a234de11dc31e02bd4"
integrity sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q=
tiny-invariant@^1.0.6:
version "1.2.0"
resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.2.0.tgz#a1141f86b672a9148c72e978a19a73b9b94a15a9"
integrity sha512-1Uhn/aqw5C6RI4KejVeTg6mIS7IqxnLJ8Mv2tV5rTc0qWobay7pDUz6Wi392Cnc8ak1H0F2cjoRzb2/AW4+Fvg==
to-arraybuffer@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz#7d229b1fcc637e466ca081180836a7aabff83f43"
@@ -7296,6 +7377,11 @@ url@^0.11.0:
punycode "1.3.2"
querystring "0.2.0"
use-memo-one@^1.1.1:
version "1.1.2"
resolved "https://registry.yarnpkg.com/use-memo-one/-/use-memo-one-1.1.2.tgz#0c8203a329f76e040047a35a1197defe342fab20"
integrity sha512-u2qFKtxLsia/r8qG0ZKkbytbztzRb317XCkT7yP8wxL0tZ/CzK2G+WWie5vWvpyeP7+YoPIwbJoIHJ4Ba4k0oQ==
use@^3.1.0:
version "3.1.1"
resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f"