Base générale d'UI
This commit is contained in:
@ -1,23 +1,29 @@
|
||||
import React from 'react';
|
||||
import { HashRouter as Router, Route, Redirect, Switch } from "react-router-dom";
|
||||
import { ConnectedHomePage as HomePage } from './HomePage/HomePage';
|
||||
import { ConnectedBoardPage as BoardPage } from './BoardPage/BoardPage';
|
||||
import { ConnectedEditBoardPage as EditBoardPage } from './BoardPage/EditBoardPage';
|
||||
import { store } from '../store/store';
|
||||
import { Provider } from 'react-redux';
|
||||
import { HomePage } from './HomePage/HomePage';
|
||||
import { BoardPage } from './BoardPage/BoardPage';
|
||||
|
||||
export class App extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<Provider store={store}>
|
||||
<React.Fragment>
|
||||
<Router>
|
||||
<Switch>
|
||||
<Route path="/" exact component={HomePage} />
|
||||
<Route path="/board/:id" exact component={BoardPage} />
|
||||
<Route component={() => <Redirect to="/" />} />
|
||||
</Switch>
|
||||
</Router>
|
||||
</React.Fragment>
|
||||
<Router>
|
||||
<Switch>
|
||||
<Route path="/" exact component={HomePage} />
|
||||
<Route path="/boards/new" exact component={EditBoardPage} />
|
||||
<Route path="/boards/:id" exact component={BoardPage} />
|
||||
<Route path="/boards/:id/edit" exact component={EditBoardPage} />
|
||||
<Route path="/boards/:id/delete" exact component={BoardPage} />
|
||||
<Route path="/logout" exact component={() => {
|
||||
window.location = "/logout";
|
||||
return null;
|
||||
}} />
|
||||
<Route component={() => <Redirect to="/" />} />
|
||||
</Switch>
|
||||
</Router>
|
||||
</Provider>
|
||||
);
|
||||
}
|
||||
|
@ -1,12 +1,103 @@
|
||||
import React from 'react';
|
||||
import { Page } from '../Page';
|
||||
import { connect } from 'react-redux';
|
||||
import Board from '@lourenci/react-kanban';
|
||||
import { fetchIssues } from '../../store/actions/issues';
|
||||
import { fetchBoards } from '../../store/actions/boards';
|
||||
import { buildKanboard, moveCard } from '../../store/actions/kanboards';
|
||||
|
||||
export class BoardPage extends React.Component {
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Page>
|
||||
|
||||
<div className="container is-fluid">
|
||||
<div className="kanboard-container is-fullheight">
|
||||
{this.renderBoard()}
|
||||
</div>
|
||||
</div>
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
renderBoard() {
|
||||
const { kanboard } = this.props;
|
||||
if (!kanboard) {
|
||||
return <p>Loading</p>
|
||||
}
|
||||
return (
|
||||
<Board disableLaneDrag={true}
|
||||
renderCard={this.renderCard}
|
||||
renderLaneHeader={this.renderLaneHeader}
|
||||
onCardDragEnd={this.onCardDragEnd.bind(this)}>
|
||||
{kanboard}
|
||||
</Board>
|
||||
);
|
||||
}
|
||||
|
||||
renderCard(card) {
|
||||
return (
|
||||
<div className="kanboard-card">
|
||||
<div className="box">
|
||||
<div className="media">
|
||||
<div className="media-content">
|
||||
<div className="content">
|
||||
<h5 className="is-size-5 is-marginless">{card.title}</h5>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderLaneHeader(lane) {
|
||||
return (
|
||||
<h3 className="kanboard-lane-title is-size-3">{lane.title}</h3>
|
||||
)
|
||||
}
|
||||
|
||||
onCardDragEnd(source, dest) {
|
||||
const { board } = this.props;
|
||||
this.props.dispatch(moveCard(
|
||||
board.id,
|
||||
source.fromLaneId,
|
||||
source.fromPosition,
|
||||
dest.toLaneId,
|
||||
dest.toPosition
|
||||
));
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const { board } = this.props;
|
||||
if (!board) {
|
||||
this.requestBoardsUpdate();
|
||||
return
|
||||
}
|
||||
|
||||
this.requestBuildKanboard();
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (prevProps.board !== this.props.board) this.requestBuildKanboard();
|
||||
}
|
||||
|
||||
requestBoardsUpdate() {
|
||||
this.props.dispatch(fetchBoards());
|
||||
}
|
||||
|
||||
requestBuildKanboard() {
|
||||
const { board } = this.props;
|
||||
if (!board) return;
|
||||
this.props.dispatch(buildKanboard(board));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export const ConnectedBoardPage = connect(function(state, props) {
|
||||
const boardID = props.match.params.id;
|
||||
return {
|
||||
board: state.boards.byID[boardID],
|
||||
kanboard: state.kanboards.byID[boardID]
|
||||
};
|
||||
})(BoardPage);
|
390
client/src/components/BoardPage/EditBoardPage.jsx
Normal file
390
client/src/components/BoardPage/EditBoardPage.jsx
Normal file
@ -0,0 +1,390 @@
|
||||
import React from 'react';
|
||||
import { Page } from '../Page';
|
||||
import { connect } from 'react-redux';
|
||||
import { selectFlagsIsLoading } from '../../store/selectors/flags';
|
||||
import { fetchBoards, saveBoard } from '../../store/actions/boards';
|
||||
import { fetchProjects } from '../../store/actions/projects';
|
||||
import uuidv4 from 'uuid/v4';
|
||||
|
||||
export class EditBoardPage extends React.Component {
|
||||
|
||||
state = {
|
||||
edited: false,
|
||||
board: {
|
||||
id: uuidv4(),
|
||||
title: "",
|
||||
description: "",
|
||||
projects: [],
|
||||
lanes: []
|
||||
},
|
||||
}
|
||||
|
||||
static getDerivedStateFromProps(props, state) {
|
||||
const { board, isLoading } = props;
|
||||
|
||||
if (isLoading || !board || state.edited) return state;
|
||||
|
||||
return {
|
||||
edited: false,
|
||||
board: {
|
||||
id: board.id,
|
||||
title: board.title,
|
||||
description: board.description,
|
||||
projects: [ ...board.projects ],
|
||||
lanes: [ ...board.lanes.map(l => ({ ...l })) ]
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.onBoardTitleChange = this.onBoardAttrChange.bind(this, 'title');
|
||||
this.onBoardDescriptionChange = this.onBoardAttrChange.bind(this, 'description');
|
||||
this.onBoardLaneTitleChange = this.onBoardLaneAttrChange.bind(this, 'title');
|
||||
this.onBoardLaneIssueLabelChange = this.onBoardLaneAttrChange.bind(this, 'issueLabel');
|
||||
}
|
||||
|
||||
render() {
|
||||
const { isLoading } = this.props;
|
||||
const { board } = this.state;
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<p>Loading...</p>
|
||||
)
|
||||
};
|
||||
|
||||
return (
|
||||
<Page>
|
||||
<div className="container is-fluid">
|
||||
<div className="columns">
|
||||
<div className="column is-6 is-offset-3">
|
||||
{
|
||||
board.id ?
|
||||
<h3 className="is-size-3">Éditer le tableau</h3> :
|
||||
<h3 className="is-size-3">Nouveau tableau</h3>
|
||||
}
|
||||
<div className="field">
|
||||
<label className="label">Titre</label>
|
||||
<div className="control">
|
||||
<input className="input" type="text"
|
||||
value={board.title}
|
||||
onChange={this.onBoardTitleChange} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="field">
|
||||
<label className="label">Description</label>
|
||||
<div className="control">
|
||||
<textarea className="textarea"
|
||||
value={board.description}
|
||||
onChange={this.onBoardDescriptionChange}>
|
||||
</textarea>
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
{ this.renderProjectSelect() }
|
||||
<hr />
|
||||
{ this.renderLanesSection() }
|
||||
<div className="field is-grouped is-grouped-right">
|
||||
<p className="control">
|
||||
<a className="button is-light is-normal" href="#/">
|
||||
Annuler
|
||||
</a>
|
||||
</p>
|
||||
<p className="control">
|
||||
<a className="button is-success is-normal"
|
||||
onClick={this.onSaveBoardClick.bind(this)}>
|
||||
Enregistrer
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
|
||||
renderProjectSelect() {
|
||||
const { projects } = this.props;
|
||||
const { board } = this.state;
|
||||
|
||||
const projectSelectField = (projectIndex, value, withDeleteAddon) => {
|
||||
return (
|
||||
<div key={`project-${projectIndex}`} className="field has-addons">
|
||||
<div className="control is-expanded">
|
||||
<div className="select is-fullwidth">
|
||||
<select value={value} onChange={this.onBoardProjectChange.bind(this, projectIndex)}>
|
||||
<option value></option>
|
||||
{
|
||||
projects.map(p => {
|
||||
return <option key={`project-${p}`} value={p}>{p}</option>;
|
||||
})
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
{
|
||||
withDeleteAddon ?
|
||||
<div className="control">
|
||||
<button type="submit" className="button is-danger"
|
||||
onClick={this.onBoardProjectDelete.bind(this, projectIndex)}>
|
||||
<span className="icon">
|
||||
<i className="fas fa-trash"></i>
|
||||
</span>
|
||||
</button>
|
||||
</div> :
|
||||
null
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<label className="label">Projets</label>
|
||||
{
|
||||
board.projects.map((p, i) => {
|
||||
return projectSelectField(i, p, true);
|
||||
})
|
||||
}
|
||||
{ projectSelectField(board.projects.length, '', false) }
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
|
||||
renderLanesSection() {
|
||||
|
||||
const { board } = this.state;
|
||||
|
||||
const laneSection = (laneIndex, lane) => {
|
||||
return (
|
||||
<React.Fragment key={`board-lane-${laneIndex}`}>
|
||||
<div className="columns">
|
||||
<div className="column is-10">
|
||||
<div className="field is-horizontal">
|
||||
<div className="field-label is-normal">
|
||||
<label className="label">Titre</label>
|
||||
</div>
|
||||
<div className="field-body">
|
||||
<div className="field">
|
||||
<div className="control">
|
||||
<input className="input" type="text"
|
||||
value={lane.title}
|
||||
onChange={this.onBoardLaneTitleChange.bind(this, laneIndex)} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="field is-horizontal">
|
||||
<div className="field-label is-normal">
|
||||
<label className="label">Label associé</label>
|
||||
</div>
|
||||
<div className="field-body">
|
||||
<div className="field">
|
||||
<div className="control">
|
||||
<input className="input" type="text"
|
||||
value={lane.issueLabel}
|
||||
onChange={this.onBoardLaneIssueLabelChange.bind(this, laneIndex)} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="column is-2 is-flex">
|
||||
<div className="buttons">
|
||||
<button className="button is-danger is-small"
|
||||
onClick={this.onBoardLaneDelete.bind(this, laneIndex)}>
|
||||
<span className="icon">
|
||||
<i className="fas fa-trash"></i>
|
||||
</span>
|
||||
</button>
|
||||
<button className="button is-primary is-small"
|
||||
onClick={this.onBoardLaneMove.bind(this, laneIndex, -1)}>
|
||||
<span className="icon">
|
||||
<i className="fas fa-chevron-up"></i>
|
||||
</span>
|
||||
</button>
|
||||
<button className="button is-primary is-small"
|
||||
onClick={this.onBoardLaneMove.bind(this, laneIndex, 1)}>
|
||||
<span className="icon">
|
||||
<i className="fas fa-chevron-down"></i>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
const lanes = board.lanes.map((l, i) => laneSection(i, l))
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<div className="level">
|
||||
<div className="level-left">
|
||||
<label className="label level-item">Voies</label>
|
||||
</div>
|
||||
<div className="level-right">
|
||||
<div className="field is-grouped is-grouped-right level-item">
|
||||
<p className="control">
|
||||
<a className="button is-primary is-small"
|
||||
onClick={this.onBoardLaneAdd.bind(this)}>
|
||||
Ajouter une voie
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{ lanes }
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
|
||||
onBoardLaneAdd() {
|
||||
this.setState(state => {
|
||||
const lanes = [
|
||||
...state.board.lanes,
|
||||
{ id: uuidv4(), title: "", issueLabel: "" }
|
||||
];
|
||||
return {
|
||||
...state,
|
||||
edited: true,
|
||||
board: {
|
||||
...state.board,
|
||||
lanes
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onBoardProjectDelete(projectIndex) {
|
||||
this.setState(state => {
|
||||
const projects = [ ...state.board.projects ]
|
||||
projects.splice(projectIndex, 1);
|
||||
return {
|
||||
...state,
|
||||
edited: true,
|
||||
board: {
|
||||
...state.board,
|
||||
projects
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onBoardLaneMove(laneIndex, direction) {
|
||||
this.setState(state => {
|
||||
const lanes = [ ...state.board.lanes ];
|
||||
|
||||
const nextLaneIndex = laneIndex+direction;
|
||||
if (nextLaneIndex < 0 || nextLaneIndex >= lanes.length) {
|
||||
return state;
|
||||
}
|
||||
|
||||
const l = lanes[laneIndex];
|
||||
lanes.splice(laneIndex, 1);
|
||||
lanes.splice(nextLaneIndex, 0, l);
|
||||
|
||||
return {
|
||||
...state,
|
||||
edited: true,
|
||||
board: {
|
||||
...state.board,
|
||||
lanes
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onBoardLaneDelete(laneIndex) {
|
||||
this.setState(state => {
|
||||
const lanes = [ ...state.board.lanes ]
|
||||
lanes.splice(laneIndex, 1);
|
||||
return {
|
||||
...state,
|
||||
edited: true,
|
||||
board: {
|
||||
...state.board,
|
||||
lanes
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onBoardProjectChange(projectIndex, evt) {
|
||||
const value = evt.target.value;
|
||||
this.setState(state => {
|
||||
const projects = [ ...state.board.projects ];
|
||||
projects[projectIndex] = value;
|
||||
return {
|
||||
...state,
|
||||
edited: true,
|
||||
board: {
|
||||
...state.board,
|
||||
projects
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onBoardAttrChange(attrName, evt) {
|
||||
const value = evt.target.value;
|
||||
this.setState(state => {
|
||||
return {
|
||||
...state,
|
||||
edited: true,
|
||||
board: {
|
||||
...state.board,
|
||||
[attrName]: value,
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onBoardLaneAttrChange(attrName, laneIndex, evt) {
|
||||
const value = evt.target.value;
|
||||
this.setState(state => {
|
||||
const lanes = [ ...state.board.lanes ];
|
||||
lanes[laneIndex] = {
|
||||
...state.board.lanes[laneIndex],
|
||||
[attrName]: value
|
||||
};
|
||||
return {
|
||||
...state,
|
||||
edited: true,
|
||||
board: {
|
||||
...state.board,
|
||||
lanes
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onSaveBoardClick() {
|
||||
const { board } = this.state;
|
||||
this.props.dispatch(saveBoard(board));
|
||||
this.props.history.push('/');
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.props.dispatch(fetchBoards());
|
||||
this.props.dispatch(fetchProjects());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export const ConnectedEditBoardPage = connect(function(state, props) {
|
||||
const boardID = props.match.params.id;
|
||||
const board = boardID ? state.boards.byID[boardID] : null;
|
||||
|
||||
const projects = Object.keys(state.projects.byName).sort();
|
||||
|
||||
const isLoading = selectFlagsIsLoading(state, 'FETCH_BOARDS', 'FETCH_PROJECTS');
|
||||
|
||||
return { board, isLoading, projects };
|
||||
})(EditBoardPage);
|
36
client/src/components/HomePage/BoardCard.jsx
Normal file
36
client/src/components/HomePage/BoardCard.jsx
Normal file
@ -0,0 +1,36 @@
|
||||
import React from 'react';
|
||||
|
||||
export class BoardCard extends React.PureComponent {
|
||||
render() {
|
||||
const { board } = this.props;
|
||||
return (
|
||||
<div className="box">
|
||||
<div className="media">
|
||||
<div className="media-content">
|
||||
<div className="content">
|
||||
<a href={`#/boards/${board.id}`}>
|
||||
<h3 className="is-size-3">{board.title}</h3>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="level is-mobile">
|
||||
<div className="level-left"></div>
|
||||
<div className="level-right">
|
||||
<a className="level-item" aria-label="edit" href={`#/boards/${board.id}/edit`}>
|
||||
<span className="icon is-small">
|
||||
<i className="fas fa-edit" aria-hidden="true"></i>
|
||||
</span>
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
@ -1,12 +1,76 @@
|
||||
import React from 'react';
|
||||
import { Page } from '../Page';
|
||||
import { BoardCard } from './BoardCard';
|
||||
import { connect } from 'react-redux';
|
||||
import { fetchBoards } from '../../store/actions/boards';
|
||||
|
||||
export class HomePage extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<Page>
|
||||
|
||||
<div className="container is-fluid">
|
||||
<div className="level">
|
||||
<div className="level-left"></div>
|
||||
<div className="level-right">
|
||||
<div className="buttons">
|
||||
<a className="button is-primary" href="#/boards/new">
|
||||
<span className="icon">
|
||||
<i className="fas fa-plus"></i>
|
||||
</span>
|
||||
<span>Nouveau tableau</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{ this.renderBoards() }
|
||||
</div>
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
renderBoards() {
|
||||
const { boards } = this.props;
|
||||
const rows = Object.values(boards)
|
||||
.reduce((boardRows, board, index) => {
|
||||
if (index % 3 === 0) {
|
||||
boardRows.push([]);
|
||||
}
|
||||
boardRows[boardRows.length-1].push(board);
|
||||
return boardRows;
|
||||
}, [])
|
||||
.map((row, rowIndex) => {
|
||||
const tiles = row.map((board) => {
|
||||
return (
|
||||
<div key={`board-${board.id}`} className={`tile is-parent is-4`}>
|
||||
<div className="tile is-child">
|
||||
<BoardCard board={board} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
return (
|
||||
<div key={`boards-row-${rowIndex}`} className="tile">
|
||||
{tiles}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
;
|
||||
|
||||
return (
|
||||
<div className="tile is-ancestor is-vertical">
|
||||
{ rows }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.props.dispatch(fetchBoards());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export const ConnectedHomePage = connect(function(state) {
|
||||
return {
|
||||
boards: state.boards.byID,
|
||||
};
|
||||
})(HomePage);
|
34
client/src/components/Navbar.jsx
Normal file
34
client/src/components/Navbar.jsx
Normal file
@ -0,0 +1,34 @@
|
||||
import React from 'react';
|
||||
|
||||
export class Navbar extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<nav className="navbar" role="navigation" aria-label="main navigation">
|
||||
<div className="container is-fluid">
|
||||
<div className="navbar-brand">
|
||||
<a className="navbar-item" href="#/">
|
||||
<h1 className="is-size-4">GiteaKan</h1>
|
||||
</a>
|
||||
<a role="button" className="navbar-burger" aria-label="menu" aria-expanded="false">
|
||||
<span aria-hidden="true"></span>
|
||||
<span aria-hidden="true"></span>
|
||||
<span aria-hidden="true"></span>
|
||||
</a>
|
||||
</div>
|
||||
<div className="navbar-menu">
|
||||
<div className="navbar-end">
|
||||
<div className="navbar-item">
|
||||
<a className="button is-small" href="#/logout">
|
||||
<span className="icon">
|
||||
<i className="fas fa-sign-out-alt"></i>
|
||||
</span>
|
||||
<span>Se déconnecter</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
}
|
@ -1,11 +1,13 @@
|
||||
import React from 'react';
|
||||
import { Navbar } from './Navbar';
|
||||
|
||||
export class Page extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
|
||||
</div>
|
||||
<React.Fragment>
|
||||
<Navbar />
|
||||
{this.props.children}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
}
|
@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||
<title>Gitea Kan</title>
|
||||
<title>GiteaKan</title>
|
||||
<link rel="stylesheet" href="css/main.css">
|
||||
</head>
|
||||
<body>
|
||||
|
@ -4,6 +4,11 @@ import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { App } from './components/App';
|
||||
|
||||
import '@fortawesome/fontawesome-free/js/fontawesome'
|
||||
import '@fortawesome/fontawesome-free/js/solid'
|
||||
import '@fortawesome/fontawesome-free/js/regular'
|
||||
import '@fortawesome/fontawesome-free/js/brands'
|
||||
|
||||
ReactDOM.render(
|
||||
<App />,
|
||||
document.getElementById('app')
|
||||
|
@ -1 +1,3 @@
|
||||
@import 'bulma/bulma.sass';
|
||||
@import 'bulma/bulma.sass';
|
||||
@import '_base.scss';
|
||||
@import '_kanboard.scss';
|
12
client/src/sass/_base.scss
Normal file
12
client/src/sass/_base.scss
Normal file
@ -0,0 +1,12 @@
|
||||
html, body {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.is-fullheight {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#app {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
40
client/src/sass/_kanboard.scss
Normal file
40
client/src/sass/_kanboard.scss
Normal file
@ -0,0 +1,40 @@
|
||||
|
||||
.kanboard-container {
|
||||
& > div {
|
||||
padding: 0;
|
||||
height: 100%;
|
||||
|
||||
& > div {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
|
||||
// Lanes
|
||||
& > div {
|
||||
&:first-child {
|
||||
margin-left: 0;
|
||||
}
|
||||
flex-grow: 1;
|
||||
flex-basis: 100%;
|
||||
background-color: transparent;
|
||||
|
||||
// Card container
|
||||
& > div > div > div {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[data-react-beautiful-dnd-droppable] {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.kanboard-card {
|
||||
margin-bottom: $size-small;
|
||||
}
|
||||
|
||||
.kanboard-lane-title {
|
||||
margin-bottom: $size-small;
|
||||
}
|
||||
}
|
15
client/src/store/actions/boards.js
Normal file
15
client/src/store/actions/boards.js
Normal file
@ -0,0 +1,15 @@
|
||||
export const FETCH_BOARDS_REQUEST = "FETCH_BOARDS_REQUEST";
|
||||
export const FETCH_BOARDS_SUCCESS = "FETCH_BOARDS_SUCCESS";
|
||||
export const FETCH_BOARDS_FAILURE = "FETCH_BOARDS_FAILURE";
|
||||
|
||||
export function fetchBoards() {
|
||||
return { type: FETCH_BOARDS_REQUEST };
|
||||
};
|
||||
|
||||
export const SAVE_BOARD_REQUEST = "SAVE_BOARD_REQUEST";
|
||||
export const SAVE_BOARD_SUCCESS = "SAVE_BOARD_SUCCESS";
|
||||
export const SAVE_BOARD_FAILURE = "SAVE_BOARD_FAILURE";
|
||||
|
||||
export function saveBoard(board) {
|
||||
return { type: SAVE_BOARD_REQUEST, board };
|
||||
};
|
23
client/src/store/actions/issues.js
Normal file
23
client/src/store/actions/issues.js
Normal file
@ -0,0 +1,23 @@
|
||||
export const FETCH_ISSUES_REQUEST = "FETCH_ISSUES_REQUEST";
|
||||
export const FETCH_ISSUES_SUCCESS = "FETCH_ISSUES_SUCCESS";
|
||||
export const FETCH_ISSUES_FAILURE = "FETCH_ISSUES_FAILURE";
|
||||
|
||||
export function fetchIssues(project) {
|
||||
return { type: FETCH_ISSUES_REQUEST, project };
|
||||
};
|
||||
|
||||
export const ADD_LABEL_REQUEST = "ADD_LABEL_REQUEST";
|
||||
export const ADD_LABEL_SUCCESS = "ADD_LABEL_SUCCESS";
|
||||
export const ADD_LABEL_FAILURE = "ADD_LABEL_FAILURE";
|
||||
|
||||
export function addLabel(project, issueNumber, label) {
|
||||
return { type: ADD_LABEL_REQUEST, project, issueNumber, label };
|
||||
}
|
||||
|
||||
export const REMOVE_LABEL_REQUEST = "REMOVE_LABEL_REQUEST";
|
||||
export const REMOVE_LABEL_SUCCESS = "REMOVE_LABEL_SUCCESS";
|
||||
export const REMOVE_LABEL_FAILURE = "REMOVE_LABEL_FAILURE";
|
||||
|
||||
export function removeLabel(project, issueNumber, label) {
|
||||
return { type: REMOVE_LABEL_REQUEST, project, issueNumber, label };
|
||||
}
|
13
client/src/store/actions/kanboards.js
Normal file
13
client/src/store/actions/kanboards.js
Normal file
@ -0,0 +1,13 @@
|
||||
export const BUILD_KANBOARD_REQUEST = "BUILD_KANBOARD_REQUEST";
|
||||
export const BUILD_KANBOARD_SUCCESS = "BUILD_KANBOARD_SUCCESS";
|
||||
export const BUILD_KANBOARD_FAILURE = "BUILD_KANBOARD_FAILURE";
|
||||
|
||||
export function buildKanboard(board) {
|
||||
return { type: BUILD_KANBOARD_REQUEST, board };
|
||||
};
|
||||
|
||||
export const MOVE_CARD = "MOVE_CARD";
|
||||
|
||||
export function moveCard(boardID, fromLaneID, fromPosition, toLaneID, toPosition) {
|
||||
return { type: MOVE_CARD, boardID, fromLaneID, fromPosition, toLaneID, toPosition };
|
||||
};
|
5
client/src/store/actions/logout.js
Normal file
5
client/src/store/actions/logout.js
Normal file
@ -0,0 +1,5 @@
|
||||
export const LOGOUT = "LOGOUT";
|
||||
|
||||
export function logout() {
|
||||
return { type: LOGOUT };
|
||||
};
|
7
client/src/store/actions/projects.js
Normal file
7
client/src/store/actions/projects.js
Normal file
@ -0,0 +1,7 @@
|
||||
export const FETCH_PROJECTS_REQUEST = "FETCH_PROJECTS_REQUEST";
|
||||
export const FETCH_PROJECTS_SUCCESS = "FETCH_PROJECTS_SUCCESS";
|
||||
export const FETCH_PROJECTS_FAILURE = "FETCH_PROJECTS_FAILURE";
|
||||
|
||||
export function fetchProjects() {
|
||||
return { type: FETCH_PROJECTS_REQUEST };
|
||||
};
|
41
client/src/store/reducers/boards.js
Normal file
41
client/src/store/reducers/boards.js
Normal file
@ -0,0 +1,41 @@
|
||||
import { SAVE_BOARD_SUCCESS, FETCH_BOARDS_SUCCESS } from "../actions/boards";
|
||||
|
||||
export const defaultState = {
|
||||
byID: {},
|
||||
};
|
||||
|
||||
export function boardsReducer(state = defaultState, action) {
|
||||
switch(action.type) {
|
||||
case SAVE_BOARD_SUCCESS:
|
||||
return handleSaveBoardSuccess(state, action);
|
||||
case FETCH_BOARDS_SUCCESS:
|
||||
return handleFetchBoardsSuccess(state, action);
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
function handleSaveBoardSuccess(state, action) {
|
||||
return {
|
||||
...state,
|
||||
byID: {
|
||||
...state.byID,
|
||||
[action.board.id.toString()]: {
|
||||
...action.board,
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function handleFetchBoardsSuccess(state, action) {
|
||||
const boardsByID = action.boards.reduce((byID, board) => {
|
||||
byID[board.id] = board;
|
||||
return byID;
|
||||
}, {});
|
||||
return {
|
||||
...state,
|
||||
byID: {
|
||||
...boardsByID,
|
||||
}
|
||||
};
|
||||
}
|
22
client/src/store/reducers/flags.js
Normal file
22
client/src/store/reducers/flags.js
Normal file
@ -0,0 +1,22 @@
|
||||
const defaultState = {
|
||||
actions: {}
|
||||
};
|
||||
|
||||
export function flagsReducer(state = defaultState, action) {
|
||||
const matches = (/^(.*)_((SUCCESS)|(FAILURE)|(REQUEST))$/).exec(action.type);
|
||||
|
||||
if(!matches) return state;
|
||||
|
||||
const actionPrefix = matches[1];
|
||||
|
||||
return {
|
||||
...state,
|
||||
actions: {
|
||||
...state.actions,
|
||||
[actionPrefix]: {
|
||||
isLoading: matches[2] === 'REQUEST'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
}
|
@ -1,5 +1,27 @@
|
||||
import { FETCH_ISSUES_SUCCESS } from "../actions/issues";
|
||||
|
||||
export function issuesReducer(state = {}, action) {
|
||||
const defaultState = {
|
||||
byProject: {}
|
||||
};
|
||||
|
||||
return state;
|
||||
export function issuesReducer(state = defaultState, action) {
|
||||
switch(action.type) {
|
||||
case FETCH_ISSUES_SUCCESS:
|
||||
return handleFetchIssuesSuccess(state, action);
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function handleFetchIssuesSuccess(state, action) {
|
||||
return {
|
||||
...state,
|
||||
byProject: {
|
||||
...state.byProject,
|
||||
[action.project]: [
|
||||
...action.issues,
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
74
client/src/store/reducers/kanboards.js
Normal file
74
client/src/store/reducers/kanboards.js
Normal file
@ -0,0 +1,74 @@
|
||||
import { BUILD_KANBOARD_SUCCESS, MOVE_CARD } from "../actions/kanboards";
|
||||
|
||||
export const defaultState = {
|
||||
byID: {},
|
||||
};
|
||||
|
||||
export function kanboardsReducer(state = defaultState, action) {
|
||||
switch(action.type) {
|
||||
case BUILD_KANBOARD_SUCCESS:
|
||||
return handleBuildKanboardSuccess(state, action);
|
||||
case MOVE_CARD:
|
||||
return handleMoveCard(state, action);
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
function handleBuildKanboardSuccess(state, action) {
|
||||
return {
|
||||
...state,
|
||||
byID: {
|
||||
...state.byID,
|
||||
[action.kanboard.id]: {
|
||||
...action.kanboard,
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function handleMoveCard(state, action) {
|
||||
const {
|
||||
boardID, fromLaneID,
|
||||
fromPosition, toLaneID,
|
||||
toPosition
|
||||
} = action;
|
||||
|
||||
const kanboard = state.byID[boardID];
|
||||
|
||||
const lanes = [ ...kanboard.lanes ];
|
||||
const fromLane = lanes[fromLaneID];
|
||||
const toLane = lanes[toLaneID];
|
||||
const card = fromLane.cards[fromPosition];
|
||||
|
||||
const fromCards = [ ...fromLane.cards ];
|
||||
if (fromLaneID !== toLaneID) {
|
||||
fromCards.splice(fromPosition, 1);
|
||||
lanes[fromLaneID] = {
|
||||
...fromLane,
|
||||
cards: fromCards,
|
||||
};
|
||||
|
||||
const toCards = [ ...toLane.cards ];
|
||||
toCards.splice(toPosition, 0, card);
|
||||
lanes[toLaneID] = {
|
||||
...toLane,
|
||||
cards: toCards,
|
||||
};
|
||||
} else {
|
||||
fromCards.splice(fromPosition, 1);
|
||||
fromCards.splice(toPosition, 0, card);
|
||||
console.log(fromCards)
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
byID: {
|
||||
...state.byID,
|
||||
[boardID]: {
|
||||
...state.byID[boardID],
|
||||
lanes,
|
||||
},
|
||||
}
|
||||
};
|
||||
}
|
27
client/src/store/reducers/projects.js
Normal file
27
client/src/store/reducers/projects.js
Normal file
@ -0,0 +1,27 @@
|
||||
import { FETCH_PROJECTS_SUCCESS } from "../actions/projects";
|
||||
|
||||
export const defaultState = {
|
||||
byName: {},
|
||||
};
|
||||
|
||||
export function projectsReducer(state = defaultState, action) {
|
||||
switch(action.type) {
|
||||
case FETCH_PROJECTS_SUCCESS:
|
||||
return handleFetchProjectsSuccess(state, action);
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
function handleFetchProjectsSuccess(state, action) {
|
||||
const projectsByName = action.projects.reduce((byName, project) => {
|
||||
byName[project.full_name] = project;
|
||||
return byName;
|
||||
}, {});
|
||||
return {
|
||||
...state,
|
||||
byName: {
|
||||
...projectsByName,
|
||||
}
|
||||
};
|
||||
}
|
@ -1,6 +1,14 @@
|
||||
import { combineReducers } from 'redux';
|
||||
import { issuesReducer } from './issues';
|
||||
import { boardsReducer } from './boards';
|
||||
import { flagsReducer } from './flags';
|
||||
import { projectsReducer } from './projects';
|
||||
import { kanboardsReducer } from './kanboards';
|
||||
|
||||
export const rootReducer = combineReducers({
|
||||
issues: issuesReducer,
|
||||
boards: boardsReducer,
|
||||
kanboards: kanboardsReducer,
|
||||
flags: flagsReducer,
|
||||
projects: projectsReducer
|
||||
});
|
31
client/src/store/sagas/boards.js
Normal file
31
client/src/store/sagas/boards.js
Normal file
@ -0,0 +1,31 @@
|
||||
import { put, call } from 'redux-saga/effects';
|
||||
import { FETCH_BOARDS_SUCCESS, SAVE_BOARD_SUCCESS, SAVE_BOARD_FAILURE, FETCH_BOARDS_FAILURE } from '../actions/boards';
|
||||
import { api } from '../../util/api';
|
||||
|
||||
const boardsLocalStorageKey = 'giteakan.boards';
|
||||
|
||||
export function* fetchBoardsSaga() {
|
||||
let boards;
|
||||
|
||||
try {
|
||||
boards = yield call(api.fetchBoards)
|
||||
} catch(error) {
|
||||
yield put({ type: FETCH_BOARDS_FAILURE, error });
|
||||
return
|
||||
}
|
||||
|
||||
yield put({ type: FETCH_BOARDS_SUCCESS, boards });
|
||||
}
|
||||
|
||||
export function* saveBoardSaga(action) {
|
||||
let { board } = action;
|
||||
|
||||
try {
|
||||
board = yield call(api.saveBoard, board)
|
||||
} catch(error) {
|
||||
yield put({ type: SAVE_BOARD_FAILURE, error });
|
||||
return
|
||||
}
|
||||
|
||||
yield put({ type: SAVE_BOARD_SUCCESS, board });
|
||||
}
|
@ -1,4 +1,10 @@
|
||||
import { GiteaUnauthorizedError } from "../../util/gitea";
|
||||
import { LOGOUT } from "../actions/logout";
|
||||
import { put } from 'redux-saga/effects';
|
||||
|
||||
export function* handleFailedActionSaga(action) {
|
||||
console.error(action.error);
|
||||
export function* failuresSaga(action) {
|
||||
const err = action.error;
|
||||
if (err instanceof GiteaUnauthorizedError) {
|
||||
yield put({ type: LOGOUT });
|
||||
}
|
||||
}
|
||||
|
59
client/src/store/sagas/issues.js
Normal file
59
client/src/store/sagas/issues.js
Normal file
@ -0,0 +1,59 @@
|
||||
import { put, call, retry } from 'redux-saga/effects';
|
||||
import { FETCH_ISSUES_SUCCESS, FETCH_ISSUES_FAILURE, ADD_LABEL_FAILURE, ADD_LABEL_SUCCESS, REMOVE_LABEL_FAILURE, REMOVE_LABEL_SUCCESS } from '../actions/issues';
|
||||
import { gitea } from '../../util/gitea';
|
||||
|
||||
export function* fetchIssuesSaga(action) {
|
||||
const { project } = action;
|
||||
|
||||
let issues;
|
||||
try {
|
||||
issues = yield call(gitea.fetchIssues.bind(gitea), action.project);
|
||||
} catch(error) {
|
||||
yield put({ type: FETCH_ISSUES_FAILURE, project, error });
|
||||
return;
|
||||
}
|
||||
|
||||
yield put({ type: FETCH_ISSUES_SUCCESS, project, issues });
|
||||
}
|
||||
|
||||
export function* addLabelSaga(action) {
|
||||
const { project, issueNumber, label } = action;
|
||||
const labels = yield call(gitea.fetchProjectLabels.bind(gitea), project);
|
||||
const giteaLabel = labels.find(l => l.name === label)
|
||||
|
||||
if (!giteaLabel) {
|
||||
yield put({ type: ADD_LABEL_FAILURE, error: new Error(`Label "${label}" not found !`) });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
yield retry(5, 250, gitea.addIssueLabel.bind(gitea), project, issueNumber, giteaLabel.id);
|
||||
} catch(error) {
|
||||
yield put({ type: ADD_LABEL_FAILURE, error });
|
||||
return;
|
||||
}
|
||||
|
||||
yield put({ type: ADD_LABEL_SUCCESS, project, issueNumber, label });
|
||||
}
|
||||
|
||||
export function* removeLabelSaga(action) {
|
||||
const { project, issueNumber, label } = action;
|
||||
const labels = yield call(gitea.fetchProjectLabels.bind(gitea), project);
|
||||
const giteaLabel = labels.find(l => l.name === label)
|
||||
|
||||
if (!giteaLabel) {
|
||||
yield put({ type: REMOVE_LABEL_FAILURE, error: new Error(`Label "${label}" not found !`) });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
yield retry(5, 250, gitea.removeIssueLabel.bind(gitea), project, issueNumber, giteaLabel.id);
|
||||
} catch(error) {
|
||||
yield put({ type: REMOVE_LABEL_FAILURE, error });
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
yield put({ type: REMOVE_LABEL_SUCCESS, project, issueNumber, label });
|
||||
|
||||
}
|
98
client/src/store/sagas/kanboards.js
Normal file
98
client/src/store/sagas/kanboards.js
Normal file
@ -0,0 +1,98 @@
|
||||
import { select, put } from 'redux-saga/effects';
|
||||
import { fetchIssues, addLabel, removeLabel } from '../actions/issues';
|
||||
import { fetchIssuesSaga } from './issues';
|
||||
import { BUILD_KANBOARD_SUCCESS } from '../actions/kanboards';
|
||||
|
||||
export function* moveCardSaga(action) {
|
||||
const {
|
||||
boardID, fromLaneID,
|
||||
fromPosition, toLaneID,
|
||||
toPosition,
|
||||
} = action;
|
||||
|
||||
const { board, kanboard} = yield select(state => {
|
||||
return {
|
||||
kanboard: state.kanboards.byID[boardID],
|
||||
board: state.boards.byID[boardID]
|
||||
}
|
||||
});
|
||||
|
||||
const toLane = kanboard.lanes[toLaneID];
|
||||
const card = toLane.cards[toPosition];
|
||||
|
||||
if (!card) return;
|
||||
|
||||
yield put(addLabel(card.project, card.number, board.lanes[toLaneID].issueLabel));
|
||||
yield put(removeLabel(card.project, card.number, board.lanes[fromLaneID].issueLabel));
|
||||
|
||||
}
|
||||
|
||||
export function* buildKanboardSaga(action) {
|
||||
|
||||
const { board } = action;
|
||||
|
||||
let kanboard;
|
||||
try {
|
||||
|
||||
for (let p, i = 0; (p = board.projects[i]); i++) {
|
||||
yield* fetchIssuesSaga(fetchIssues(p));
|
||||
}
|
||||
|
||||
const issues = yield select(state => state.issues);
|
||||
|
||||
kanboard = createKanboard(board, issues);
|
||||
|
||||
} catch(error) {
|
||||
yield put({ type: BUILD_KANBOARD_FAILURE, error });
|
||||
return
|
||||
}
|
||||
|
||||
yield put({ type: BUILD_KANBOARD_SUCCESS, kanboard });
|
||||
|
||||
}
|
||||
|
||||
function createCards(projects, issues, lane) {
|
||||
return projects.reduce((laneCards, p) => {
|
||||
|
||||
const projectIssues = p in issues.byProject ? issues.byProject[p] : [];
|
||||
|
||||
return projectIssues.reduce((projectCards, issue) => {
|
||||
const hasLabel = issue.labels.some(l => l.name === lane.issueLabel);
|
||||
|
||||
if (hasLabel) {
|
||||
projectCards.push({
|
||||
id: issue.id,
|
||||
title: `#${issue.number} - ${issue.title}`,
|
||||
description: "",
|
||||
project: p,
|
||||
labels: issue.labels,
|
||||
assignee: issue.assignee,
|
||||
number: issue.number
|
||||
});
|
||||
}
|
||||
|
||||
return projectCards;
|
||||
|
||||
}, laneCards);
|
||||
|
||||
}, []);
|
||||
}
|
||||
|
||||
function createLane(projects, issues, lane, index) {
|
||||
return {
|
||||
id: index,
|
||||
title: lane.title,
|
||||
cards: createCards(projects, issues, lane)
|
||||
}
|
||||
}
|
||||
|
||||
function createKanboard(board, issues) {
|
||||
if (!board) return null;
|
||||
|
||||
const kanboard = {
|
||||
id: board.id,
|
||||
lanes: board.lanes.map(createLane.bind(null, board.projects, issues)),
|
||||
};
|
||||
|
||||
return kanboard;
|
||||
}
|
3
client/src/store/sagas/logout.js
Normal file
3
client/src/store/sagas/logout.js
Normal file
@ -0,0 +1,3 @@
|
||||
export function* logoutSaga() {
|
||||
window.location = '/logout';
|
||||
}
|
16
client/src/store/sagas/projects.js
Normal file
16
client/src/store/sagas/projects.js
Normal file
@ -0,0 +1,16 @@
|
||||
import { put, call } from 'redux-saga/effects';
|
||||
import { FETCH_PROJECTS_SUCCESS, FETCH_PROJECTS_FAILURE } from '../actions/projects';
|
||||
import { gitea } from '../../util/gitea';
|
||||
|
||||
export function* fetchProjectsSaga() {
|
||||
|
||||
let projects;
|
||||
try {
|
||||
projects = yield call(gitea.fetchUserProjects.bind(gitea))
|
||||
} catch(error) {
|
||||
yield put({ type: FETCH_PROJECTS_FAILURE, error });
|
||||
return;
|
||||
}
|
||||
|
||||
yield put({ type: FETCH_PROJECTS_SUCCESS, projects });
|
||||
}
|
@ -1,9 +1,28 @@
|
||||
import { all, takeEvery } from 'redux-saga/effects';
|
||||
import { handleFailedActionSaga } from './failure';
|
||||
import { all, takeEvery, takeLatest } from 'redux-saga/effects';
|
||||
import { failuresSaga } from './failure';
|
||||
import { FETCH_BOARDS_REQUEST, SAVE_BOARD_REQUEST } from '../actions/boards';
|
||||
import { fetchBoardsSaga, saveBoardSaga } from './boards';
|
||||
import { FETCH_ISSUES_REQUEST, ADD_LABEL_REQUEST, REMOVE_LABEL_REQUEST } from '../actions/issues';
|
||||
import { fetchIssuesSaga, addLabelSaga, removeLabelSaga } from './issues';
|
||||
import { FETCH_PROJECTS_REQUEST } from '../actions/projects';
|
||||
import { fetchProjectsSaga } from './projects';
|
||||
import { LOGOUT } from '../actions/logout';
|
||||
import { logoutSaga } from './logout';
|
||||
import { BUILD_KANBOARD_REQUEST, MOVE_CARD } from '../actions/kanboards';
|
||||
import { buildKanboardSaga, moveCardSaga } from './kanboards';
|
||||
|
||||
export function* rootSaga() {
|
||||
yield all([
|
||||
takeEvery(patternFromRegExp(/^.*_FAILURE/), handleFailedActionSaga),
|
||||
takeEvery(patternFromRegExp(/^.*_FAILURE/), failuresSaga),
|
||||
takeLatest(FETCH_BOARDS_REQUEST, fetchBoardsSaga),
|
||||
takeLatest(BUILD_KANBOARD_REQUEST, buildKanboardSaga),
|
||||
takeLatest(SAVE_BOARD_REQUEST, saveBoardSaga),
|
||||
takeLatest(FETCH_ISSUES_REQUEST, fetchIssuesSaga),
|
||||
takeLatest(FETCH_PROJECTS_REQUEST, fetchProjectsSaga),
|
||||
takeEvery(MOVE_CARD, moveCardSaga),
|
||||
takeEvery(ADD_LABEL_REQUEST, addLabelSaga),
|
||||
takeEvery(REMOVE_LABEL_REQUEST, removeLabelSaga),
|
||||
takeLatest(LOGOUT, logoutSaga)
|
||||
]);
|
||||
}
|
||||
|
||||
|
7
client/src/store/selectors/flags.js
Normal file
7
client/src/store/selectors/flags.js
Normal file
@ -0,0 +1,7 @@
|
||||
export function selectFlagsIsLoading(state, ...actionPrefixes) {
|
||||
const { actions } = state.flags;
|
||||
return actionPrefixes.reduce((isLoading, prefix) => {
|
||||
if (!(prefix in actions)) return isLoading;
|
||||
return isLoading || actions[prefix].isLoading;
|
||||
}, false);
|
||||
};
|
24
client/src/util/api.js
Normal file
24
client/src/util/api.js
Normal file
@ -0,0 +1,24 @@
|
||||
|
||||
export class APIClient {
|
||||
|
||||
saveBoard(board) {
|
||||
return fetch(`/api/boards`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(board)
|
||||
})
|
||||
.then(res => res.json())
|
||||
;
|
||||
}
|
||||
|
||||
fetchBoards() {
|
||||
return fetch(`/api/boards`)
|
||||
.then(res => res.json())
|
||||
;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export const api = new APIClient();
|
@ -1,7 +1,65 @@
|
||||
|
||||
export class GiteaUnauthorizedError extends Error {
|
||||
constructor(...args) {
|
||||
super(...args)
|
||||
Error.captureStackTrace(this, GiteaUnauthorizedError)
|
||||
}
|
||||
}
|
||||
|
||||
export class GiteaClient {
|
||||
|
||||
constructor() {
|
||||
|
||||
fetchIssues(project) {
|
||||
return fetch(`/gitea/api/v1/repos/${project}/issues`)
|
||||
.then(this.assertAuthorization)
|
||||
.then(res => res.json())
|
||||
;
|
||||
}
|
||||
|
||||
fetchUserProjects() {
|
||||
return fetch(`/gitea/api/v1/user/repos`)
|
||||
.then(this.assertOk)
|
||||
.then(this.assertAuthorization)
|
||||
.then(res => res.json())
|
||||
;
|
||||
}
|
||||
|
||||
addIssueLabel(project, issueNumber, labelID) {
|
||||
return fetch(`/gitea/api/v1/repos/${project}/issues/${issueNumber}/labels`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ labels: [labelID] }),
|
||||
})
|
||||
.then(this.assertOk)
|
||||
.then(this.assertAuthorization)
|
||||
.then(res => res.json())
|
||||
}
|
||||
|
||||
fetchProjectLabels(project) {
|
||||
return fetch(`/gitea/api/v1/repos/${project}/labels`)
|
||||
.then(this.assertOk)
|
||||
.then(this.assertAuthorization)
|
||||
.then(res => res.json())
|
||||
;
|
||||
}
|
||||
|
||||
removeIssueLabel(project, issueNumber, labelID) {
|
||||
return fetch(`/gitea/api/v1/repos/${project}/issues/${issueNumber}/labels/${labelID}`, {
|
||||
method: 'DELETE'
|
||||
})
|
||||
.then(this.assertOk)
|
||||
.then(this.assertAuthorization)
|
||||
}
|
||||
|
||||
assertOk(res) {
|
||||
if (!res.ok) return Promise.reject(new Error('Request failed'));
|
||||
return res;
|
||||
}
|
||||
|
||||
assertAuthorization(res) {
|
||||
if (res.status === 401 || res.status === 404) return Promise.reject(new GiteaUnauthorizedError());
|
||||
return res;
|
||||
}
|
||||
|
||||
}
|
||||
|
Reference in New Issue
Block a user