Allow board delete via UI

ref #16
This commit is contained in:
wpetit 2019-12-13 13:28:59 +01:00
parent 1dedda7d50
commit 860ee438fc
12 changed files with 117 additions and 20 deletions

View File

@ -16,7 +16,6 @@ export class App extends React.Component {
<Route path="/boards/new" exact component={EditBoardPage} /> <Route path="/boards/new" exact component={EditBoardPage} />
<Route path="/boards/:id" exact component={BoardPage} /> <Route path="/boards/:id" exact component={BoardPage} />
<Route path="/boards/:id/edit" exact component={EditBoardPage} /> <Route path="/boards/:id/edit" exact component={EditBoardPage} />
<Route path="/boards/:id/delete" exact component={BoardPage} />
<Route path="/logout" exact component={() => { <Route path="/logout" exact component={() => {
window.location = "/logout"; window.location = "/logout";
return null; return null;

View File

@ -2,16 +2,17 @@ import React from 'react';
import { Page } from '../Page'; import { Page } from '../Page';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { selectFlagsIsLoading } from '../../store/selectors/flags'; import { selectFlagsIsLoading } from '../../store/selectors/flags';
import { fetchBoards, saveBoard } from '../../store/actions/boards'; import { fetchBoards, saveBoard, deleteBoard } from '../../store/actions/boards';
import { fetchProjects } from '../../store/actions/projects'; import { fetchProjects } from '../../store/actions/projects';
import uuidv4 from 'uuid/v4'; import uuidv4 from 'uuid/v4';
import { Loader } from '../Loader';
export class EditBoardPage extends React.Component { export class EditBoardPage extends React.Component {
state = { state = {
edited: false, edited: false,
board: { board: {
id: uuidv4(), id: "",
title: "", title: "",
description: "", description: "",
projects: [], projects: [],
@ -42,6 +43,7 @@ export class EditBoardPage extends React.Component {
this.onBoardDescriptionChange = this.onBoardAttrChange.bind(this, 'description'); this.onBoardDescriptionChange = this.onBoardAttrChange.bind(this, 'description');
this.onBoardLaneTitleChange = this.onBoardLaneAttrChange.bind(this, 'title'); this.onBoardLaneTitleChange = this.onBoardLaneAttrChange.bind(this, 'title');
this.onBoardLaneIssueLabelChange = this.onBoardLaneAttrChange.bind(this, 'issueLabel'); this.onBoardLaneIssueLabelChange = this.onBoardLaneAttrChange.bind(this, 'issueLabel');
this.onDeleteBoardClick = this.onDeleteBoardClick.bind(this);
} }
render() { render() {
@ -50,7 +52,9 @@ export class EditBoardPage extends React.Component {
if (isLoading) { if (isLoading) {
return ( return (
<p>Loading...</p> <Page>
<Loader></Loader>
</Page>
) )
}; };
@ -59,11 +63,27 @@ export class EditBoardPage extends React.Component {
<div className="container is-fluid has-margin-top-normal"> <div className="container is-fluid has-margin-top-normal">
<div className="columns"> <div className="columns">
<div className="column is-6 is-offset-3"> <div className="column is-6 is-offset-3">
<div className="level is-mobile">
<div className="level-left">
{ {
board.id ? board.id ?
<h3 className="is-size-3">Éditer le tableau</h3> : <h3 className="is-size-3 level-item">Éditer le tableau</h3> :
<h3 className="is-size-3">Nouveau tableau</h3> <h3 className="is-size-3 level-item">Nouveau tableau</h3>
} }
</div>
<div className="level-right">
{
board.id ?
<button onClick={this.onDeleteBoardClick} className="level-item button is-danger">
<span className="icon">
<i className="fas fa-trash"></i>
</span>
<span>Supprimer</span>
</button> :
null
}
</div>
</div>
<div className="field"> <div className="field">
<label className="label">Titre</label> <label className="label">Titre</label>
<div className="control"> <div className="control">
@ -366,8 +386,16 @@ export class EditBoardPage extends React.Component {
} }
onSaveBoardClick() { onSaveBoardClick() {
let { board } = this.state;
board = { ...board };
if (!board.id) board.id = uuidv4();
this.props.dispatch(saveBoard({...board}));
this.props.history.push('/');
}
onDeleteBoardClick() {
const { board } = this.state; const { board } = this.state;
this.props.dispatch(saveBoard(board)); this.props.dispatch(deleteBoard(board.id));
this.props.history.push('/'); this.props.history.push('/');
} }

View File

@ -26,11 +26,6 @@ export class BoardCard extends React.PureComponent {
<i className="fas fa-edit" aria-hidden="true"></i> <i className="fas fa-edit" aria-hidden="true"></i>
</span> </span>
</a> </a>
<a className="level-item" aria-label="delete" href={`#/boards/${board.id}/delete`}>
<span className="icon is-small has-text-danger">
<i className="fas fa-trash-alt" aria-hidden="true"></i>
</span>
</a>
</div> </div>
</div> </div>
</div> </div>

View File

@ -13,3 +13,11 @@ export const SAVE_BOARD_FAILURE = "SAVE_BOARD_FAILURE";
export function saveBoard(board) { export function saveBoard(board) {
return { type: SAVE_BOARD_REQUEST, board }; return { type: SAVE_BOARD_REQUEST, board };
}; };
export const DELETE_BOARD_REQUEST = "DELETE_BOARD_REQUEST";
export const DELETE_BOARD_SUCCESS = "DELETE_BOARD_SUCCESS";
export const DELETE_BOARD_FAILURE = "DELETE_BOARD_FAILURE";
export function deleteBoard(id) {
return { type: DELETE_BOARD_REQUEST, id };
};

View File

@ -16,12 +16,13 @@ export function boardsReducer(state = defaultState, action) {
} }
function handleSaveBoardSuccess(state, action) { function handleSaveBoardSuccess(state, action) {
const { board } = action;
return { return {
...state, ...state,
byID: { byID: {
...state.byID, ...state.byID,
[action.board.id.toString()]: { [board.id]: {
...action.board, ...board,
} }
} }
}; };

View File

@ -1,5 +1,5 @@
import { put, call } from 'redux-saga/effects'; import { put, call } from 'redux-saga/effects';
import { FETCH_BOARDS_SUCCESS, SAVE_BOARD_SUCCESS, SAVE_BOARD_FAILURE, FETCH_BOARDS_FAILURE } from '../actions/boards'; import { FETCH_BOARDS_SUCCESS, SAVE_BOARD_SUCCESS, SAVE_BOARD_FAILURE, FETCH_BOARDS_FAILURE, DELETE_BOARD_FAILURE, DELETE_BOARD_SUCCESS } from '../actions/boards';
import { api } from '../../util/api'; import { api } from '../../util/api';
export function* fetchBoardsSaga() { export function* fetchBoardsSaga() {
@ -27,3 +27,17 @@ export function* saveBoardSaga(action) {
yield put({ type: SAVE_BOARD_SUCCESS, board }); yield put({ type: SAVE_BOARD_SUCCESS, board });
} }
export function* deleteBoardSaga(action) {
let { id } = action;
try {
board = yield call(api.deleteBoard, id)
} catch(error) {
yield put({ type: DELETE_BOARD_FAILURE, error });
return
}
yield put({ type: DELETE_BOARD_SUCCESS, id });
}

View File

@ -1,7 +1,7 @@
import { all, takeEvery, takeLatest } from 'redux-saga/effects'; import { all, takeEvery, takeLatest } from 'redux-saga/effects';
import { failuresSaga } from './failure'; import { failuresSaga } from './failure';
import { FETCH_BOARDS_REQUEST, SAVE_BOARD_REQUEST } from '../actions/boards'; import { FETCH_BOARDS_REQUEST, SAVE_BOARD_REQUEST, DELETE_BOARD_REQUEST } from '../actions/boards';
import { fetchBoardsSaga, saveBoardSaga } from './boards'; import { fetchBoardsSaga, saveBoardSaga, deleteBoardSaga } from './boards';
import { FETCH_ISSUES_REQUEST, ADD_LABEL_REQUEST, REMOVE_LABEL_REQUEST, CREATE_ISSUE_REQUEST, CREATE_ISSUE_SUCCESS } from '../actions/issues'; import { FETCH_ISSUES_REQUEST, ADD_LABEL_REQUEST, REMOVE_LABEL_REQUEST, CREATE_ISSUE_REQUEST, CREATE_ISSUE_SUCCESS } from '../actions/issues';
import { fetchIssuesSaga, addLabelSaga, removeLabelSaga, createIssueSaga } from './issues'; import { fetchIssuesSaga, addLabelSaga, removeLabelSaga, createIssueSaga } from './issues';
import { FETCH_PROJECTS_REQUEST } from '../actions/projects'; import { FETCH_PROJECTS_REQUEST } from '../actions/projects';
@ -17,6 +17,7 @@ export function* rootSaga() {
takeLatest(FETCH_BOARDS_REQUEST, fetchBoardsSaga), takeLatest(FETCH_BOARDS_REQUEST, fetchBoardsSaga),
takeLatest(BUILD_KANBOARD_REQUEST, buildKanboardSaga), takeLatest(BUILD_KANBOARD_REQUEST, buildKanboardSaga),
takeLatest(SAVE_BOARD_REQUEST, saveBoardSaga), takeLatest(SAVE_BOARD_REQUEST, saveBoardSaga),
takeLatest(DELETE_BOARD_REQUEST, deleteBoardSaga),
takeLatest(FETCH_ISSUES_REQUEST, fetchIssuesSaga), takeLatest(FETCH_ISSUES_REQUEST, fetchIssuesSaga),
takeLatest(FETCH_PROJECTS_REQUEST, fetchProjectsSaga), takeLatest(FETCH_PROJECTS_REQUEST, fetchProjectsSaga),
takeEvery(MOVE_CARD, moveCardSaga), takeEvery(MOVE_CARD, moveCardSaga),

View File

@ -13,6 +13,12 @@ export class APIClient {
; ;
} }
deleteBoard(id) {
return fetch(`/api/boards/${id}`, {
method: 'DELETE'
});
}
fetchBoards() { fetchBoards() {
return fetch(`/api/boards`) return fetch(`/api/boards`)
.then(res => res.json()) .then(res => res.json())

View File

@ -1,5 +1,13 @@
package repository package repository
import (
"github.com/pkg/errors"
)
var (
ErrNotFound = errors.New("not found")
)
type Repository struct { type Repository struct {
boards BoardRepository boards BoardRepository
} }

View File

@ -61,6 +61,18 @@ func (r *BoardRepository) Save(board *repository.Board) error {
} }
func (r *BoardRepository) Delete(id repository.BoardID) error { func (r *BoardRepository) Delete(id repository.BoardID) error {
b := &boardItem{
ID: string(id),
}
if err := r.db.DeleteStruct(b); err != nil {
if err == storm.ErrNotFound {
return repository.ErrNotFound
}
return errors.Wrapf(err, "could not delete board '%s'", id)
}
return nil return nil
} }

View File

@ -4,6 +4,8 @@ import (
"encoding/json" "encoding/json"
"net/http" "net/http"
"github.com/go-chi/chi"
"forge.cadoles.com/wpetit/gitea-kan/internal/repository" "forge.cadoles.com/wpetit/gitea-kan/internal/repository"
"github.com/pkg/errors" "github.com/pkg/errors"
"gitlab.com/wpetit/goweb/middleware/container" "gitlab.com/wpetit/goweb/middleware/container"
@ -18,6 +20,8 @@ func serveBoards(w http.ResponseWriter, r *http.Request) {
panic(errors.Wrap(err, "could not retrieve boards list")) panic(errors.Wrap(err, "could not retrieve boards list"))
} }
w.Header().Add("Content-Type", "application/json")
encoder := json.NewEncoder(w) encoder := json.NewEncoder(w)
if err := encoder.Encode(boards); err != nil { if err := encoder.Encode(boards); err != nil {
panic(errors.Wrap(err, "could not encode boards list")) panic(errors.Wrap(err, "could not encode boards list"))
@ -39,8 +43,28 @@ func saveBoard(w http.ResponseWriter, r *http.Request) {
panic(errors.Wrap(err, "could not save board")) panic(errors.Wrap(err, "could not save board"))
} }
w.Header().Add("Content-Type", "application/json")
encoder := json.NewEncoder(w) encoder := json.NewEncoder(w)
if err := encoder.Encode(board); err != nil { if err := encoder.Encode(board); err != nil {
panic(errors.Wrap(err, "could not encode board")) panic(errors.Wrap(err, "could not encode board"))
} }
} }
func deleteBoard(w http.ResponseWriter, r *http.Request) {
boardID := repository.BoardID(chi.URLParam(r, "boardID"))
ctn := container.Must(r.Context())
repo := repository.Must(ctn)
if err := repo.Boards().Delete(boardID); err != nil {
if err == repository.ErrNotFound {
http.NotFound(w, r)
return
}
panic(err)
}
w.WriteHeader(http.StatusNoContent)
}

View File

@ -21,6 +21,7 @@ func Mount(r *chi.Mux, config *config.Config) {
r.Get("/logout", handleLogout) r.Get("/logout", handleLogout)
r.Get("/api/boards", serveBoards) r.Get("/api/boards", serveBoards)
r.Post("/api/boards", saveBoard) r.Post("/api/boards", saveBoard)
r.Delete("/api/boards/{boardID}", deleteBoard)
r.Handle("/gitea/api/*", http.StripPrefix("/gitea", http.HandlerFunc(proxyAPIRequest))) r.Handle("/gitea/api/*", http.StripPrefix("/gitea", http.HandlerFunc(proxyAPIRequest)))
r.Get("/*", static.Dir(config.HTTP.PublicDir, "", html5PushStateHandler)) r.Get("/*", static.Dir(config.HTTP.PublicDir, "", html5PushStateHandler))
}) })