Files
gengitkan/client/src/components/BoardPage/EditBoardPage.tsx
William Petit b4ce7c3777 Possibilité de créer une voie de type "Backlog"
Une voie peut désormais "récolter" toutes les issues qui ne sont pas
déjà sélectionnées par d'autres voies i.e. matérialiser un "backlog".

Voir #22
2020-04-30 15:43:40 +02:00

448 lines
14 KiB
TypeScript

import React from 'react';
import { Page } from '../Page';
import { connect, DispatchProp } from 'react-redux';
import { selectFlagsIsLoading } from '../../store/selectors/flags';
import { fetchBoards, saveBoard, deleteBoard } from '../../store/actions/boards';
import { fetchProjects } from '../../store/actions/projects';
import uuidv4 from 'uuid/v4';
import { Loader } from '../Loader';
import { RouteComponentProps } from 'react-router';
export interface EditorBoardPageProps extends DispatchProp, RouteComponentProps {
isLoading: boolean
projects: any
}
export class EditBoardPage extends React.Component<EditorBoardPageProps> {
state = {
edited: false,
board: {
id: "",
title: "",
description: "",
projects: [],
lanes: []
},
}
onBoardTitleChange: (evt: any) => void;
onBoardDescriptionChange: (evt: any) => void;
onBoardLaneTitleChange: (laneIndex: any, evt: any) => void;
onBoardLaneIssueLabelChange: (laneIndex: any, evt: any) => void;
onBoardLaneIssueCollectRemainingIssuesChange: (laneIndex: any, evt: any) => void;
static getDerivedStateFromProps(props: any, state: any) {
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: any) => ({ ...l })) ]
}
};
}
constructor(props: any) {
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');
this.onBoardLaneIssueCollectRemainingIssuesChange = this.onBoardLaneAttrChange.bind(this, 'collectRemainingIssues');
this.onDeleteBoardClick = this.onDeleteBoardClick.bind(this);
}
render() {
const { isLoading } = this.props;
const { board } = this.state;
if (isLoading) {
return (
<Page>
<Loader></Loader>
</Page>
)
};
return (
<Page>
<div className="container is-fluid has-margin-top-normal">
<div className="columns">
<div className="column is-6 is-offset-3">
<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>
<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: number, value: any, withDeleteAddon: boolean) => {
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: any) => {
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: number, lane: any) => {
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 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>
</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: any) => {
const lanes = [
...state.board.lanes,
{ id: uuidv4(), title: "", issueLabel: "" }
];
return {
...state,
edited: true,
board: {
...state.board,
lanes
}
}
});
}
onBoardProjectDelete(projectIndex: number) {
this.setState((state: any) => {
const projects = [ ...state.board.projects ]
projects.splice(projectIndex, 1);
return {
...state,
edited: true,
board: {
...state.board,
projects
}
}
});
}
onBoardLaneMove(laneIndex: number, direction: number) {
this.setState((state: any) => {
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: number) {
this.setState((state: any) => {
const lanes = [ ...state.board.lanes ]
lanes.splice(laneIndex, 1);
return {
...state,
edited: true,
board: {
...state.board,
lanes
}
}
});
}
onBoardProjectChange(projectIndex: number, evt: React.ChangeEvent) {
const value = (evt.target as HTMLInputElement).value ;
this.setState((state: any) => {
const projects = [ ...state.board.projects ];
projects[projectIndex] = value;
return {
...state,
edited: true,
board: {
...state.board,
projects
}
}
});
}
onBoardAttrChange(attrName: string, evt: React.ChangeEvent) {
const value = (evt.target as HTMLInputElement).value;
this.setState((state: any) => {
return {
...state,
edited: true,
board: {
...state.board,
[attrName]: value,
}
}
});
}
onBoardLaneAttrChange(attrName: string, laneIndex: number, evt: React.ChangeEvent) {
const input = evt.target as HTMLInputElement;
const value = input.type === "checkbox" ? input.checked : input.value;
this.setState((state: any) => {
const lanes = [ ...state.board.lanes ];
lanes[laneIndex] = {
...state.board.lanes[laneIndex],
[attrName]: value
};
console.log(lanes);
return {
...state,
edited: true,
board: {
...state.board,
lanes
}
}
});
}
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;
this.props.dispatch(deleteBoard(board.id));
this.props.history.push('/');
}
componentDidMount() {
this.props.dispatch(fetchBoards());
this.props.dispatch(fetchProjects());
}
}
export const ConnectedEditBoardPage = connect(function(state: any, props: any) {
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);