diff --git a/app/controllers/boards_controller.rb b/app/controllers/boards_controller.rb index 25b93589..3f441d66 100644 --- a/app/controllers/boards_controller.rb +++ b/app/controllers/boards_controller.rb @@ -1,5 +1,81 @@ class BoardsController < ApplicationController + include ApplicationHelper + + before_action :authenticate_admin, only: [:create, :update, :update_order, :destroy] + + def index + boards = Board.order(order: :asc) + + render json: boards + end + def show @board = Board.find(params[:id]) end + + def create + board = Board.new(board_params) + + if board.save + render json: board, status: :created + else + render json: { + error: I18n.t('errors.board.create', message: board.errors.full_messages) + }, status: :unprocessable_entity + end + end + + def update + board = Board.find(params[:id]) + board.assign_attributes(board_params) + + if board.save + render json: board, status: :ok + else + print board.errors.full_messages + + render json: { + error: I18n.t('errors.board.update', message: board.errors.full_messages) + }, status: :unprocessable_entity + end + end + + def destroy + board = Board.find(params[:id]) + + if board.destroy + render json: { + id: params[:id] + }, status: :accepted + else + render json: { + error: I18n.t('errors.board.destroy', message: board.errors.full_messages) + }, status: :unprocessable_entity + end + end + + def update_order + workflow_output = ReorderWorkflow.new( + entity_classname: Board, + column_name: 'order', + entity_id: params[:board][:id], + src_index: params[:board][:src_index], + dst_index: params[:board][:dst_index] + ).run + + if workflow_output + render json: workflow_output + else + render json: { + error: I18n.t("errors.board.update_order") + }, status: :unprocessable_entity + end + end + + private + def board_params + params + .require(:board) + .permit(:name, :description) + end end diff --git a/app/controllers/site_settings_controller.rb b/app/controllers/site_settings_controller.rb index 96208a0c..a6987fcf 100644 --- a/app/controllers/site_settings_controller.rb +++ b/app/controllers/site_settings_controller.rb @@ -6,6 +6,9 @@ class SiteSettingsController < ApplicationController def general end + def boards + end + def post_statuses end end \ No newline at end of file diff --git a/app/javascript/actions/Board/deleteBoard.ts b/app/javascript/actions/Board/deleteBoard.ts new file mode 100644 index 00000000..47d793e1 --- /dev/null +++ b/app/javascript/actions/Board/deleteBoard.ts @@ -0,0 +1,69 @@ +import { Action } from "redux"; +import { ThunkAction } from "redux-thunk"; +import HttpStatus from "../../constants/http_status"; + +import buildRequestHeaders from "../../helpers/buildRequestHeaders"; +import { State } from "../../reducers/rootReducer"; + +export const BOARD_DELETE_START = 'BOARD_DELETE_START'; +interface BoardDeleteStartAction { + type: typeof BOARD_DELETE_START; +} + +export const BOARD_DELETE_SUCCESS = 'BOARD_DELETE_SUCCESS'; +interface BoardDeleteSuccessAction { + type: typeof BOARD_DELETE_SUCCESS; + id: number; +} + +export const BOARD_DELETE_FAILURE = 'BOARD_DELETE_FAILURE'; +interface BoardDeleteFailureAction { + type: typeof BOARD_DELETE_FAILURE; + error: string; +} + +export type BoardDeleteActionTypes = + BoardDeleteStartAction | + BoardDeleteSuccessAction | + BoardDeleteFailureAction; + +const boardDeleteStart = (): BoardDeleteStartAction => ({ + type: BOARD_DELETE_START, +}); + +const boardDeleteSuccess = ( + id: number, +): BoardDeleteSuccessAction => ({ + type: BOARD_DELETE_SUCCESS, + id, +}); + +const boardDeleteFailure = (error: string): BoardDeleteFailureAction => ({ + type: BOARD_DELETE_FAILURE, + error, +}); + +export const deleteBoard = ( + id: number, + authenticityToken: string, +): ThunkAction> => ( + async (dispatch) => { + dispatch(boardDeleteStart()); + + try { + const res = await fetch(`/boards/${id}`, { + method: 'DELETE', + headers: buildRequestHeaders(authenticityToken), + }); + const json = await res.json(); + + if (res.status === HttpStatus.Accepted) { + dispatch(boardDeleteSuccess(id)); + } else { + dispatch(boardDeleteFailure(json.error)); + } + } catch (e) { + dispatch(boardDeleteFailure(e)); + } + } +); \ No newline at end of file diff --git a/app/javascript/actions/Board/requestBoards.ts b/app/javascript/actions/Board/requestBoards.ts new file mode 100644 index 00000000..1a28472c --- /dev/null +++ b/app/javascript/actions/Board/requestBoards.ts @@ -0,0 +1,59 @@ +import { Action } from 'redux'; +import { ThunkAction } from 'redux-thunk'; + +import IBoard from '../../interfaces/IBoard'; + +import { State } from '../../reducers/rootReducer'; + +export const BOARDS_REQUEST_START = 'BOARDS_REQUEST_START'; +interface BoardsRequestStartAction { + type: typeof BOARDS_REQUEST_START; +} + +export const BOARDS_REQUEST_SUCCESS = 'BOARDS_REQUEST_SUCCESS'; +interface BoardsRequestSuccessAction { + type: typeof BOARDS_REQUEST_SUCCESS; + boards: Array; +} + +export const BOARDS_REQUEST_FAILURE = 'BOARDS_REQUEST_FAILURE'; +interface BoardsRequestFailureAction { + type: typeof BOARDS_REQUEST_FAILURE; + error: string; +} + +export type BoardsRequestActionTypes = + BoardsRequestStartAction | + BoardsRequestSuccessAction | + BoardsRequestFailureAction; + + +const boardsRequestStart = (): BoardsRequestActionTypes => ({ + type: BOARDS_REQUEST_START, +}); + +const boardsRequestSuccess = ( + boards: Array +): BoardsRequestActionTypes => ({ + type: BOARDS_REQUEST_SUCCESS, + boards, +}); + +const boardsRequestFailure = (error: string): BoardsRequestActionTypes => ({ + type: BOARDS_REQUEST_FAILURE, + error, +}); + +export const requestBoards = (): ThunkAction> => ( + async (dispatch) => { + dispatch(boardsRequestStart()); + + try { + const response = await fetch('/boards'); + const json = await response.json(); + dispatch(boardsRequestSuccess(json)); + } catch (e) { + dispatch(boardsRequestFailure(e)); + } + } +) \ No newline at end of file diff --git a/app/javascript/actions/Board/submitBoard.ts b/app/javascript/actions/Board/submitBoard.ts new file mode 100644 index 00000000..7b183d6e --- /dev/null +++ b/app/javascript/actions/Board/submitBoard.ts @@ -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 IBoardJSON from "../../interfaces/json/IBoard"; +import { State } from "../../reducers/rootReducer"; + +export const BOARD_SUBMIT_START = 'BOARD_SUBMIT_START'; +interface BoardSubmitStartAction { + type: typeof BOARD_SUBMIT_START; +} + +export const BOARD_SUBMIT_SUCCESS = 'BOARD_SUBMIT_SUCCESS'; +interface BoardSubmitSuccessAction { + type: typeof BOARD_SUBMIT_SUCCESS; + board: IBoardJSON; +} + +export const BOARD_SUBMIT_FAILURE = 'BOARD_SUBMIT_FAILURE'; +interface BoardSubmitFailureAction { + type: typeof BOARD_SUBMIT_FAILURE; + error: string; +} + +export type BoardSubmitActionTypes = + BoardSubmitStartAction | + BoardSubmitSuccessAction | + BoardSubmitFailureAction; + +const boardSubmitStart = (): BoardSubmitStartAction => ({ + type: BOARD_SUBMIT_START, +}); + +const boardSubmitSuccess = ( + boardJSON: IBoardJSON, +): BoardSubmitSuccessAction => ({ + type: BOARD_SUBMIT_SUCCESS, + board: boardJSON, +}); + +const boardSubmitFailure = (error: string): BoardSubmitFailureAction => ({ + type: BOARD_SUBMIT_FAILURE, + error, +}); + +export const submitBoard = ( + name: string, + description: string, + authenticityToken: string, +): ThunkAction> => async (dispatch) => { + dispatch(boardSubmitStart()); + + try { + const res = await fetch(`/boards`, { + method: 'POST', + headers: buildRequestHeaders(authenticityToken), + body: JSON.stringify({ + board: { + name, + description, + }, + }), + }); + const json = await res.json(); + + if (res.status === HttpStatus.Created) { + dispatch(boardSubmitSuccess(json)); + } else { + dispatch(boardSubmitFailure(json.error)); + } + + return Promise.resolve(res); + } catch (e) { + dispatch(boardSubmitFailure(e)); + + return Promise.resolve(null); + } +}; \ No newline at end of file diff --git a/app/javascript/actions/Board/updateBoard.ts b/app/javascript/actions/Board/updateBoard.ts new file mode 100644 index 00000000..2cd1f914 --- /dev/null +++ b/app/javascript/actions/Board/updateBoard.ts @@ -0,0 +1,80 @@ +import { Action } from "redux"; +import { ThunkAction } from "redux-thunk"; + +import HttpStatus from "../../constants/http_status"; +import buildRequestHeaders from "../../helpers/buildRequestHeaders"; +import IBoardJSON from "../../interfaces/json/IBoard"; +import { State } from "../../reducers/rootReducer"; + +export const BOARD_UPDATE_START = 'BOARD_UPDATE_START'; +interface BoardUpdateStartAction { + type: typeof BOARD_UPDATE_START; +} + +export const BOARD_UPDATE_SUCCESS = 'BOARD_UPDATE_SUCCESS'; +interface BoardUpdateSuccessAction { + type: typeof BOARD_UPDATE_SUCCESS; + board: IBoardJSON; +} + +export const BOARD_UPDATE_FAILURE = 'BOARD_UPDATE_FAILURE'; +interface BoardUpdateFailureAction { + type: typeof BOARD_UPDATE_FAILURE; + error: string; +} + +export type BoardUpdateActionTypes = + BoardUpdateStartAction | + BoardUpdateSuccessAction | + BoardUpdateFailureAction; + +const boardUpdateStart = (): BoardUpdateStartAction => ({ + type: BOARD_UPDATE_START, +}); + +const boardUpdateSuccess = ( + boardJSON: IBoardJSON, +): BoardUpdateSuccessAction => ({ + type: BOARD_UPDATE_SUCCESS, + board: boardJSON, +}); + +const boardUpdateFailure = (error: string): BoardUpdateFailureAction => ({ + type: BOARD_UPDATE_FAILURE, + error, +}); + +export const updateBoard = ( + id: number, + name: string, + description: string, + authenticityToken: string, +): ThunkAction> => async (dispatch) => { + dispatch(boardUpdateStart()); + + try { + const res = await fetch(`/boards/${id}`, { + method: 'PATCH', + headers: buildRequestHeaders(authenticityToken), + body: JSON.stringify({ + board: { + name, + description, + }, + }), + }); + const json = await res.json(); + + if (res.status === HttpStatus.OK) { + dispatch(boardUpdateSuccess(json)); + } else { + dispatch(boardUpdateFailure(json.error)); + } + + return Promise.resolve(res); + } catch (e) { + dispatch(boardUpdateFailure(e)); + + return Promise.resolve(null); + } +}; \ No newline at end of file diff --git a/app/javascript/actions/Board/updateBoardOrder.ts b/app/javascript/actions/Board/updateBoardOrder.ts new file mode 100644 index 00000000..15adee1d --- /dev/null +++ b/app/javascript/actions/Board/updateBoardOrder.ts @@ -0,0 +1,89 @@ +import { Action } from "redux"; +import { ThunkAction } from "redux-thunk"; + +import HttpStatus from "../../constants/http_status"; +import buildRequestHeaders from "../../helpers/buildRequestHeaders"; +import createNewOrdering from "../../helpers/createNewOrdering"; +import IBoard from "../../interfaces/IBoard"; + +import { State } from "../../reducers/rootReducer"; + +export const BOARD_ORDER_UPDATE_START = 'BOARD_ORDER_UPDATE_START'; +interface BoardOrderUpdateStartAction { + type: typeof BOARD_ORDER_UPDATE_START; + newOrder: Array; +} + +export const BOARD_ORDER_UPDATE_SUCCESS = 'BOARD_ORDER_UPDATE_SUCCESS'; +interface BoardOrderUpdateSuccessAction { + type: typeof BOARD_ORDER_UPDATE_SUCCESS; +} + +export const BOARD_ORDER_UPDATE_FAILURE = 'BOARD_ORDER_UPDATE_FAILURE'; +interface BoardOrderUpdateFailureAction { + type: typeof BOARD_ORDER_UPDATE_FAILURE; + error: string; + oldOrder: Array; +} + +export type BoardOrderUpdateActionTypes = + BoardOrderUpdateStartAction | + BoardOrderUpdateSuccessAction | + BoardOrderUpdateFailureAction; + +const boardOrderUpdateStart = ( + newOrder: Array +): BoardOrderUpdateStartAction => ({ + type: BOARD_ORDER_UPDATE_START, + newOrder, +}); + +const boardOrderUpdateSuccess = (): BoardOrderUpdateSuccessAction => ({ + type: BOARD_ORDER_UPDATE_SUCCESS, +}); + +const boardOrderUpdateFailure = ( + error: string, + oldOrder: Array +): BoardOrderUpdateFailureAction => ({ + type: BOARD_ORDER_UPDATE_FAILURE, + error, + oldOrder, +}); + +export const updateBoardOrder = ( + id: number, + boards: Array, + sourceIndex: number, + destinationIndex: number, + + authenticityToken: string, +): ThunkAction> => async (dispatch) => { + const oldOrder = boards; + let newOrder = createNewOrdering(boards, sourceIndex, destinationIndex); + + dispatch(boardOrderUpdateStart(newOrder)); + + try { + const res = await fetch(`/boards/update_order`, { + method: 'PATCH', + headers: buildRequestHeaders(authenticityToken), + body: JSON.stringify({ + board: { + id: id, + src_index: sourceIndex, + dst_index: destinationIndex, + }, + }), + }); + const json = await res.json(); + + if (res.status === HttpStatus.OK) { + dispatch(boardOrderUpdateSuccess()); + } else { + dispatch(boardOrderUpdateFailure(json.error, oldOrder)); + } + } catch (e) { + dispatch(boardOrderUpdateFailure(e, oldOrder)); + } +}; diff --git a/app/javascript/actions/PostStatus/deletePostStatus.ts b/app/javascript/actions/PostStatus/deletePostStatus.ts index ab9dff2f..c769bc6b 100644 --- a/app/javascript/actions/PostStatus/deletePostStatus.ts +++ b/app/javascript/actions/PostStatus/deletePostStatus.ts @@ -1,5 +1,6 @@ import { Action } from "redux"; import { ThunkAction } from "redux-thunk"; +import HttpStatus from "../../constants/http_status"; import buildRequestHeaders from "../../helpers/buildRequestHeaders"; import { State } from "../../reducers/rootReducer"; @@ -49,12 +50,17 @@ export const deletePostStatus = ( dispatch(postStatusDeleteStart()); try { - const response = await fetch(`/post_statuses/${id}`, { + const res = await fetch(`/post_statuses/${id}`, { method: 'DELETE', headers: buildRequestHeaders(authenticityToken), }); - const json = await response.json(); - dispatch(postStatusDeleteSuccess(id)); + const json = await res.json(); + + if (res.status === HttpStatus.Accepted) { + dispatch(postStatusDeleteSuccess(id)); + } else { + dispatch(postStatusDeleteFailure(json.error)); + } } catch (e) { dispatch(postStatusDeleteFailure(e)); } diff --git a/app/javascript/actions/PostStatus/requestPostStatuses.ts b/app/javascript/actions/PostStatus/requestPostStatuses.ts index 0663a69a..21c2eafb 100644 --- a/app/javascript/actions/PostStatus/requestPostStatuses.ts +++ b/app/javascript/actions/PostStatus/requestPostStatuses.ts @@ -25,7 +25,7 @@ interface PostStatusesRequestFailureAction { export type PostStatusesRequestActionTypes = PostStatusesRequestStartAction | PostStatusesRequestSuccessAction | - PostStatusesRequestFailureAction + PostStatusesRequestFailureAction; const postStatusesRequestStart = (): PostStatusesRequestActionTypes => ({ diff --git a/app/javascript/actions/PostStatus/updatePostStatusOrder.ts b/app/javascript/actions/PostStatus/updatePostStatusOrder.ts index 5606d2c3..2d5acad5 100644 --- a/app/javascript/actions/PostStatus/updatePostStatusOrder.ts +++ b/app/javascript/actions/PostStatus/updatePostStatusOrder.ts @@ -3,6 +3,7 @@ import { ThunkAction } from "redux-thunk"; import HttpStatus from "../../constants/http_status"; import buildRequestHeaders from "../../helpers/buildRequestHeaders"; +import createNewOrdering from "../../helpers/createNewOrdering"; import IPostStatus from "../../interfaces/IPostStatus"; import { State } from "../../reducers/rootReducer"; @@ -21,6 +22,7 @@ export const POSTSTATUS_ORDER_UPDATE_FAILURE = 'POSTSTATUS_ORDER_UPDATE_FAILURE' interface PostStatusOrderUpdateFailureAction { type: typeof POSTSTATUS_ORDER_UPDATE_FAILURE; error: string; + oldOrder: Array; } export type PostStatusOrderUpdateActionTypes = @@ -40,10 +42,12 @@ const postStatusOrderUpdateSuccess = (): PostStatusOrderUpdateSuccessAction => ( }); const postStatusOrderUpdateFailure = ( - error: string + error: string, + oldOrder: Array ): PostStatusOrderUpdateFailureAction => ({ type: POSTSTATUS_ORDER_UPDATE_FAILURE, error, + oldOrder, }); export const updatePostStatusOrder = ( @@ -54,7 +58,8 @@ export const updatePostStatusOrder = ( authenticityToken: string, ): ThunkAction> => async (dispatch) => { - let newOrder = createNewOrder(postStatuses, sourceIndex, destinationIndex); + const oldOrder = postStatuses; + let newOrder = createNewOrdering(postStatuses, sourceIndex, destinationIndex); dispatch(postStatusOrderUpdateStart(newOrder)); @@ -75,22 +80,10 @@ export const updatePostStatusOrder = ( if (res.status === HttpStatus.OK) { dispatch(postStatusOrderUpdateSuccess()); } else { - dispatch(postStatusOrderUpdateFailure(json.error)); + dispatch(postStatusOrderUpdateFailure(json.error, oldOrder)); } } catch (e) { - dispatch(postStatusOrderUpdateFailure(e)); + dispatch(postStatusOrderUpdateFailure(e, oldOrder)); } }; -function createNewOrder( - oldOrder: Array, - sourceIndex: number, - destinationIndex: number -) { - let newOrder = JSON.parse(JSON.stringify(oldOrder)); - - const [reorderedItem] = newOrder.splice(sourceIndex, 1); - newOrder.splice(destinationIndex, 0, reorderedItem); - - return newOrder; -} \ No newline at end of file diff --git a/app/javascript/components/SiteSettings/Boards/BoardEditable.tsx b/app/javascript/components/SiteSettings/Boards/BoardEditable.tsx new file mode 100644 index 00000000..d8f90af2 --- /dev/null +++ b/app/javascript/components/SiteSettings/Boards/BoardEditable.tsx @@ -0,0 +1,121 @@ +import * as React from 'react'; + +import { Draggable } from 'react-beautiful-dnd'; +import { DescriptionText } from '../../shared/CustomTexts'; + +import DragZone from '../../shared/DragZone'; +import PostBoardLabel from '../../shared/PostBoardLabel'; +import Separator from '../../shared/Separator'; +import BoardForm from './BoardForm'; + +interface Props { + id: number; + name: string; + description?: string; + index: number; + settingsAreUpdating: boolean; + + handleUpdate( + id: number, + description: string, + name: string, + onSuccess: Function, + ): void; + handleDelete(id: number): void; +} + +interface State { + editMode: boolean; +} + +class BoardsEditable extends React.Component { + 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, description: string) { + this.props.handleUpdate( + id, + name, + description, + () => this.setState({editMode: false}), + ); + } + + render() { + const { + id, + name, + description, + index, + settingsAreUpdating, + handleDelete, + } = this.props; + const { editMode } = this.state; + + return ( + + {(provided, snapshot) => ( +
  • + + + { editMode === false ? + +
    +
    + +
    +
    + {description} +
    +
    + + +
    + : + + + + + Cancel + + + } +
  • + )} +
    + ); + } +} + +export default BoardsEditable; \ No newline at end of file diff --git a/app/javascript/components/SiteSettings/Boards/BoardForm.tsx b/app/javascript/components/SiteSettings/Boards/BoardForm.tsx new file mode 100644 index 00000000..8fcba403 --- /dev/null +++ b/app/javascript/components/SiteSettings/Boards/BoardForm.tsx @@ -0,0 +1,112 @@ +import * as React from 'react'; + +import Button from '../../shared/Button'; + +interface Props { + mode: 'create' | 'update'; + + id?: number; + name?: string; + description?: string; + + handleSubmit?( + name: string, + description: string, + onSuccess: Function, + ): void; + handleUpdate?( + id: number, + name: string, + description?: string, + ): void; +} + +interface State { + name: string; + description: string; +} + +class BoardForm extends React.Component { + initialState: State = { + name: '', + description: '', + }; + + constructor(props: Props) { + super(props); + + this.state = this.props.mode === 'create' ? + this.initialState + : + { + name: this.props.name, + description: this.props.description, + }; + + this.onSubmit = this.onSubmit.bind(this); + } + + isFormValid() { + return this.state.name && this.state.name.length > 0; + } + + onNameChange(nameText: string) { + this.setState({ + name: nameText, + }); + } + + onDescriptionChange(descriptionText: string) { + this.setState({ + description: descriptionText, + }); + } + + onSubmit() { + if (this.props.mode === 'create') { + this.props.handleSubmit( + this.state.name, + this.state.description, + () => this.setState({...this.initialState}), + ); + } else { + this.props.handleUpdate(this.props.id, this.state.name, this.state.description); + } + } + + render() { + const {mode} = this.props; + const {name, description} = this.state; + + return ( +
    +
    + this.onNameChange(e.target.value)} + className="form-control" + /> + + +
    + +