mirror of
https://github.com/astuto/astuto.git
synced 2025-12-16 11:47:56 +01:00
Add Boards management to sitesettings (#107)
This commit is contained in:
committed by
GitHub
parent
7b8a4d6709
commit
6be2394dc5
@@ -1,5 +1,81 @@
|
|||||||
class BoardsController < ApplicationController
|
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
|
def show
|
||||||
@board = Board.find(params[:id])
|
@board = Board.find(params[:id])
|
||||||
end
|
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
|
end
|
||||||
|
|||||||
@@ -6,6 +6,9 @@ class SiteSettingsController < ApplicationController
|
|||||||
def general
|
def general
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def boards
|
||||||
|
end
|
||||||
|
|
||||||
def post_statuses
|
def post_statuses
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
69
app/javascript/actions/Board/deleteBoard.ts
Normal file
69
app/javascript/actions/Board/deleteBoard.ts
Normal file
@@ -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<void, State, null, Action<string>> => (
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
59
app/javascript/actions/Board/requestBoards.ts
Normal file
59
app/javascript/actions/Board/requestBoards.ts
Normal file
@@ -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<IBoard>;
|
||||||
|
}
|
||||||
|
|
||||||
|
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<IBoard>
|
||||||
|
): BoardsRequestActionTypes => ({
|
||||||
|
type: BOARDS_REQUEST_SUCCESS,
|
||||||
|
boards,
|
||||||
|
});
|
||||||
|
|
||||||
|
const boardsRequestFailure = (error: string): BoardsRequestActionTypes => ({
|
||||||
|
type: BOARDS_REQUEST_FAILURE,
|
||||||
|
error,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const requestBoards = (): ThunkAction<void, State, null, Action<string>> => (
|
||||||
|
async (dispatch) => {
|
||||||
|
dispatch(boardsRequestStart());
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/boards');
|
||||||
|
const json = await response.json();
|
||||||
|
dispatch(boardsRequestSuccess(json));
|
||||||
|
} catch (e) {
|
||||||
|
dispatch(boardsRequestFailure(e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
79
app/javascript/actions/Board/submitBoard.ts
Normal file
79
app/javascript/actions/Board/submitBoard.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 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<void, State, null, Action<string>> => 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);
|
||||||
|
}
|
||||||
|
};
|
||||||
80
app/javascript/actions/Board/updateBoard.ts
Normal file
80
app/javascript/actions/Board/updateBoard.ts
Normal file
@@ -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<void, State, null, Action<string>> => 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);
|
||||||
|
}
|
||||||
|
};
|
||||||
89
app/javascript/actions/Board/updateBoardOrder.ts
Normal file
89
app/javascript/actions/Board/updateBoardOrder.ts
Normal file
@@ -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<IBoard>;
|
||||||
|
}
|
||||||
|
|
||||||
|
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<IBoard>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type BoardOrderUpdateActionTypes =
|
||||||
|
BoardOrderUpdateStartAction |
|
||||||
|
BoardOrderUpdateSuccessAction |
|
||||||
|
BoardOrderUpdateFailureAction;
|
||||||
|
|
||||||
|
const boardOrderUpdateStart = (
|
||||||
|
newOrder: Array<IBoard>
|
||||||
|
): BoardOrderUpdateStartAction => ({
|
||||||
|
type: BOARD_ORDER_UPDATE_START,
|
||||||
|
newOrder,
|
||||||
|
});
|
||||||
|
|
||||||
|
const boardOrderUpdateSuccess = (): BoardOrderUpdateSuccessAction => ({
|
||||||
|
type: BOARD_ORDER_UPDATE_SUCCESS,
|
||||||
|
});
|
||||||
|
|
||||||
|
const boardOrderUpdateFailure = (
|
||||||
|
error: string,
|
||||||
|
oldOrder: Array<IBoard>
|
||||||
|
): BoardOrderUpdateFailureAction => ({
|
||||||
|
type: BOARD_ORDER_UPDATE_FAILURE,
|
||||||
|
error,
|
||||||
|
oldOrder,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const updateBoardOrder = (
|
||||||
|
id: number,
|
||||||
|
boards: Array<IBoard>,
|
||||||
|
sourceIndex: number,
|
||||||
|
destinationIndex: number,
|
||||||
|
|
||||||
|
authenticityToken: string,
|
||||||
|
): ThunkAction<void, State, null, Action<string>> => 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));
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Action } from "redux";
|
import { Action } from "redux";
|
||||||
import { ThunkAction } from "redux-thunk";
|
import { ThunkAction } from "redux-thunk";
|
||||||
|
import HttpStatus from "../../constants/http_status";
|
||||||
import buildRequestHeaders from "../../helpers/buildRequestHeaders";
|
import buildRequestHeaders from "../../helpers/buildRequestHeaders";
|
||||||
import { State } from "../../reducers/rootReducer";
|
import { State } from "../../reducers/rootReducer";
|
||||||
|
|
||||||
@@ -49,12 +50,17 @@ export const deletePostStatus = (
|
|||||||
dispatch(postStatusDeleteStart());
|
dispatch(postStatusDeleteStart());
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`/post_statuses/${id}`, {
|
const res = await fetch(`/post_statuses/${id}`, {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
headers: buildRequestHeaders(authenticityToken),
|
headers: buildRequestHeaders(authenticityToken),
|
||||||
});
|
});
|
||||||
const json = await response.json();
|
const json = await res.json();
|
||||||
dispatch(postStatusDeleteSuccess(id));
|
|
||||||
|
if (res.status === HttpStatus.Accepted) {
|
||||||
|
dispatch(postStatusDeleteSuccess(id));
|
||||||
|
} else {
|
||||||
|
dispatch(postStatusDeleteFailure(json.error));
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
dispatch(postStatusDeleteFailure(e));
|
dispatch(postStatusDeleteFailure(e));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ interface PostStatusesRequestFailureAction {
|
|||||||
export type PostStatusesRequestActionTypes =
|
export type PostStatusesRequestActionTypes =
|
||||||
PostStatusesRequestStartAction |
|
PostStatusesRequestStartAction |
|
||||||
PostStatusesRequestSuccessAction |
|
PostStatusesRequestSuccessAction |
|
||||||
PostStatusesRequestFailureAction
|
PostStatusesRequestFailureAction;
|
||||||
|
|
||||||
|
|
||||||
const postStatusesRequestStart = (): PostStatusesRequestActionTypes => ({
|
const postStatusesRequestStart = (): PostStatusesRequestActionTypes => ({
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { ThunkAction } from "redux-thunk";
|
|||||||
import HttpStatus from "../../constants/http_status";
|
import HttpStatus from "../../constants/http_status";
|
||||||
|
|
||||||
import buildRequestHeaders from "../../helpers/buildRequestHeaders";
|
import buildRequestHeaders from "../../helpers/buildRequestHeaders";
|
||||||
|
import createNewOrdering from "../../helpers/createNewOrdering";
|
||||||
import IPostStatus from "../../interfaces/IPostStatus";
|
import IPostStatus from "../../interfaces/IPostStatus";
|
||||||
import { State } from "../../reducers/rootReducer";
|
import { State } from "../../reducers/rootReducer";
|
||||||
|
|
||||||
@@ -21,6 +22,7 @@ export const POSTSTATUS_ORDER_UPDATE_FAILURE = 'POSTSTATUS_ORDER_UPDATE_FAILURE'
|
|||||||
interface PostStatusOrderUpdateFailureAction {
|
interface PostStatusOrderUpdateFailureAction {
|
||||||
type: typeof POSTSTATUS_ORDER_UPDATE_FAILURE;
|
type: typeof POSTSTATUS_ORDER_UPDATE_FAILURE;
|
||||||
error: string;
|
error: string;
|
||||||
|
oldOrder: Array<IPostStatus>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type PostStatusOrderUpdateActionTypes =
|
export type PostStatusOrderUpdateActionTypes =
|
||||||
@@ -40,10 +42,12 @@ const postStatusOrderUpdateSuccess = (): PostStatusOrderUpdateSuccessAction => (
|
|||||||
});
|
});
|
||||||
|
|
||||||
const postStatusOrderUpdateFailure = (
|
const postStatusOrderUpdateFailure = (
|
||||||
error: string
|
error: string,
|
||||||
|
oldOrder: Array<IPostStatus>
|
||||||
): PostStatusOrderUpdateFailureAction => ({
|
): PostStatusOrderUpdateFailureAction => ({
|
||||||
type: POSTSTATUS_ORDER_UPDATE_FAILURE,
|
type: POSTSTATUS_ORDER_UPDATE_FAILURE,
|
||||||
error,
|
error,
|
||||||
|
oldOrder,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const updatePostStatusOrder = (
|
export const updatePostStatusOrder = (
|
||||||
@@ -54,7 +58,8 @@ export const updatePostStatusOrder = (
|
|||||||
|
|
||||||
authenticityToken: string,
|
authenticityToken: string,
|
||||||
): ThunkAction<void, State, null, Action<string>> => async (dispatch) => {
|
): ThunkAction<void, State, null, Action<string>> => async (dispatch) => {
|
||||||
let newOrder = createNewOrder(postStatuses, sourceIndex, destinationIndex);
|
const oldOrder = postStatuses;
|
||||||
|
let newOrder = createNewOrdering(postStatuses, sourceIndex, destinationIndex);
|
||||||
|
|
||||||
dispatch(postStatusOrderUpdateStart(newOrder));
|
dispatch(postStatusOrderUpdateStart(newOrder));
|
||||||
|
|
||||||
@@ -75,22 +80,10 @@ export const updatePostStatusOrder = (
|
|||||||
if (res.status === HttpStatus.OK) {
|
if (res.status === HttpStatus.OK) {
|
||||||
dispatch(postStatusOrderUpdateSuccess());
|
dispatch(postStatusOrderUpdateSuccess());
|
||||||
} else {
|
} else {
|
||||||
dispatch(postStatusOrderUpdateFailure(json.error));
|
dispatch(postStatusOrderUpdateFailure(json.error, oldOrder));
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
dispatch(postStatusOrderUpdateFailure(e));
|
dispatch(postStatusOrderUpdateFailure(e, oldOrder));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
121
app/javascript/components/SiteSettings/Boards/BoardEditable.tsx
Normal file
121
app/javascript/components/SiteSettings/Boards/BoardEditable.tsx
Normal file
@@ -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<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, 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 (
|
||||||
|
<Draggable key={id} draggableId={id.toString()} index={index} isDragDisabled={settingsAreUpdating}>
|
||||||
|
{(provided, snapshot) => (
|
||||||
|
<li className={`boardEditable${snapshot.isDragging ? ' dragging' : ''}`} ref={provided.innerRef} {...provided.draggableProps}>
|
||||||
|
<DragZone dndProvided={provided} isDragDisabled={settingsAreUpdating} />
|
||||||
|
|
||||||
|
{ editMode === false ?
|
||||||
|
<React.Fragment>
|
||||||
|
<div className="boardInfo">
|
||||||
|
<div className="boardName">
|
||||||
|
<PostBoardLabel name={name} />
|
||||||
|
</div>
|
||||||
|
<div className="boardDescription">
|
||||||
|
<DescriptionText limit={80}>{description}</DescriptionText>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="boardEditableActions">
|
||||||
|
<a onClick={this.toggleEditMode}>Edit</a>
|
||||||
|
|
||||||
|
<Separator />
|
||||||
|
|
||||||
|
<a
|
||||||
|
onClick={() => handleDelete(id)}
|
||||||
|
data-confirm="Are you sure?"
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</React.Fragment>
|
||||||
|
:
|
||||||
|
<React.Fragment>
|
||||||
|
<BoardForm
|
||||||
|
mode='update'
|
||||||
|
id={id}
|
||||||
|
name={name}
|
||||||
|
description={description}
|
||||||
|
handleUpdate={this.handleUpdate}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<a
|
||||||
|
className="boardFormCancelButton"
|
||||||
|
onClick={this.toggleEditMode}>
|
||||||
|
Cancel
|
||||||
|
</a>
|
||||||
|
</React.Fragment>
|
||||||
|
}
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
</Draggable>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default BoardsEditable;
|
||||||
112
app/javascript/components/SiteSettings/Boards/BoardForm.tsx
Normal file
112
app/javascript/components/SiteSettings/Boards/BoardForm.tsx
Normal file
@@ -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<Props, State> {
|
||||||
|
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 (
|
||||||
|
<div className="boardForm">
|
||||||
|
<div className="boardMandatoryForm">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Board name"
|
||||||
|
value={name}
|
||||||
|
onChange={e => this.onNameChange(e.target.value)}
|
||||||
|
className="form-control"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
onClick={this.onSubmit}
|
||||||
|
className="newBoardButton"
|
||||||
|
disabled={!this.isFormValid()}
|
||||||
|
>
|
||||||
|
{mode === 'create' ? 'Create' : 'Save'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<textarea
|
||||||
|
placeholder="Optional board description"
|
||||||
|
value={description}
|
||||||
|
onChange={e => this.onDescriptionChange(e.target.value)}
|
||||||
|
className="form-control"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default BoardForm;
|
||||||
@@ -0,0 +1,140 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
|
||||||
|
import { DragDropContext, Droppable } from 'react-beautiful-dnd';
|
||||||
|
|
||||||
|
import BoardEditable from './BoardEditable';
|
||||||
|
import BoardForm from './BoardForm';
|
||||||
|
import SiteSettingsInfoBox from '../../shared/SiteSettingsInfoBox';
|
||||||
|
import Spinner from '../../shared/Spinner';
|
||||||
|
import { BoardsState } from '../../../reducers/boardsReducer';
|
||||||
|
import { CenteredMutedText } from '../../shared/CustomTexts';
|
||||||
|
import IBoard from '../../../interfaces/IBoard';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
authenticityToken: string;
|
||||||
|
|
||||||
|
boards: BoardsState;
|
||||||
|
settingsAreUpdating: boolean;
|
||||||
|
settingsError: string;
|
||||||
|
|
||||||
|
requestBoards(): void;
|
||||||
|
submitBoard(
|
||||||
|
name: string,
|
||||||
|
description: string,
|
||||||
|
onSuccess: Function,
|
||||||
|
authenticityToken: string,
|
||||||
|
): void;
|
||||||
|
updateBoard(
|
||||||
|
id: number,
|
||||||
|
name: string,
|
||||||
|
description: string,
|
||||||
|
onSuccess: Function,
|
||||||
|
authenticityToken: string,
|
||||||
|
): void;
|
||||||
|
updateBoardOrder(
|
||||||
|
id: number,
|
||||||
|
boards: Array<IBoard>,
|
||||||
|
sourceIndex: number,
|
||||||
|
destinationIndex: number,
|
||||||
|
authenticityToken: string,
|
||||||
|
): void;
|
||||||
|
deleteBoard(id: number, authenticityToken: string): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
class BoardsSiteSettingsP 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.requestBoards();
|
||||||
|
}
|
||||||
|
|
||||||
|
handleSubmit(name: string, description: string, onSuccess: Function) {
|
||||||
|
this.props.submitBoard(name, description, onSuccess, this.props.authenticityToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleUpdate(id: number, name: string, description: string, onSuccess: Function) {
|
||||||
|
this.props.updateBoard(id, name, description, onSuccess, this.props.authenticityToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleDragEnd(result) {
|
||||||
|
if (result.destination == null || result.source.index === result.destination.index)
|
||||||
|
return;
|
||||||
|
|
||||||
|
this.props.updateBoardOrder(
|
||||||
|
parseInt(result.draggableId),
|
||||||
|
this.props.boards.items,
|
||||||
|
result.source.index,
|
||||||
|
result.destination.index,
|
||||||
|
this.props.authenticityToken,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleDelete(id: number) {
|
||||||
|
this.props.deleteBoard(id, this.props.authenticityToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
boards,
|
||||||
|
settingsAreUpdating,
|
||||||
|
settingsError,
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
<div className="content">
|
||||||
|
<h2>Boards</h2>
|
||||||
|
|
||||||
|
{
|
||||||
|
boards.items.length > 0 ?
|
||||||
|
<DragDropContext onDragEnd={this.handleDragEnd}>
|
||||||
|
<Droppable droppableId="boards">
|
||||||
|
{provided => (
|
||||||
|
<ul ref={provided.innerRef} {...provided.droppableProps} className="boardsList">
|
||||||
|
{boards.items.map((board, i) => (
|
||||||
|
<BoardEditable
|
||||||
|
id={board.id}
|
||||||
|
name={board.name}
|
||||||
|
description={board.description}
|
||||||
|
index={i}
|
||||||
|
settingsAreUpdating={settingsAreUpdating}
|
||||||
|
|
||||||
|
handleUpdate={this.handleUpdate}
|
||||||
|
handleDelete={this.handleDelete}
|
||||||
|
|
||||||
|
key={board.id}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
{provided.placeholder}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
</Droppable>
|
||||||
|
</DragDropContext>
|
||||||
|
:
|
||||||
|
boards.areLoading ?
|
||||||
|
<Spinner />
|
||||||
|
:
|
||||||
|
<CenteredMutedText>There are no boards. Create one below!</CenteredMutedText>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="content">
|
||||||
|
<h2>New</h2>
|
||||||
|
|
||||||
|
<BoardForm mode='create' handleSubmit={this.handleSubmit} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<SiteSettingsInfoBox areUpdating={settingsAreUpdating || boards.areLoading} error={settingsError} />
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default BoardsSiteSettingsP;
|
||||||
33
app/javascript/components/SiteSettings/Boards/index.tsx
Normal file
33
app/javascript/components/SiteSettings/Boards/index.tsx
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import { Provider } from 'react-redux';
|
||||||
|
import { Store } from 'redux';
|
||||||
|
import BoardsSiteSettings from '../../../containers/BoardsSiteSettings';
|
||||||
|
|
||||||
|
import createStoreHelper from '../../../helpers/createStore';
|
||||||
|
import { State } from '../../../reducers/rootReducer';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
authenticityToken: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
class BoardsSiteSettingsRoot extends React.Component<Props> {
|
||||||
|
store: Store<State, any>;
|
||||||
|
|
||||||
|
constructor(props: Props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.store = createStoreHelper();
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<Provider store={this.store}>
|
||||||
|
<BoardsSiteSettings
|
||||||
|
authenticityToken={this.props.authenticityToken}
|
||||||
|
/>
|
||||||
|
</Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default BoardsSiteSettingsRoot;
|
||||||
@@ -89,7 +89,7 @@ class PostStatusForm extends React.Component<Props, State> {
|
|||||||
<div className="postStatusForm">
|
<div className="postStatusForm">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Name"
|
placeholder="Post status name"
|
||||||
value={name}
|
value={name}
|
||||||
onChange={e => this.onNameChange(e.target.value)}
|
onChange={e => this.onNameChange(e.target.value)}
|
||||||
className="form-control"
|
className="form-control"
|
||||||
@@ -99,7 +99,7 @@ class PostStatusForm extends React.Component<Props, State> {
|
|||||||
type="color"
|
type="color"
|
||||||
value={color}
|
value={color}
|
||||||
onChange={e => this.onColorChange(e.target.value)}
|
onChange={e => this.onColorChange(e.target.value)}
|
||||||
className="form-control"
|
className="form-control postStatusColorInput"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ class PostStatusesSiteSettingsP extends React.Component<Props> {
|
|||||||
<DragDropContext onDragEnd={this.handleDragEnd}>
|
<DragDropContext onDragEnd={this.handleDragEnd}>
|
||||||
<Droppable droppableId="postStatuses">
|
<Droppable droppableId="postStatuses">
|
||||||
{provided => (
|
{provided => (
|
||||||
<ul ref={provided.innerRef} {...provided.droppableProps} className="postStatusList">
|
<ul ref={provided.innerRef} {...provided.droppableProps} className="postStatusesList">
|
||||||
{postStatuses.items.map((postStatus, i) => (
|
{postStatuses.items.map((postStatus, i) => (
|
||||||
<PostStatusEditable
|
<PostStatusEditable
|
||||||
id={postStatus.id}
|
id={postStatus.id}
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
|
|
||||||
import IBoard from '../../interfaces/IBoard';
|
interface Props {
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
const PostBoardLabel = ({ name }: IBoard) => (
|
const PostBoardLabel = ({ name }: Props) => (
|
||||||
<span className="badge badgeLight">{name?.toUpperCase()}</span>
|
<span className="badge badgeLight">{name?.toUpperCase()}</span>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ const SiteSettingsInfoBox = ({ areUpdating, error }: Props) => (
|
|||||||
<Spinner />
|
<Spinner />
|
||||||
:
|
:
|
||||||
error ?
|
error ?
|
||||||
<span className="error">An error occurred: {error}</span>
|
<span className="error">An error occurred: {JSON.stringify(error)}</span>
|
||||||
:
|
:
|
||||||
<span>Everything up to date</span>
|
<span>Everything up to date</span>
|
||||||
}
|
}
|
||||||
|
|||||||
64
app/javascript/containers/BoardsSiteSettings.tsx
Normal file
64
app/javascript/containers/BoardsSiteSettings.tsx
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
import { connect } from "react-redux";
|
||||||
|
|
||||||
|
import BoardsSiteSettingsP from "../components/SiteSettings/Boards/BoardsSiteSettingsP";
|
||||||
|
import { requestBoards } from "../actions/Board/requestBoards";
|
||||||
|
import { updateBoardOrder } from "../actions/Board/updateBoardOrder";
|
||||||
|
import IBoard from "../interfaces/IBoard";
|
||||||
|
import { State } from "../reducers/rootReducer";
|
||||||
|
import { submitBoard } from "../actions/Board/submitBoard";
|
||||||
|
import HttpStatus from "../constants/http_status";
|
||||||
|
import { deleteBoard } from "../actions/Board/deleteBoard";
|
||||||
|
import { updateBoard } from "../actions/Board/updateBoard";
|
||||||
|
|
||||||
|
const mapStateToProps = (state: State) => ({
|
||||||
|
boards: state.boards,
|
||||||
|
settingsAreUpdating: state.siteSettings.boards.areUpdating,
|
||||||
|
settingsError: state.siteSettings.boards.error,
|
||||||
|
});
|
||||||
|
|
||||||
|
const mapDispatchToProps = (dispatch: any) => ({
|
||||||
|
requestBoards() {
|
||||||
|
dispatch(requestBoards());
|
||||||
|
},
|
||||||
|
|
||||||
|
submitBoard(
|
||||||
|
name: string,
|
||||||
|
description: string,
|
||||||
|
onSuccess: Function,
|
||||||
|
authenticityToken: string,
|
||||||
|
) {
|
||||||
|
dispatch(submitBoard(name, description, authenticityToken)).then(res => {
|
||||||
|
if (res && res.status === HttpStatus.Created) onSuccess();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
updateBoard(
|
||||||
|
id: number,
|
||||||
|
name: string,
|
||||||
|
description: string,
|
||||||
|
onSuccess: Function,
|
||||||
|
authenticityToken: string,
|
||||||
|
) {
|
||||||
|
dispatch(updateBoard(id, name, description, authenticityToken)).then(res => {
|
||||||
|
if (res && res.status === HttpStatus.OK) onSuccess();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
updateBoardOrder(
|
||||||
|
id: number,
|
||||||
|
boards: Array<IBoard>,
|
||||||
|
sourceIndex: number,
|
||||||
|
destinationIndex: number,
|
||||||
|
authenticityToken: string) {
|
||||||
|
dispatch(updateBoardOrder(id, boards, sourceIndex, destinationIndex, authenticityToken));
|
||||||
|
},
|
||||||
|
|
||||||
|
deleteBoard(id: number, authenticityToken: string) {
|
||||||
|
dispatch(deleteBoard(id, authenticityToken));
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default connect(
|
||||||
|
mapStateToProps,
|
||||||
|
mapDispatchToProps
|
||||||
|
)(BoardsSiteSettingsP);
|
||||||
@@ -1,11 +1,12 @@
|
|||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { deletePostStatus } from "../actions/PostStatus/deletePostStatus";
|
|
||||||
|
import PostStatusesSiteSettingsP from "../components/SiteSettings/PostStatuses/PostStatusesSiteSettingsP";
|
||||||
|
|
||||||
import { requestPostStatuses } from "../actions/PostStatus/requestPostStatuses";
|
import { requestPostStatuses } from "../actions/PostStatus/requestPostStatuses";
|
||||||
import { submitPostStatus } from "../actions/PostStatus/submitPostStatus";
|
import { submitPostStatus } from "../actions/PostStatus/submitPostStatus";
|
||||||
import { updatePostStatus } from "../actions/PostStatus/updatePostStatus";
|
import { updatePostStatus } from "../actions/PostStatus/updatePostStatus";
|
||||||
import { updatePostStatusOrder } from "../actions/PostStatus/updatePostStatusOrder";
|
import { updatePostStatusOrder } from "../actions/PostStatus/updatePostStatusOrder";
|
||||||
import PostStatusesSiteSettingsP from "../components/SiteSettings/PostStatuses/PostStatusesSiteSettingsP";
|
import { deletePostStatus } from "../actions/PostStatus/deletePostStatus";
|
||||||
import HttpStatus from "../constants/http_status";
|
import HttpStatus from "../constants/http_status";
|
||||||
import IPostStatus from "../interfaces/IPostStatus";
|
import IPostStatus from "../interfaces/IPostStatus";
|
||||||
import { State } from "../reducers/rootReducer";
|
import { State } from "../reducers/rootReducer";
|
||||||
|
|||||||
14
app/javascript/helpers/createNewOrdering.ts
Normal file
14
app/javascript/helpers/createNewOrdering.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
function createNewOrdering(
|
||||||
|
oldOrder: Array<any>,
|
||||||
|
sourceIndex: number,
|
||||||
|
destinationIndex: number
|
||||||
|
) {
|
||||||
|
let newOrder = JSON.parse(JSON.stringify(oldOrder));
|
||||||
|
|
||||||
|
const [reorderedItem] = newOrder.splice(sourceIndex, 1);
|
||||||
|
newOrder.splice(destinationIndex, 0, reorderedItem);
|
||||||
|
|
||||||
|
return newOrder;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default createNewOrdering;
|
||||||
7
app/javascript/interfaces/json/IBoard.ts
Normal file
7
app/javascript/interfaces/json/IBoard.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
interface IBoardJSON {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default IBoardJSON;
|
||||||
93
app/javascript/reducers/SiteSettings/boardsReducer.ts
Normal file
93
app/javascript/reducers/SiteSettings/boardsReducer.ts
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
import {
|
||||||
|
BoardsRequestActionTypes,
|
||||||
|
BOARDS_REQUEST_START,
|
||||||
|
BOARDS_REQUEST_SUCCESS,
|
||||||
|
BOARDS_REQUEST_FAILURE,
|
||||||
|
} from '../../actions/Board/requestBoards';
|
||||||
|
|
||||||
|
import {
|
||||||
|
BoardSubmitActionTypes,
|
||||||
|
BOARD_SUBMIT_START,
|
||||||
|
BOARD_SUBMIT_SUCCESS,
|
||||||
|
BOARD_SUBMIT_FAILURE,
|
||||||
|
} from '../../actions/Board/submitBoard';
|
||||||
|
|
||||||
|
import {
|
||||||
|
BoardUpdateActionTypes,
|
||||||
|
BOARD_UPDATE_START,
|
||||||
|
BOARD_UPDATE_SUCCESS,
|
||||||
|
BOARD_UPDATE_FAILURE,
|
||||||
|
} from '../../actions/Board/updateBoard';
|
||||||
|
|
||||||
|
import {
|
||||||
|
BoardOrderUpdateActionTypes,
|
||||||
|
BOARD_ORDER_UPDATE_START,
|
||||||
|
BOARD_ORDER_UPDATE_SUCCESS,
|
||||||
|
BOARD_ORDER_UPDATE_FAILURE,
|
||||||
|
} from '../../actions/Board/updateBoardOrder';
|
||||||
|
|
||||||
|
import {
|
||||||
|
BoardDeleteActionTypes,
|
||||||
|
BOARD_DELETE_START,
|
||||||
|
BOARD_DELETE_SUCCESS,
|
||||||
|
BOARD_DELETE_FAILURE,
|
||||||
|
} from '../../actions/Board/deleteBoard';
|
||||||
|
|
||||||
|
export interface SiteSettingsBoardsState {
|
||||||
|
areUpdating: boolean;
|
||||||
|
error: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialState: SiteSettingsBoardsState = {
|
||||||
|
areUpdating: false,
|
||||||
|
error: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
const siteSettingsBoardsReducer = (
|
||||||
|
state = initialState,
|
||||||
|
action:
|
||||||
|
BoardsRequestActionTypes |
|
||||||
|
BoardSubmitActionTypes |
|
||||||
|
BoardUpdateActionTypes |
|
||||||
|
BoardOrderUpdateActionTypes |
|
||||||
|
BoardDeleteActionTypes
|
||||||
|
): SiteSettingsBoardsState => {
|
||||||
|
switch (action.type) {
|
||||||
|
case BOARDS_REQUEST_START:
|
||||||
|
case BOARD_SUBMIT_START:
|
||||||
|
case BOARD_UPDATE_START:
|
||||||
|
case BOARD_ORDER_UPDATE_START:
|
||||||
|
case BOARD_DELETE_START:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
areUpdating: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
case BOARDS_REQUEST_SUCCESS:
|
||||||
|
case BOARD_SUBMIT_SUCCESS:
|
||||||
|
case BOARD_UPDATE_SUCCESS:
|
||||||
|
case BOARD_ORDER_UPDATE_SUCCESS:
|
||||||
|
case BOARD_DELETE_SUCCESS:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
areUpdating: false,
|
||||||
|
error: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
case BOARDS_REQUEST_FAILURE:
|
||||||
|
case BOARD_SUBMIT_FAILURE:
|
||||||
|
case BOARD_UPDATE_FAILURE:
|
||||||
|
case BOARD_ORDER_UPDATE_FAILURE:
|
||||||
|
case BOARD_DELETE_FAILURE:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
areUpdating: false,
|
||||||
|
error: action.error,
|
||||||
|
};
|
||||||
|
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default siteSettingsBoardsReducer;
|
||||||
116
app/javascript/reducers/boardsReducer.ts
Normal file
116
app/javascript/reducers/boardsReducer.ts
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
import {
|
||||||
|
BoardsRequestActionTypes,
|
||||||
|
BOARDS_REQUEST_START,
|
||||||
|
BOARDS_REQUEST_SUCCESS,
|
||||||
|
BOARDS_REQUEST_FAILURE,
|
||||||
|
} from '../actions/Board/requestBoards';
|
||||||
|
|
||||||
|
import {
|
||||||
|
BoardSubmitActionTypes,
|
||||||
|
BOARD_SUBMIT_SUCCESS,
|
||||||
|
} from '../actions/Board/submitBoard';
|
||||||
|
|
||||||
|
import {
|
||||||
|
BoardUpdateActionTypes,
|
||||||
|
BOARD_UPDATE_SUCCESS
|
||||||
|
} from '../actions/Board/updateBoard';
|
||||||
|
|
||||||
|
import {
|
||||||
|
BoardOrderUpdateActionTypes,
|
||||||
|
BOARD_ORDER_UPDATE_START,
|
||||||
|
BOARD_ORDER_UPDATE_FAILURE,
|
||||||
|
} from '../actions/Board/updateBoardOrder';
|
||||||
|
|
||||||
|
import {
|
||||||
|
BoardDeleteActionTypes,
|
||||||
|
BOARD_DELETE_SUCCESS,
|
||||||
|
} from '../actions/Board/deleteBoard';
|
||||||
|
|
||||||
|
import IBoard from "../interfaces/IBoard";
|
||||||
|
|
||||||
|
export interface BoardsState {
|
||||||
|
items: Array<IBoard>;
|
||||||
|
areLoading: boolean;
|
||||||
|
error: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialState: BoardsState = {
|
||||||
|
items: [],
|
||||||
|
areLoading: false,
|
||||||
|
error: '',
|
||||||
|
}
|
||||||
|
|
||||||
|
const boardsReducer = (
|
||||||
|
state = initialState,
|
||||||
|
action:
|
||||||
|
BoardsRequestActionTypes |
|
||||||
|
BoardSubmitActionTypes |
|
||||||
|
BoardUpdateActionTypes |
|
||||||
|
BoardOrderUpdateActionTypes |
|
||||||
|
BoardDeleteActionTypes
|
||||||
|
) => {
|
||||||
|
switch (action.type) {
|
||||||
|
case BOARDS_REQUEST_START:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
areLoading: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
case BOARDS_REQUEST_SUCCESS:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
items: action.boards.map(board => ({
|
||||||
|
id: board.id,
|
||||||
|
name: board.name,
|
||||||
|
description: board.description,
|
||||||
|
})),
|
||||||
|
areLoading: false,
|
||||||
|
error: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
case BOARDS_REQUEST_FAILURE:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
areLoading: false,
|
||||||
|
error: action.error,
|
||||||
|
};
|
||||||
|
|
||||||
|
case BOARD_SUBMIT_SUCCESS:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
items: [...state.items, action.board],
|
||||||
|
};
|
||||||
|
|
||||||
|
case BOARD_UPDATE_SUCCESS:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
items: state.items.map(board => {
|
||||||
|
if (board.id !== action.board.id) return board;
|
||||||
|
return {...board, name: action.board.name, description: action.board.description};
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
case BOARD_ORDER_UPDATE_START:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
items: action.newOrder,
|
||||||
|
};
|
||||||
|
|
||||||
|
case BOARD_ORDER_UPDATE_FAILURE:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
items: action.oldOrder,
|
||||||
|
};
|
||||||
|
|
||||||
|
case BOARD_DELETE_SUCCESS:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
items: state.items.filter(board => board.id !== action.id),
|
||||||
|
};
|
||||||
|
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default boardsReducer;
|
||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
import {
|
import {
|
||||||
PostStatusOrderUpdateActionTypes,
|
PostStatusOrderUpdateActionTypes,
|
||||||
POSTSTATUS_ORDER_UPDATE_START,
|
POSTSTATUS_ORDER_UPDATE_START,
|
||||||
|
POSTSTATUS_ORDER_UPDATE_FAILURE,
|
||||||
} from '../actions/PostStatus/updatePostStatusOrder';
|
} from '../actions/PostStatus/updatePostStatusOrder';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -100,6 +101,12 @@ const postStatusesReducer = (
|
|||||||
items: action.newOrder,
|
items: action.newOrder,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
case POSTSTATUS_ORDER_UPDATE_FAILURE:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
items: action.oldOrder,
|
||||||
|
};
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
import { combineReducers } from 'redux';
|
import { combineReducers } from 'redux';
|
||||||
|
|
||||||
import postsReducer from './postsReducer';
|
import postsReducer from './postsReducer';
|
||||||
|
import boardsReducer from './boardsReducer';
|
||||||
import postStatusesReducer from './postStatusesReducer';
|
import postStatusesReducer from './postStatusesReducer';
|
||||||
import currentPostReducer from './currentPostReducer';
|
import currentPostReducer from './currentPostReducer';
|
||||||
import siteSettingsReducer from './siteSettingsReducer';
|
import siteSettingsReducer from './siteSettingsReducer';
|
||||||
|
|
||||||
const rootReducer = combineReducers({
|
const rootReducer = combineReducers({
|
||||||
posts: postsReducer,
|
posts: postsReducer,
|
||||||
|
boards: boardsReducer,
|
||||||
postStatuses: postStatusesReducer,
|
postStatuses: postStatusesReducer,
|
||||||
currentPost: currentPostReducer,
|
currentPost: currentPostReducer,
|
||||||
siteSettings: siteSettingsReducer,
|
siteSettings: siteSettingsReducer,
|
||||||
|
|||||||
@@ -1,3 +1,38 @@
|
|||||||
|
import {
|
||||||
|
BoardsRequestActionTypes,
|
||||||
|
BOARDS_REQUEST_START,
|
||||||
|
BOARDS_REQUEST_SUCCESS,
|
||||||
|
BOARDS_REQUEST_FAILURE,
|
||||||
|
} from '../actions/Board/requestBoards';
|
||||||
|
|
||||||
|
import {
|
||||||
|
BoardSubmitActionTypes,
|
||||||
|
BOARD_SUBMIT_START,
|
||||||
|
BOARD_SUBMIT_SUCCESS,
|
||||||
|
BOARD_SUBMIT_FAILURE,
|
||||||
|
} from '../actions/Board/submitBoard';
|
||||||
|
|
||||||
|
import {
|
||||||
|
BoardOrderUpdateActionTypes,
|
||||||
|
BOARD_ORDER_UPDATE_START,
|
||||||
|
BOARD_ORDER_UPDATE_SUCCESS,
|
||||||
|
BOARD_ORDER_UPDATE_FAILURE,
|
||||||
|
} from '../actions/Board/updateBoardOrder';
|
||||||
|
|
||||||
|
import {
|
||||||
|
BoardUpdateActionTypes,
|
||||||
|
BOARD_UPDATE_START,
|
||||||
|
BOARD_UPDATE_SUCCESS,
|
||||||
|
BOARD_UPDATE_FAILURE,
|
||||||
|
} from '../actions/Board/updateBoard';
|
||||||
|
|
||||||
|
import {
|
||||||
|
BoardDeleteActionTypes,
|
||||||
|
BOARD_DELETE_START,
|
||||||
|
BOARD_DELETE_SUCCESS,
|
||||||
|
BOARD_DELETE_FAILURE,
|
||||||
|
} from '../actions/Board/deleteBoard';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
PostStatusOrderUpdateActionTypes,
|
PostStatusOrderUpdateActionTypes,
|
||||||
POSTSTATUS_ORDER_UPDATE_START,
|
POSTSTATUS_ORDER_UPDATE_START,
|
||||||
@@ -26,24 +61,53 @@ import {
|
|||||||
POSTSTATUS_UPDATE_FAILURE,
|
POSTSTATUS_UPDATE_FAILURE,
|
||||||
} from '../actions/PostStatus/updatePostStatus';
|
} from '../actions/PostStatus/updatePostStatus';
|
||||||
|
|
||||||
|
import siteSettingsBoardsReducer, { SiteSettingsBoardsState } from './SiteSettings/boardsReducer';
|
||||||
import siteSettingsPostStatusesReducer, { SiteSettingsPostStatusesState } from './SiteSettings/postStatusesReducer';
|
import siteSettingsPostStatusesReducer, { SiteSettingsPostStatusesState } from './SiteSettings/postStatusesReducer';
|
||||||
|
|
||||||
interface SiteSettingsState {
|
interface SiteSettingsState {
|
||||||
|
boards: SiteSettingsBoardsState;
|
||||||
postStatuses: SiteSettingsPostStatusesState;
|
postStatuses: SiteSettingsPostStatusesState;
|
||||||
}
|
}
|
||||||
|
|
||||||
const initialState: SiteSettingsState = {
|
const initialState: SiteSettingsState = {
|
||||||
|
boards: siteSettingsBoardsReducer(undefined, {} as any),
|
||||||
postStatuses: siteSettingsPostStatusesReducer(undefined, {} as any),
|
postStatuses: siteSettingsPostStatusesReducer(undefined, {} as any),
|
||||||
};
|
};
|
||||||
|
|
||||||
const siteSettingsReducer = (
|
const siteSettingsReducer = (
|
||||||
state = initialState,
|
state = initialState,
|
||||||
action: PostStatusOrderUpdateActionTypes |
|
action:
|
||||||
|
BoardsRequestActionTypes |
|
||||||
|
BoardSubmitActionTypes |
|
||||||
|
BoardUpdateActionTypes |
|
||||||
|
BoardOrderUpdateActionTypes |
|
||||||
|
BoardDeleteActionTypes |
|
||||||
|
PostStatusOrderUpdateActionTypes |
|
||||||
PostStatusDeleteActionTypes |
|
PostStatusDeleteActionTypes |
|
||||||
PostStatusSubmitActionTypes |
|
PostStatusSubmitActionTypes |
|
||||||
PostStatusUpdateActionTypes
|
PostStatusUpdateActionTypes
|
||||||
): SiteSettingsState => {
|
): SiteSettingsState => {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
|
case BOARDS_REQUEST_START:
|
||||||
|
case BOARDS_REQUEST_SUCCESS:
|
||||||
|
case BOARDS_REQUEST_FAILURE:
|
||||||
|
case BOARD_SUBMIT_START:
|
||||||
|
case BOARD_SUBMIT_SUCCESS:
|
||||||
|
case BOARD_SUBMIT_FAILURE:
|
||||||
|
case BOARD_UPDATE_START:
|
||||||
|
case BOARD_UPDATE_SUCCESS:
|
||||||
|
case BOARD_UPDATE_FAILURE:
|
||||||
|
case BOARD_ORDER_UPDATE_START:
|
||||||
|
case BOARD_ORDER_UPDATE_SUCCESS:
|
||||||
|
case BOARD_ORDER_UPDATE_FAILURE:
|
||||||
|
case BOARD_DELETE_START:
|
||||||
|
case BOARD_DELETE_SUCCESS:
|
||||||
|
case BOARD_DELETE_FAILURE:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
boards: siteSettingsBoardsReducer(state.boards, action),
|
||||||
|
};
|
||||||
|
|
||||||
case POSTSTATUS_ORDER_UPDATE_START:
|
case POSTSTATUS_ORDER_UPDATE_START:
|
||||||
case POSTSTATUS_ORDER_UPDATE_SUCCESS:
|
case POSTSTATUS_ORDER_UPDATE_SUCCESS:
|
||||||
case POSTSTATUS_ORDER_UPDATE_FAILURE:
|
case POSTSTATUS_ORDER_UPDATE_FAILURE:
|
||||||
@@ -57,7 +121,8 @@ const siteSettingsReducer = (
|
|||||||
case POSTSTATUS_UPDATE_SUCCESS:
|
case POSTSTATUS_UPDATE_SUCCESS:
|
||||||
case POSTSTATUS_UPDATE_FAILURE:
|
case POSTSTATUS_UPDATE_FAILURE:
|
||||||
return {
|
return {
|
||||||
postStatuses: siteSettingsPostStatusesReducer(state.postStatuses, action)
|
...state,
|
||||||
|
postStatuses: siteSettingsPostStatusesReducer(state.postStatuses, action),
|
||||||
};
|
};
|
||||||
|
|
||||||
default:
|
default:
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
.boardsList {
|
||||||
|
@extend
|
||||||
|
.p-0,
|
||||||
|
.m-0;
|
||||||
|
|
||||||
|
list-style: none;
|
||||||
|
|
||||||
|
.boardEditable {
|
||||||
|
@extend
|
||||||
|
.d-flex,
|
||||||
|
.justify-content-between,
|
||||||
|
.p-3;
|
||||||
|
|
||||||
|
column-gap: 32px;
|
||||||
|
|
||||||
|
a {
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover { text-decoration: underline; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.boardInfo {
|
||||||
|
@extend
|
||||||
|
.d-flex,
|
||||||
|
.flex-column;
|
||||||
|
|
||||||
|
row-gap: 8px;
|
||||||
|
|
||||||
|
.boardName { @extend .align-self-center; }
|
||||||
|
.boardDescription { @extend .text-center; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.boardEditableActions, .boardFormCancelButton {
|
||||||
|
@extend .align-self-center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.boardForm {
|
||||||
|
@extend
|
||||||
|
.d-flex,
|
||||||
|
.flex-column,
|
||||||
|
.m-2;
|
||||||
|
|
||||||
|
flex-grow: 1;
|
||||||
|
row-gap: 8px;
|
||||||
|
|
||||||
|
.boardMandatoryForm {
|
||||||
|
@extend .d-flex;
|
||||||
|
|
||||||
|
column-gap: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
.postStatusList {
|
.postStatusesList {
|
||||||
@extend
|
@extend
|
||||||
.p-0,
|
.p-0,
|
||||||
.m-0;
|
.m-0;
|
||||||
@@ -28,5 +28,9 @@
|
|||||||
.d-flex,
|
.d-flex,
|
||||||
.m-2;
|
.m-2;
|
||||||
|
|
||||||
column-gap: 16px;
|
column-gap: 8px;
|
||||||
|
|
||||||
|
.postStatusColorInput {
|
||||||
|
flex: 0 1 100px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -93,7 +93,7 @@ a {
|
|||||||
.drag-zone {
|
.drag-zone {
|
||||||
@extend
|
@extend
|
||||||
.align-self-center,
|
.align-self-center,
|
||||||
.pl-4,
|
.pl-1,
|
||||||
.pr-4,
|
.pr-4,
|
||||||
.pt-1,
|
.pt-1,
|
||||||
.pb-1;
|
.pb-1;
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
|
|
||||||
/* Site Settings Components */
|
/* Site Settings Components */
|
||||||
@import 'components/SiteSettings';
|
@import 'components/SiteSettings';
|
||||||
|
@import 'components/SiteSettings/Boards';
|
||||||
@import 'components/SiteSettings/PostStatuses';
|
@import 'components/SiteSettings/PostStatuses';
|
||||||
|
|
||||||
/* Icons */
|
/* Icons */
|
||||||
|
|||||||
@@ -1,16 +1,8 @@
|
|||||||
class Board < ApplicationRecord
|
class Board < ApplicationRecord
|
||||||
has_many :posts, dependent: :destroy
|
include Orderable
|
||||||
|
|
||||||
after_initialize :set_order_to_last
|
has_many :posts, dependent: :destroy
|
||||||
|
|
||||||
validates :name, presence: true, uniqueness: true
|
validates :name, presence: true, uniqueness: true
|
||||||
validates :description, length: { in: 0..1024 }, allow_nil: true
|
validates :description, length: { in: 0..1024 }, allow_nil: true
|
||||||
|
|
||||||
def set_order_to_last
|
|
||||||
return unless new_record?
|
|
||||||
return unless order.nil?
|
|
||||||
|
|
||||||
order_last = Board.maximum(:order) || 0
|
|
||||||
self.order = order_last + 1
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|||||||
35
app/models/concerns/Orderable.rb
Normal file
35
app/models/concerns/Orderable.rb
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
# An Orderable model is a model with an 'order' column
|
||||||
|
|
||||||
|
# 1) An new Orderable entity gets, by default, an
|
||||||
|
# 'order' equal to current maximum order + 1
|
||||||
|
# 2) When an Orderable entity gets deleted
|
||||||
|
# all other entities are reordered to be consistent
|
||||||
|
|
||||||
|
# Note: update is not handled by the Orderable concern,
|
||||||
|
# but rather in the entity controller action "update_order"
|
||||||
|
|
||||||
|
module Orderable
|
||||||
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
|
included do
|
||||||
|
after_initialize :set_order_to_last
|
||||||
|
after_destroy :ensure_coherent_order
|
||||||
|
|
||||||
|
validates :order, presence: true, numericality: { only_integer: true, greater_than_or_equal_to: 0 }
|
||||||
|
|
||||||
|
def set_order_to_last
|
||||||
|
return unless new_record?
|
||||||
|
return unless order.nil?
|
||||||
|
|
||||||
|
order_last = self.class.maximum(:order) || -1
|
||||||
|
self.order = order_last + 1
|
||||||
|
end
|
||||||
|
|
||||||
|
def ensure_coherent_order
|
||||||
|
EnsureCoherentOrderingWorkflow.new(
|
||||||
|
entity_classname: self.class,
|
||||||
|
column_name: 'order'
|
||||||
|
).run
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -1,12 +1,10 @@
|
|||||||
class PostStatus < ApplicationRecord
|
class PostStatus < ApplicationRecord
|
||||||
has_many :posts, dependent: :nullify
|
include Orderable
|
||||||
|
|
||||||
after_initialize :set_random_color, :set_order_to_last
|
has_many :posts, dependent: :nullify
|
||||||
after_destroy :ensure_coherent_order
|
|
||||||
|
|
||||||
validates :name, presence: true, uniqueness: true
|
validates :name, presence: true, uniqueness: true
|
||||||
validates :color, format: { with: /\A#(?:[0-9a-fA-F]{3}){1,2}\z/ }
|
validates :color, format: { with: /\A#(?:[0-9a-fA-F]{3}){1,2}\z/ }
|
||||||
validates :order, presence: true, numericality: { only_integer: true, greater_than_or_equal_to: 0 }
|
|
||||||
|
|
||||||
class << self
|
class << self
|
||||||
def find_roadmap
|
def find_roadmap
|
||||||
@@ -14,26 +12,4 @@ class PostStatus < ApplicationRecord
|
|||||||
.order(order: :asc)
|
.order(order: :asc)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def set_random_color
|
|
||||||
return unless new_record?
|
|
||||||
return unless color.nil?
|
|
||||||
|
|
||||||
self.color = '#' + Random.bytes(3).unpack1('H*')
|
|
||||||
end
|
|
||||||
|
|
||||||
def set_order_to_last
|
|
||||||
return unless new_record?
|
|
||||||
return unless order.nil?
|
|
||||||
|
|
||||||
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
|
end
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
<div class="verticalNavigation" role="tablist" aria-orientation="vertical">
|
<div class="verticalNavigation" role="tablist" aria-orientation="vertical">
|
||||||
<%= render 'menu_link', label: 'General', path: site_settings_general_path %>
|
<%= render 'menu_link', label: 'General', path: site_settings_general_path %>
|
||||||
<%= render 'menu_link', label: 'Appearance', path: '#' %>
|
<%= render 'menu_link', label: 'Appearance', path: '#' %>
|
||||||
|
<%= render 'menu_link', label: 'Boards', path: site_settings_boards_path %>
|
||||||
<%= render 'menu_link', label: 'Post statuses', path: site_settings_post_statuses_path %>
|
<%= render 'menu_link', label: 'Post statuses', path: site_settings_post_statuses_path %>
|
||||||
<%= render 'menu_link', label: 'Widgets', path: '#' %>
|
<%= render 'menu_link', label: 'Widgets', path: '#' %>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
13
app/views/site_settings/boards.html.erb
Normal file
13
app/views/site_settings/boards.html.erb
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<div class="multiColumnContainer">
|
||||||
|
<%= render 'menu' %>
|
||||||
|
<div class="multiRowContent">
|
||||||
|
<%=
|
||||||
|
react_component(
|
||||||
|
'SiteSettings/Boards',
|
||||||
|
{
|
||||||
|
authenticityToken: form_authenticity_token
|
||||||
|
}
|
||||||
|
)
|
||||||
|
%>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -19,13 +19,18 @@ Rails.application.routes.draw do
|
|||||||
resources :likes, only: [:index]
|
resources :likes, only: [:index]
|
||||||
resources :comments, only: [:index, :create, :update]
|
resources :comments, only: [:index, :create, :update]
|
||||||
end
|
end
|
||||||
resources :boards, only: [:show]
|
|
||||||
|
resources :boards, only: [:index, :create, :update, :destroy, :show] do
|
||||||
|
patch 'update_order', on: :collection
|
||||||
|
end
|
||||||
|
|
||||||
resources :post_statuses, only: [:index, :create, :update, :destroy] do
|
resources :post_statuses, only: [:index, :create, :update, :destroy] do
|
||||||
patch 'update_order', on: :collection
|
patch 'update_order', on: :collection
|
||||||
end
|
end
|
||||||
|
|
||||||
namespace :site_settings do
|
namespace :site_settings do
|
||||||
get 'general'
|
get 'general'
|
||||||
|
get 'boards'
|
||||||
get 'post_statuses'
|
get 'post_statuses'
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
40
db/seeds.rb
40
db/seeds.rb
@@ -8,14 +8,42 @@ admin = User.create(
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Create some boards
|
# Create some boards
|
||||||
feature_board = Board.create(name: 'Feature Requests', description: 'Post here your feature requests.')
|
feature_board = Board.create(
|
||||||
bug_board = Board.create(name: 'Bug Reports', description: 'Post here your bug reports.')
|
name: 'Feature Requests',
|
||||||
|
description: 'Let us know about new features you would like to see in our product!',
|
||||||
|
order: 0
|
||||||
|
)
|
||||||
|
bug_board = Board.create(
|
||||||
|
name: 'Bug Reports',
|
||||||
|
description: 'Tell us everything about problems you encountered in our services!',
|
||||||
|
order: 1
|
||||||
|
)
|
||||||
|
|
||||||
# Create some post statuses
|
# Create some post statuses
|
||||||
planned_post_status = PostStatus.create(name: 'Planned', color: '#0096ff', order: 0, show_in_roadmap: true)
|
planned_post_status = PostStatus.create(
|
||||||
in_progress_post_status = PostStatus.create(name: 'In Progress', color: '#9437ff', order: 1, show_in_roadmap: true)
|
name: 'Planned',
|
||||||
completed_post_status = PostStatus.create(name: 'Completed', color: '#6ac47c', order: 2, show_in_roadmap: true)
|
color: '#0096ff',
|
||||||
rejected_post_status = PostStatus.create(name: 'Rejected', color: '#ff2600', order: 3, show_in_roadmap: false)
|
order: 0,
|
||||||
|
show_in_roadmap: true
|
||||||
|
)
|
||||||
|
in_progress_post_status = PostStatus.create(
|
||||||
|
name: 'In Progress',
|
||||||
|
color: '#9437ff',
|
||||||
|
order: 1,
|
||||||
|
show_in_roadmap: true
|
||||||
|
)
|
||||||
|
completed_post_status = PostStatus.create(
|
||||||
|
name: 'Completed',
|
||||||
|
color: '#6ac47c',
|
||||||
|
order: 2,
|
||||||
|
show_in_roadmap: true
|
||||||
|
)
|
||||||
|
rejected_post_status = PostStatus.create(
|
||||||
|
name: 'Rejected',
|
||||||
|
color: '#ff2600',
|
||||||
|
order: 3,
|
||||||
|
show_in_roadmap: false
|
||||||
|
)
|
||||||
|
|
||||||
# Create some posts
|
# Create some posts
|
||||||
post1 = Post.create(
|
post1 = Post.create(
|
||||||
|
|||||||
@@ -2,6 +2,6 @@ FactoryBot.define do
|
|||||||
factory :board do
|
factory :board do
|
||||||
sequence(:name) { |n| "Board#{n}" }
|
sequence(:name) { |n| "Board#{n}" }
|
||||||
description { 'My fantastic board' }
|
description { 'My fantastic board' }
|
||||||
order { 1 }
|
sequence(:order) { |n| n }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -30,12 +30,19 @@ RSpec.describe Board, type: :model do
|
|||||||
expect(empty_description_board).to be_valid
|
expect(empty_description_board).to be_valid
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'automatically sets order to last order if not specified' do
|
it 'is Orderable' do
|
||||||
order = 10
|
# I didn't used FactoryBot because it didn't apply
|
||||||
board1 = FactoryBot.create(:board, order: order)
|
# the custom logic to the 'order' column
|
||||||
board2 = Board.new
|
|
||||||
|
board1 = Board.create(name: 'b1', order: 0)
|
||||||
|
board2 = Board.create(name: 'b2')
|
||||||
|
board3 = Board.new
|
||||||
|
|
||||||
expect(board1.order).to eq(order)
|
expect(board2.order).to eq(1)
|
||||||
expect(board2.order).to eq(order + 1)
|
expect(board3.order).to eq(2)
|
||||||
|
|
||||||
|
board1.destroy
|
||||||
|
|
||||||
|
expect(board2.reload.order).to eq(0)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -62,18 +62,19 @@ RSpec.describe PostStatus, type: :model do
|
|||||||
expect(roadmap.second).to eq(post_status2)
|
expect(roadmap.second).to eq(post_status2)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'automatically sets a random color if not specified' do
|
it 'is Orderable' do
|
||||||
post_status1 = PostStatus.new
|
# I didn't used FactoryBot because it didn't apply
|
||||||
|
# the custom logic to the 'order' column
|
||||||
|
|
||||||
expect(post_status1.color).to match(/\A#(?:[0-9a-fA-F]{3}){1,2}\z/)
|
post_status1 = PostStatus.create(name: 'ps1', color: '#000000', order: 0)
|
||||||
end
|
post_status2 = PostStatus.create(name: 'ps2', color: '#000000')
|
||||||
|
post_status3 = PostStatus.new
|
||||||
it 'automatically sets order to last order if not specified' do
|
|
||||||
order = 10
|
|
||||||
post_status1 = FactoryBot.create(:post_status, order: order)
|
|
||||||
post_status2 = PostStatus.new
|
|
||||||
|
|
||||||
expect(post_status1.order).to eq(order)
|
expect(post_status2.order).to eq(1)
|
||||||
expect(post_status2.order).to eq(order + 1)
|
expect(post_status3.order).to eq(2)
|
||||||
|
|
||||||
|
post_status1.destroy
|
||||||
|
|
||||||
|
expect(post_status2.reload.order).to eq(0)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -6,13 +6,26 @@ RSpec.describe 'boards routing', :aggregate_failures, type: :routing do
|
|||||||
controller: 'boards', action: 'show', id: '1'
|
controller: 'boards', action: 'show', id: '1'
|
||||||
)
|
)
|
||||||
|
|
||||||
expect(get: '/boards').not_to be_routable
|
expect(get: '/boards').to route_to(
|
||||||
|
controller: 'boards', action: 'index'
|
||||||
|
)
|
||||||
|
|
||||||
expect(get: '/boards/new').not_to route_to(
|
expect(get: '/boards/new').not_to route_to(
|
||||||
controller: 'boards', action: 'new'
|
controller: 'boards', action: 'new'
|
||||||
)
|
)
|
||||||
|
|
||||||
expect(get: '/boards/1/edit').not_to be_routable
|
expect(get: '/boards/1/edit').not_to be_routable
|
||||||
expect(post: '/boards').not_to be_routable
|
|
||||||
expect(patch: '/boards/1').not_to be_routable
|
expect(post: '/boards').to route_to(
|
||||||
expect(delete: '/boards/1').not_to be_routable
|
controller: 'boards', action: 'create'
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(patch: '/boards/1').to route_to(
|
||||||
|
controller: 'boards', action: 'update', id: '1'
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(delete: '/boards/1').to route_to(
|
||||||
|
controller: 'boards', action: 'destroy', id: '1'
|
||||||
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -9,7 +9,12 @@ Capybara.register_driver :chrome_headless do |app|
|
|||||||
options.add_argument('--disable-dev-shm-usage')
|
options.add_argument('--disable-dev-shm-usage')
|
||||||
options.add_argument('--window-size=1400,1400')
|
options.add_argument('--window-size=1400,1400')
|
||||||
|
|
||||||
Capybara::Selenium::Driver.new(app, browser: :chrome, options: options)
|
capabilities = [
|
||||||
|
options,
|
||||||
|
Selenium::WebDriver::Remote::Capabilities.chrome
|
||||||
|
]
|
||||||
|
|
||||||
|
Capybara::Selenium::Driver.new(app, browser: :chrome, capabilities: capabilities)
|
||||||
end
|
end
|
||||||
|
|
||||||
Capybara.javascript_driver = :chrome_headless
|
Capybara.javascript_driver = :chrome_headless
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ feature 'sign up', type: :system do
|
|||||||
fill_in 'Email', with: user.email
|
fill_in 'Email', with: user.email
|
||||||
fill_in 'Password', with: user.password
|
fill_in 'Password', with: user.password
|
||||||
fill_in 'Password confirmation', with: user.password
|
fill_in 'Password confirmation', with: user.password
|
||||||
check 'Notifications enabled'
|
|
||||||
click_button 'Sign up'
|
click_button 'Sign up'
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -76,16 +75,4 @@ feature 'sign up', type: :system do
|
|||||||
expect_to_be_on_sign_up_page
|
expect_to_be_on_sign_up_page
|
||||||
expect(page).to have_css('.alert')
|
expect(page).to have_css('.alert')
|
||||||
end
|
end
|
||||||
|
|
||||||
scenario 'with disabled notifications' do
|
|
||||||
visit new_user_registration_path
|
|
||||||
fill_in 'Full name', with: user.full_name
|
|
||||||
fill_in 'Email', with: user.email
|
|
||||||
fill_in 'Password', with: user.password
|
|
||||||
fill_in 'Password confirmation', with: user.password
|
|
||||||
uncheck 'Notifications enabled'
|
|
||||||
click_button 'Sign up'
|
|
||||||
|
|
||||||
expect(User.last.notifications_enabled).to eq(false)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|||||||
Reference in New Issue
Block a user