mirror of
https://github.com/astuto/astuto.git
synced 2025-12-14 18:57:51 +01:00
Add post status administration (#105)
This commit is contained in:
committed by
GitHub
parent
c5148147e3
commit
5256ea911a
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
11
app/controllers/site_settings_controller.rb
Normal file
11
app/controllers/site_settings_controller.rb
Normal file
@@ -0,0 +1,11 @@
|
||||
class SiteSettingsController < ApplicationController
|
||||
include ApplicationHelper
|
||||
|
||||
before_action :authenticate_admin
|
||||
|
||||
def general
|
||||
end
|
||||
|
||||
def post_statuses
|
||||
end
|
||||
end
|
||||
@@ -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
|
||||
|
||||
62
app/javascript/actions/deletePostStatus.ts
Normal file
62
app/javascript/actions/deletePostStatus.ts
Normal 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));
|
||||
}
|
||||
}
|
||||
);
|
||||
@@ -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));
|
||||
|
||||
@@ -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');
|
||||
|
||||
78
app/javascript/actions/submitPostStatus.ts
Normal file
78
app/javascript/actions/submitPostStatus.ts
Normal 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);
|
||||
}
|
||||
};
|
||||
79
app/javascript/actions/updatePostStatus.ts
Normal file
79
app/javascript/actions/updatePostStatus.ts
Normal 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);
|
||||
}
|
||||
};
|
||||
96
app/javascript/actions/updatePostStatusOrder.ts
Normal file
96
app/javascript/actions/updatePostStatusOrder.ts
Normal 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;
|
||||
}
|
||||
@@ -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...',
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
|
||||
12
app/javascript/components/shared/DragZone.tsx
Normal file
12
app/javascript/components/shared/DragZone.tsx
Normal 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;
|
||||
24
app/javascript/components/shared/SiteSettingsInfoBox.tsx
Normal file
24
app/javascript/components/shared/SiteSettingsInfoBox.tsx
Normal 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;
|
||||
7
app/javascript/constants/http_status.ts
Normal file
7
app/javascript/constants/http_status.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
enum HttpStatus {
|
||||
OK = 200,
|
||||
Created = 201,
|
||||
Accepted = 202,
|
||||
}
|
||||
|
||||
export default HttpStatus;
|
||||
64
app/javascript/containers/PostStatusesSiteSettings.tsx
Normal file
64
app/javascript/containers/PostStatusesSiteSettings.tsx
Normal 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);
|
||||
7
app/javascript/interfaces/json/IPostStatus.ts
Normal file
7
app/javascript/interfaces/json/IPostStatus.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
interface IPostStatusJSON {
|
||||
id: number;
|
||||
name: string;
|
||||
color: string;
|
||||
}
|
||||
|
||||
export default IPostStatusJSON;
|
||||
81
app/javascript/reducers/SiteSettings/postStatusesReducer.ts
Normal file
81
app/javascript/reducers/SiteSettings/postStatusesReducer.ts
Normal 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;
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -18,7 +18,6 @@ import {
|
||||
COMMENT_SUBMIT_FAILURE,
|
||||
} from '../actions/submitComment';
|
||||
|
||||
|
||||
import replyFormReducer, { ReplyFormState } from './replyFormReducer';
|
||||
|
||||
import ICommentJSON from '../interfaces/json/IComment';
|
||||
|
||||
@@ -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>
|
||||
|
||||
68
app/javascript/reducers/siteSettingsReducer.ts
Normal file
68
app/javascript/reducers/siteSettingsReducer.ts
Normal 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;
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
.siteSettingsInfo {
|
||||
text-align: center;
|
||||
|
||||
.error {
|
||||
color: red;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
23
app/javascript/stylesheets/icons/drag_icon.scss
Normal file
23
app/javascript/stylesheets/icons/drag_icon.scss
Normal 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%;
|
||||
}
|
||||
@@ -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';
|
||||
@@ -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
|
||||
|
||||
@@ -32,4 +32,8 @@ class User < ApplicationRecord
|
||||
def power_user?
|
||||
role == 'admin' || role == 'moderator'
|
||||
end
|
||||
|
||||
def admin?
|
||||
role == 'admin'
|
||||
end
|
||||
end
|
||||
|
||||
@@ -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' %>
|
||||
|
||||
11
app/views/site_settings/_menu.html.erb
Normal file
11
app/views/site_settings/_menu.html.erb
Normal 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>
|
||||
8
app/views/site_settings/_menu_link.html.erb
Normal file
8
app/views/site_settings/_menu_link.html.erb
Normal file
@@ -0,0 +1,8 @@
|
||||
<a
|
||||
href="<%= path %>"
|
||||
class="nav-link<%= current_page?(path) ? ' active' : '' %>"
|
||||
role="tabs"
|
||||
aria-controls="v-pills-home"
|
||||
>
|
||||
<%= label %>
|
||||
</a>
|
||||
8
app/views/site_settings/general.html.erb
Normal file
8
app/views/site_settings/general.html.erb
Normal file
@@ -0,0 +1,8 @@
|
||||
<div class="multiColumnContainer">
|
||||
<%= render 'menu' %>
|
||||
|
||||
<div class="content">
|
||||
<h2>General</h2>
|
||||
Nome sito: <input type="text" />
|
||||
</div>
|
||||
</div>
|
||||
13
app/views/site_settings/post_statuses.html.erb
Normal file
13
app/views/site_settings/post_statuses.html.erb
Normal file
@@ -0,0 +1,13 @@
|
||||
<div class="multiColumnContainer">
|
||||
<%= render 'menu' %>
|
||||
<div class="multiRowContent">
|
||||
<%=
|
||||
react_component(
|
||||
'SiteSettings/PostStatuses',
|
||||
{
|
||||
authenticityToken: form_authenticity_token
|
||||
}
|
||||
)
|
||||
%>
|
||||
</div>
|
||||
</div>
|
||||
22
app/workflows/EnsureCoherentOrderingWorkflow.rb
Normal file
22
app/workflows/EnsureCoherentOrderingWorkflow.rb
Normal 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
|
||||
68
app/workflows/ReorderWorkflow.rb
Normal file
68
app/workflows/ReorderWorkflow.rb
Normal 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
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
1
script/rspec-no-system-specs.sh
Normal file
1
script/rspec-no-system-specs.sh
Normal file
@@ -0,0 +1 @@
|
||||
rspec --exclude-pattern "spec/system/*_spec.rb"
|
||||
@@ -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
|
||||
|
||||
33
spec/workflows/ensure_coherent_ordering_workflow_spec.rb
Normal file
33
spec/workflows/ensure_coherent_ordering_workflow_spec.rb
Normal 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
|
||||
35
spec/workflows/reorder_workflow_spec.rb
Normal file
35
spec/workflows/reorder_workflow_spec.rb
Normal 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
|
||||
86
yarn.lock
86
yarn.lock
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user