Add Boards management to sitesettings (#107)

This commit is contained in:
Riccardo Graziosi
2022-05-08 16:36:35 +02:00
committed by GitHub
parent 7b8a4d6709
commit 6be2394dc5
44 changed files with 1464 additions and 112 deletions

View File

@@ -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

View File

@@ -6,6 +6,9 @@ class SiteSettingsController < ApplicationController
def general
end
def boards
end
def post_statuses
end
end

View 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));
}
}
);

View 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));
}
}
)

View File

@@ -0,0 +1,79 @@
import { Action } from "redux";
import { ThunkAction } from "redux-thunk";
import HttpStatus from "../../constants/http_status";
import buildRequestHeaders from "../../helpers/buildRequestHeaders";
import 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);
}
};

View 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);
}
};

View 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));
}
};

View File

@@ -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));
}

View File

@@ -25,7 +25,7 @@ interface PostStatusesRequestFailureAction {
export type PostStatusesRequestActionTypes =
PostStatusesRequestStartAction |
PostStatusesRequestSuccessAction |
PostStatusesRequestFailureAction
PostStatusesRequestFailureAction;
const postStatusesRequestStart = (): PostStatusesRequestActionTypes => ({

View File

@@ -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<IPostStatus>;
}
export type PostStatusOrderUpdateActionTypes =
@@ -40,10 +42,12 @@ const postStatusOrderUpdateSuccess = (): PostStatusOrderUpdateSuccessAction => (
});
const postStatusOrderUpdateFailure = (
error: string
error: string,
oldOrder: Array<IPostStatus>
): PostStatusOrderUpdateFailureAction => ({
type: POSTSTATUS_ORDER_UPDATE_FAILURE,
error,
oldOrder,
});
export const updatePostStatusOrder = (
@@ -54,7 +58,8 @@ export const updatePostStatusOrder = (
authenticityToken: string,
): 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));
@@ -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<IPostStatus>,
sourceIndex: number,
destinationIndex: number
) {
let newOrder = JSON.parse(JSON.stringify(oldOrder));
const [reorderedItem] = newOrder.splice(sourceIndex, 1);
newOrder.splice(destinationIndex, 0, reorderedItem);
return newOrder;
}

View File

@@ -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;

View 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;

View File

@@ -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;

View 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;

View File

@@ -89,7 +89,7 @@ class PostStatusForm extends React.Component<Props, State> {
<div className="postStatusForm">
<input
type="text"
placeholder="Name"
placeholder="Post status name"
value={name}
onChange={e => this.onNameChange(e.target.value)}
className="form-control"
@@ -99,7 +99,7 @@ class PostStatusForm extends React.Component<Props, State> {
type="color"
value={color}
onChange={e => this.onColorChange(e.target.value)}
className="form-control"
className="form-control postStatusColorInput"
/>
<Button

View File

@@ -92,7 +92,7 @@ class PostStatusesSiteSettingsP extends React.Component<Props> {
<DragDropContext onDragEnd={this.handleDragEnd}>
<Droppable droppableId="postStatuses">
{provided => (
<ul ref={provided.innerRef} {...provided.droppableProps} className="postStatusList">
<ul ref={provided.innerRef} {...provided.droppableProps} className="postStatusesList">
{postStatuses.items.map((postStatus, i) => (
<PostStatusEditable
id={postStatus.id}

View File

@@ -1,8 +1,10 @@
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>
);

View File

@@ -14,7 +14,7 @@ const SiteSettingsInfoBox = ({ areUpdating, error }: Props) => (
<Spinner />
:
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>
}

View 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);

View File

@@ -1,11 +1,12 @@
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 { submitPostStatus } from "../actions/PostStatus/submitPostStatus";
import { updatePostStatus } from "../actions/PostStatus/updatePostStatus";
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 IPostStatus from "../interfaces/IPostStatus";
import { State } from "../reducers/rootReducer";

View 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;

View File

@@ -0,0 +1,7 @@
interface IBoardJSON {
id: number;
name: string;
description?: string;
}
export default IBoardJSON;

View 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;

View 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;

View File

@@ -8,6 +8,7 @@ import {
import {
PostStatusOrderUpdateActionTypes,
POSTSTATUS_ORDER_UPDATE_START,
POSTSTATUS_ORDER_UPDATE_FAILURE,
} from '../actions/PostStatus/updatePostStatusOrder';
import {
@@ -100,6 +101,12 @@ const postStatusesReducer = (
items: action.newOrder,
};
case POSTSTATUS_ORDER_UPDATE_FAILURE:
return {
...state,
items: action.oldOrder,
};
default:
return state;
}

View File

@@ -1,12 +1,14 @@
import { combineReducers } from 'redux';
import postsReducer from './postsReducer';
import boardsReducer from './boardsReducer';
import postStatusesReducer from './postStatusesReducer';
import currentPostReducer from './currentPostReducer';
import siteSettingsReducer from './siteSettingsReducer';
const rootReducer = combineReducers({
posts: postsReducer,
boards: boardsReducer,
postStatuses: postStatusesReducer,
currentPost: currentPostReducer,
siteSettings: siteSettingsReducer,

View File

@@ -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 {
PostStatusOrderUpdateActionTypes,
POSTSTATUS_ORDER_UPDATE_START,
@@ -26,24 +61,53 @@ import {
POSTSTATUS_UPDATE_FAILURE,
} from '../actions/PostStatus/updatePostStatus';
import siteSettingsBoardsReducer, { SiteSettingsBoardsState } from './SiteSettings/boardsReducer';
import siteSettingsPostStatusesReducer, { SiteSettingsPostStatusesState } from './SiteSettings/postStatusesReducer';
interface SiteSettingsState {
boards: SiteSettingsBoardsState;
postStatuses: SiteSettingsPostStatusesState;
}
const initialState: SiteSettingsState = {
boards: siteSettingsBoardsReducer(undefined, {} as any),
postStatuses: siteSettingsPostStatusesReducer(undefined, {} as any),
};
const siteSettingsReducer = (
state = initialState,
action: PostStatusOrderUpdateActionTypes |
action:
BoardsRequestActionTypes |
BoardSubmitActionTypes |
BoardUpdateActionTypes |
BoardOrderUpdateActionTypes |
BoardDeleteActionTypes |
PostStatusOrderUpdateActionTypes |
PostStatusDeleteActionTypes |
PostStatusSubmitActionTypes |
PostStatusUpdateActionTypes
): SiteSettingsState => {
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_SUCCESS:
case POSTSTATUS_ORDER_UPDATE_FAILURE:
@@ -57,7 +121,8 @@ const siteSettingsReducer = (
case POSTSTATUS_UPDATE_SUCCESS:
case POSTSTATUS_UPDATE_FAILURE:
return {
postStatuses: siteSettingsPostStatusesReducer(state.postStatuses, action)
...state,
postStatuses: siteSettingsPostStatusesReducer(state.postStatuses, action),
};
default:

View File

@@ -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;
}
}

View File

@@ -1,4 +1,4 @@
.postStatusList {
.postStatusesList {
@extend
.p-0,
.m-0;
@@ -28,5 +28,9 @@
.d-flex,
.m-2;
column-gap: 16px;
column-gap: 8px;
.postStatusColorInput {
flex: 0 1 100px;
}
}

View File

@@ -93,7 +93,7 @@ a {
.drag-zone {
@extend
.align-self-center,
.pl-4,
.pl-1,
.pr-4,
.pt-1,
.pb-1;

View File

@@ -17,6 +17,7 @@
/* Site Settings Components */
@import 'components/SiteSettings';
@import 'components/SiteSettings/Boards';
@import 'components/SiteSettings/PostStatuses';
/* Icons */

View File

@@ -1,16 +1,8 @@
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 :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

View 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

View File

@@ -1,12 +1,10 @@
class PostStatus < ApplicationRecord
has_many :posts, dependent: :nullify
include Orderable
after_initialize :set_random_color, :set_order_to_last
after_destroy :ensure_coherent_order
has_many :posts, dependent: :nullify
validates :name, presence: true, uniqueness: true
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
def find_roadmap
@@ -14,26 +12,4 @@ class PostStatus < ApplicationRecord
.order(order: :asc)
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

View File

@@ -4,6 +4,7 @@
<div class="verticalNavigation" role="tablist" aria-orientation="vertical">
<%= render 'menu_link', label: 'General', path: site_settings_general_path %>
<%= render 'menu_link', label: 'Appearance', path: '#' %>
<%= render 'menu_link', label: 'Boards', path: site_settings_boards_path %>
<%= render 'menu_link', label: 'Post statuses', path: site_settings_post_statuses_path %>
<%= render 'menu_link', label: 'Widgets', path: '#' %>
</div>

View File

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

View File

@@ -19,13 +19,18 @@ Rails.application.routes.draw do
resources :likes, only: [:index]
resources :comments, only: [:index, :create, :update]
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
patch 'update_order', on: :collection
end
namespace :site_settings do
get 'general'
get 'boards'
get 'post_statuses'
end
end

View File

@@ -8,14 +8,42 @@ admin = User.create(
)
# Create some boards
feature_board = Board.create(name: 'Feature Requests', description: 'Post here your feature requests.')
bug_board = Board.create(name: 'Bug Reports', description: 'Post here your bug reports.')
feature_board = Board.create(
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
planned_post_status = PostStatus.create(name: 'Planned', color: '#0096ff', 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)
planned_post_status = PostStatus.create(
name: 'Planned',
color: '#0096ff',
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
post1 = Post.create(

View File

@@ -2,6 +2,6 @@ FactoryBot.define do
factory :board do
sequence(:name) { |n| "Board#{n}" }
description { 'My fantastic board' }
order { 1 }
sequence(:order) { |n| n }
end
end

View File

@@ -30,12 +30,19 @@ RSpec.describe Board, type: :model do
expect(empty_description_board).to be_valid
end
it 'automatically sets order to last order if not specified' do
order = 10
board1 = FactoryBot.create(:board, order: order)
board2 = Board.new
it 'is Orderable' do
# I didn't used FactoryBot because it didn't apply
# the custom logic to the 'order' column
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(order + 1)
expect(board2.order).to eq(1)
expect(board3.order).to eq(2)
board1.destroy
expect(board2.reload.order).to eq(0)
end
end

View File

@@ -62,18 +62,19 @@ RSpec.describe PostStatus, type: :model do
expect(roadmap.second).to eq(post_status2)
end
it 'automatically sets a random color if not specified' do
post_status1 = PostStatus.new
it 'is Orderable' do
# 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/)
end
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
post_status1 = PostStatus.create(name: 'ps1', color: '#000000', order: 0)
post_status2 = PostStatus.create(name: 'ps2', color: '#000000')
post_status3 = PostStatus.new
expect(post_status1.order).to eq(order)
expect(post_status2.order).to eq(order + 1)
expect(post_status2.order).to eq(1)
expect(post_status3.order).to eq(2)
post_status1.destroy
expect(post_status2.reload.order).to eq(0)
end
end

View File

@@ -6,13 +6,26 @@ RSpec.describe 'boards routing', :aggregate_failures, type: :routing do
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(
controller: 'boards', action: 'new'
)
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(delete: '/boards/1').not_to be_routable
expect(post: '/boards').to route_to(
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

View File

@@ -9,7 +9,12 @@ Capybara.register_driver :chrome_headless do |app|
options.add_argument('--disable-dev-shm-usage')
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
Capybara.javascript_driver = :chrome_headless

View File

@@ -9,7 +9,6 @@ feature 'sign up', type: :system do
fill_in 'Email', with: user.email
fill_in 'Password', with: user.password
fill_in 'Password confirmation', with: user.password
check 'Notifications enabled'
click_button 'Sign up'
end
@@ -76,16 +75,4 @@ feature 'sign up', type: :system do
expect_to_be_on_sign_up_page
expect(page).to have_css('.alert')
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