2019-12-01 22:12:13 +01:00
|
|
|
import React from 'react';
|
|
|
|
import { Page } from '../Page';
|
2020-04-30 13:02:56 +02:00
|
|
|
import { connect, DispatchProp } from 'react-redux';
|
2019-12-01 22:12:13 +01:00
|
|
|
import { selectFlagsIsLoading } from '../../store/selectors/flags';
|
2019-12-13 13:28:59 +01:00
|
|
|
import { fetchBoards, saveBoard, deleteBoard } from '../../store/actions/boards';
|
2019-12-01 22:12:13 +01:00
|
|
|
import { fetchProjects } from '../../store/actions/projects';
|
|
|
|
import uuidv4 from 'uuid/v4';
|
2019-12-13 13:28:59 +01:00
|
|
|
import { Loader } from '../Loader';
|
2020-04-30 13:02:56 +02:00
|
|
|
import { RouteComponentProps } from 'react-router';
|
2019-12-01 22:12:13 +01:00
|
|
|
|
2020-04-30 13:02:56 +02:00
|
|
|
export interface EditorBoardPageProps extends DispatchProp, RouteComponentProps {
|
|
|
|
isLoading: boolean
|
|
|
|
projects: any
|
|
|
|
}
|
|
|
|
|
|
|
|
export class EditBoardPage extends React.Component<EditorBoardPageProps> {
|
2019-12-01 22:12:13 +01:00
|
|
|
|
|
|
|
state = {
|
|
|
|
edited: false,
|
|
|
|
board: {
|
2019-12-13 13:28:59 +01:00
|
|
|
id: "",
|
2019-12-01 22:12:13 +01:00
|
|
|
title: "",
|
|
|
|
description: "",
|
|
|
|
projects: [],
|
|
|
|
lanes: []
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
2020-04-30 13:02:56 +02:00
|
|
|
onBoardTitleChange: (evt: any) => void;
|
|
|
|
onBoardDescriptionChange: (evt: any) => void;
|
|
|
|
onBoardLaneTitleChange: (laneIndex: any, evt: any) => void;
|
|
|
|
onBoardLaneIssueLabelChange: (laneIndex: any, evt: any) => void;
|
2020-04-30 15:43:40 +02:00
|
|
|
onBoardLaneIssueCollectRemainingIssuesChange: (laneIndex: any, evt: any) => void;
|
2020-04-30 13:02:56 +02:00
|
|
|
|
|
|
|
static getDerivedStateFromProps(props: any, state: any) {
|
2019-12-01 22:12:13 +01:00
|
|
|
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 ],
|
2020-04-30 13:02:56 +02:00
|
|
|
lanes: [ ...board.lanes.map((l: any) => ({ ...l })) ]
|
2019-12-01 22:12:13 +01:00
|
|
|
}
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2020-04-30 13:02:56 +02:00
|
|
|
constructor(props: any) {
|
2019-12-01 22:12:13 +01:00
|
|
|
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');
|
2020-04-30 15:43:40 +02:00
|
|
|
this.onBoardLaneIssueCollectRemainingIssuesChange = this.onBoardLaneAttrChange.bind(this, 'collectRemainingIssues');
|
2019-12-13 13:28:59 +01:00
|
|
|
this.onDeleteBoardClick = this.onDeleteBoardClick.bind(this);
|
2019-12-01 22:12:13 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
render() {
|
|
|
|
const { isLoading } = this.props;
|
|
|
|
const { board } = this.state;
|
|
|
|
|
|
|
|
if (isLoading) {
|
|
|
|
return (
|
2019-12-13 13:28:59 +01:00
|
|
|
<Page>
|
|
|
|
<Loader></Loader>
|
|
|
|
</Page>
|
2019-12-01 22:12:13 +01:00
|
|
|
)
|
|
|
|
};
|
|
|
|
|
|
|
|
return (
|
|
|
|
<Page>
|
2019-12-13 12:00:49 +01:00
|
|
|
<div className="container is-fluid has-margin-top-normal">
|
2019-12-01 22:12:13 +01:00
|
|
|
<div className="columns">
|
|
|
|
<div className="column is-6 is-offset-3">
|
2019-12-13 13:28:59 +01:00
|
|
|
<div className="level is-mobile">
|
|
|
|
<div className="level-left">
|
|
|
|
{
|
|
|
|
board.id ?
|
|
|
|
<h3 className="is-size-3 level-item">Éditer le 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>
|
2019-12-01 22:12:13 +01:00
|
|
|
<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;
|
|
|
|
|
2020-04-30 13:02:56 +02:00
|
|
|
const projectSelectField = (projectIndex: number, value: any, withDeleteAddon: boolean) => {
|
2019-12-01 22:12:13 +01:00
|
|
|
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)}>
|
2020-04-30 13:02:56 +02:00
|
|
|
<option value=""></option>
|
2019-12-01 22:12:13 +01:00
|
|
|
{
|
2020-04-30 13:02:56 +02:00
|
|
|
projects.map((p: any) => {
|
2019-12-01 22:12:13 +01:00
|
|
|
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;
|
|
|
|
|
2020-04-30 13:02:56 +02:00
|
|
|
const laneSection = (laneIndex: number, lane: any) => {
|
2019-12-01 22:12:13 +01:00
|
|
|
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>
|
2020-04-30 15:43:40 +02:00
|
|
|
<div className="field is-horizontal">
|
|
|
|
<div className="field-label is-normal"></div>
|
|
|
|
<div className="field-body">
|
|
|
|
<div className="field">
|
|
|
|
<div className="control">
|
|
|
|
<label className="checkbox">
|
|
|
|
<input type="checkbox"
|
|
|
|
checked={lane.hasOwnProperty('collectRemainingIssues') ? !!lane.collectRemainingIssues : false}
|
|
|
|
onChange={this.onBoardLaneIssueCollectRemainingIssuesChange.bind(this, laneIndex)} />
|
|
|
|
Inclure tous les tickets "restants" (i.e. non sélectionnés par les autres voies)
|
|
|
|
</label>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
2019-12-01 22:12:13 +01:00
|
|
|
</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() {
|
2020-04-30 13:02:56 +02:00
|
|
|
this.setState((state: any) => {
|
2019-12-01 22:12:13 +01:00
|
|
|
const lanes = [
|
|
|
|
...state.board.lanes,
|
|
|
|
{ id: uuidv4(), title: "", issueLabel: "" }
|
|
|
|
];
|
|
|
|
return {
|
|
|
|
...state,
|
|
|
|
edited: true,
|
|
|
|
board: {
|
|
|
|
...state.board,
|
|
|
|
lanes
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2020-04-30 13:02:56 +02:00
|
|
|
onBoardProjectDelete(projectIndex: number) {
|
|
|
|
this.setState((state: any) => {
|
2019-12-01 22:12:13 +01:00
|
|
|
const projects = [ ...state.board.projects ]
|
|
|
|
projects.splice(projectIndex, 1);
|
|
|
|
return {
|
|
|
|
...state,
|
|
|
|
edited: true,
|
|
|
|
board: {
|
|
|
|
...state.board,
|
|
|
|
projects
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2020-04-30 13:02:56 +02:00
|
|
|
onBoardLaneMove(laneIndex: number, direction: number) {
|
|
|
|
this.setState((state: any) => {
|
2019-12-01 22:12:13 +01:00
|
|
|
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
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2020-04-30 13:02:56 +02:00
|
|
|
onBoardLaneDelete(laneIndex: number) {
|
|
|
|
this.setState((state: any) => {
|
2019-12-01 22:12:13 +01:00
|
|
|
const lanes = [ ...state.board.lanes ]
|
|
|
|
lanes.splice(laneIndex, 1);
|
|
|
|
return {
|
|
|
|
...state,
|
|
|
|
edited: true,
|
|
|
|
board: {
|
|
|
|
...state.board,
|
|
|
|
lanes
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2020-04-30 13:02:56 +02:00
|
|
|
onBoardProjectChange(projectIndex: number, evt: React.ChangeEvent) {
|
|
|
|
const value = (evt.target as HTMLInputElement).value ;
|
|
|
|
this.setState((state: any) => {
|
2019-12-01 22:12:13 +01:00
|
|
|
const projects = [ ...state.board.projects ];
|
|
|
|
projects[projectIndex] = value;
|
|
|
|
return {
|
|
|
|
...state,
|
|
|
|
edited: true,
|
|
|
|
board: {
|
|
|
|
...state.board,
|
|
|
|
projects
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2020-04-30 13:02:56 +02:00
|
|
|
onBoardAttrChange(attrName: string, evt: React.ChangeEvent) {
|
|
|
|
const value = (evt.target as HTMLInputElement).value;
|
|
|
|
this.setState((state: any) => {
|
2019-12-01 22:12:13 +01:00
|
|
|
return {
|
|
|
|
...state,
|
|
|
|
edited: true,
|
|
|
|
board: {
|
|
|
|
...state.board,
|
|
|
|
[attrName]: value,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2020-04-30 13:02:56 +02:00
|
|
|
onBoardLaneAttrChange(attrName: string, laneIndex: number, evt: React.ChangeEvent) {
|
2020-04-30 15:43:40 +02:00
|
|
|
const input = evt.target as HTMLInputElement;
|
|
|
|
const value = input.type === "checkbox" ? input.checked : input.value;
|
2020-04-30 13:02:56 +02:00
|
|
|
this.setState((state: any) => {
|
2019-12-01 22:12:13 +01:00
|
|
|
const lanes = [ ...state.board.lanes ];
|
|
|
|
lanes[laneIndex] = {
|
|
|
|
...state.board.lanes[laneIndex],
|
|
|
|
[attrName]: value
|
|
|
|
};
|
2020-04-30 15:43:40 +02:00
|
|
|
console.log(lanes);
|
2019-12-01 22:12:13 +01:00
|
|
|
return {
|
|
|
|
...state,
|
|
|
|
edited: true,
|
|
|
|
board: {
|
|
|
|
...state.board,
|
|
|
|
lanes
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
onSaveBoardClick() {
|
2019-12-13 13:28:59 +01:00
|
|
|
let { board } = this.state;
|
|
|
|
board = { ...board };
|
|
|
|
if (!board.id) board.id = uuidv4();
|
|
|
|
this.props.dispatch(saveBoard({...board}));
|
|
|
|
this.props.history.push('/');
|
|
|
|
}
|
|
|
|
|
|
|
|
onDeleteBoardClick() {
|
2019-12-01 22:12:13 +01:00
|
|
|
const { board } = this.state;
|
2019-12-13 13:28:59 +01:00
|
|
|
this.props.dispatch(deleteBoard(board.id));
|
2019-12-01 22:12:13 +01:00
|
|
|
this.props.history.push('/');
|
|
|
|
}
|
|
|
|
|
|
|
|
componentDidMount() {
|
|
|
|
this.props.dispatch(fetchBoards());
|
|
|
|
this.props.dispatch(fetchProjects());
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
2020-04-30 13:02:56 +02:00
|
|
|
export const ConnectedEditBoardPage = connect(function(state: any, props: any) {
|
2019-12-01 22:12:13 +01:00
|
|
|
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);
|