356 lines
11 KiB
TypeScript
356 lines
11 KiB
TypeScript
import React, { Fragment } from 'react';
|
|
import { Page } from '../Page';
|
|
import { connect, DispatchProp } from 'react-redux';
|
|
import Board, { addColumn } from '@lourenci/react-kanban';
|
|
import { fetchBoards } from '../../store/actions/boards';
|
|
import { createIssue, fetchIssues } from '../../store/actions/issues';
|
|
import { buildKanboard, moveCard } from '../../store/actions/kanboards';
|
|
import { Loader } from '../Loader';
|
|
import { IssueCard } from './IssueCard';
|
|
import { Modal } from '../Modal';
|
|
import { fetchProjectsMilestones } from '../../store/actions/projects';
|
|
|
|
export interface BoardPageProps extends DispatchProp {
|
|
board: any
|
|
kanboard: any
|
|
milestones: any
|
|
}
|
|
|
|
export class BoardPage extends React.Component<BoardPageProps> {
|
|
|
|
state = {
|
|
newCardModalActive: false,
|
|
newCardLaneID: 0,
|
|
newCard: {
|
|
title: "",
|
|
body: "",
|
|
project: ""
|
|
},
|
|
compactMode: true,
|
|
hasError: false,
|
|
selectedMilestone: "",
|
|
}
|
|
|
|
onNewCardTitleChange: (evt: any) => void;
|
|
onNewCardBodyChange: (evt: any) => void;
|
|
onNewCardProjectChange: (evt: any) => void;
|
|
|
|
constructor(props: BoardPageProps) {
|
|
super(props);
|
|
this.renderLaneHeader = this.renderLaneHeader.bind(this);
|
|
this.onNewCardClick = this.onNewCardClick.bind(this);
|
|
this.onNewCardCloseClick = this.onNewCardCloseClick.bind(this);
|
|
this.onNewCardTitleChange = this.onNewCardAttrChange.bind(this, 'title');
|
|
this.onNewCardBodyChange = this.onNewCardAttrChange.bind(this, 'body');
|
|
this.onNewCardProjectChange = this.onNewCardAttrChange.bind(this, 'project');
|
|
this.onNewCardSaveClick = this.onNewCardSaveClick.bind(this);
|
|
}
|
|
|
|
componentDidCatch(error, errorInfo) {
|
|
// You can also log the error to an error reporting service
|
|
console.error(error, errorInfo);
|
|
}
|
|
|
|
handleMilestonesChange(e: any) {
|
|
let m = (e.target as HTMLInputElement).value;
|
|
this.setState(state => ({ ...state, selectedMilestone: m }), this.requestBuildKanboard);
|
|
//this.requestBuildKanboard();
|
|
}
|
|
|
|
render() {
|
|
const { board } = this.props;
|
|
return (
|
|
<Page title={`${board ? (board.title + ' - ') : ''}GenGitKan`}>
|
|
{this.renderBoard()}
|
|
</Page>
|
|
);
|
|
}
|
|
|
|
renderBoard() {
|
|
const { kanboard, board, milestones } = this.props;
|
|
const { selectedMilestone } = this.state;
|
|
|
|
if (!kanboard) {
|
|
return <Loader></Loader>
|
|
}
|
|
|
|
return (
|
|
<Fragment>
|
|
<nav className="navbar is-light">
|
|
<div className="container is-fluid">
|
|
<div className="navbar-start">
|
|
<div className="navbar-item">
|
|
{board.title}
|
|
</div>
|
|
</div>
|
|
<div className="navbar-end">
|
|
<div className="navbar-item">
|
|
<div className="select">
|
|
<select onChange={this.handleMilestonesChange.bind(this)}>
|
|
<option value="">Sélectionner un jalon</option>
|
|
{
|
|
milestones.length > 0 ? milestones.map((k: any) => {
|
|
return (
|
|
<optgroup label={k.project}>
|
|
{
|
|
k.milestones.map((m: any) => {
|
|
return (
|
|
<option value={m.title} key={m.id}>{m.title}</option>
|
|
)
|
|
})
|
|
}
|
|
</optgroup>
|
|
)
|
|
}) : ""
|
|
}
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<div className="navbar-item">
|
|
|
|
<div className="field">
|
|
|
|
<input id="compactMode"
|
|
checked={this.state.compactMode}
|
|
onChange={this.onCompactModeChange.bind(this)}
|
|
type="checkbox"
|
|
className="switch is-outlined is-success"
|
|
name="compactMode"
|
|
/>
|
|
<label htmlFor="compactMode">Mode compact</label>
|
|
</div>
|
|
</div>
|
|
<a href={`#/boards/${board.id}/edit`} className="navbar-item">
|
|
<span className="icon">
|
|
<i className="fa fa-edit fa-fw"></i>
|
|
</span>
|
|
<span>Modifier le tableau</span>
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</nav>
|
|
<div className="container is-fluid">
|
|
<div className="kanboard-container is-fullheight">
|
|
<Board
|
|
renderCard={this.renderCard.bind(this)}
|
|
renderColumnHeader={this.renderLaneHeader.bind(this)}
|
|
onCardDragEnd={this.onCardDragEnd.bind(this)}
|
|
disableColumnDrag={true}
|
|
>
|
|
{kanboard}
|
|
</Board>
|
|
{this.renderNewCardModal()}
|
|
</div>
|
|
</div>
|
|
</Fragment>
|
|
|
|
);
|
|
}
|
|
|
|
renderNewCardModal() {
|
|
const { newCardModalActive, newCardLaneID } = this.state;
|
|
const { board } = this.props;
|
|
if (!board || newCardLaneID === undefined) return null;
|
|
|
|
return (
|
|
<Modal active={newCardModalActive}>
|
|
<div className="new-card-modal">
|
|
<article className="message">
|
|
<div className="message-header">
|
|
<p><span>"{board.lanes[newCardLaneID].title}" - Nouveau ticket</span></p>
|
|
<button className="delete" aria-label="delete" onClick={this.onNewCardCloseClick}></button>
|
|
</div>
|
|
<div className="message-body">
|
|
<div className="field">
|
|
<div className="control">
|
|
<input className="input is-medium" type="text"
|
|
placeholder="Titre de votre ticket..."
|
|
value={this.state.newCard.title}
|
|
onChange={this.onNewCardTitleChange} />
|
|
</div>
|
|
</div>
|
|
<div className="field">
|
|
<div className="control">
|
|
<textarea className="textarea"
|
|
placeholder="Description du nouveau ticket..."
|
|
value={this.state.newCard.body}
|
|
onChange={this.onNewCardBodyChange}
|
|
rows={10}>
|
|
</textarea>
|
|
</div>
|
|
</div>
|
|
<div className="field">
|
|
<div className="control is-expanded">
|
|
<div className="select is-fullwidth">
|
|
<select
|
|
value={this.state.newCard.project}
|
|
onChange={this.onNewCardProjectChange}>
|
|
{
|
|
board.projects.map((p: any, i: number) => {
|
|
return <option key={`new-card-project-${i}`} value={p}>{p}</option>
|
|
})
|
|
}
|
|
</select>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div className="field is-grouped is-grouped-right">
|
|
<p className="control">
|
|
<a className="button is-light"
|
|
onClick={this.onNewCardCloseClick}>
|
|
Annuler
|
|
</a>
|
|
</p>
|
|
<p className="control">
|
|
<a className="button is-primary"
|
|
onClick={this.onNewCardSaveClick}>
|
|
Enregistrer
|
|
</a>
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</article>
|
|
</div>
|
|
</Modal>
|
|
)
|
|
}
|
|
|
|
renderCard(card: any) {
|
|
return <IssueCard compact={this.state.compactMode} card={card} />;
|
|
}
|
|
|
|
renderLaneHeader(lane: any) {
|
|
return (
|
|
<div className="kanboard-lane">
|
|
<div className="level">
|
|
<div className="level-left">
|
|
<div className="level-item">
|
|
<span className="tag is-primary is-light is-normal">{lane.cards.length}</span>
|
|
</div>
|
|
<button className="button is-light level-item is-small expand"
|
|
onClick={this.onMinimizeColumn}>
|
|
<span className="icon">
|
|
<i className="fas fa-chevron-right" aria-hidden="true"></i>
|
|
</span>
|
|
</button>
|
|
<h3 className="level-item is-size-5">
|
|
{lane.title}
|
|
</h3>
|
|
</div>
|
|
<div className="level-right is-show-expand">
|
|
<button className="button is-light level-item is-small"
|
|
onClick={this.onNewCardClick.bind(this, lane.id)}>
|
|
<span className="icon">
|
|
<i className="fas fa-plus" aria-hidden="true"></i>
|
|
</span>
|
|
</button>
|
|
<button className="button is-light level-item is-small"
|
|
onClick={this.onMinimizeColumn}>
|
|
<span className="icon">
|
|
<i className="fas fa-chevron-left" aria-hidden="true"></i>
|
|
</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
onMinimizeColumn(e: any) {
|
|
e.currentTarget.closest('.react-kanban-column').classList.toggle('minimized');
|
|
}
|
|
|
|
onCardDragEnd(card: any, source: any, dest: any) {
|
|
const { board } = this.props;
|
|
this.props.dispatch(moveCard(
|
|
board.id,
|
|
source.fromColumnId,
|
|
source.fromPosition,
|
|
dest.toColumnId,
|
|
dest.toPosition
|
|
));
|
|
}
|
|
|
|
componentDidMount() {
|
|
const { board } = this.props;
|
|
if (!board) {
|
|
this.requestBoardsUpdate();
|
|
return
|
|
}
|
|
|
|
this.requestBuildKanboard();
|
|
}
|
|
|
|
onNewCardClick(laneID: string) {
|
|
const { board } = this.props;
|
|
this.setState({
|
|
newCardModalActive: true,
|
|
newCardLaneID: laneID,
|
|
newCard: {
|
|
title: "",
|
|
body: "",
|
|
project: board.projects[0],
|
|
}
|
|
});
|
|
}
|
|
|
|
onNewCardCloseClick() {
|
|
this.setState({ newCardModalActive: false });
|
|
}
|
|
|
|
onNewCardAttrChange(attrName: string, evt: React.ChangeEvent) {
|
|
const value = (evt.target as HTMLInputElement).value;
|
|
this.setState((state: any) => {
|
|
return {
|
|
...state,
|
|
newCard: {
|
|
...state.newCard,
|
|
[attrName]: value,
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
onNewCardSaveClick() {
|
|
const { newCard, newCardLaneID } = this.state;
|
|
const { board } = this.props;
|
|
this.setState({ newCardModalActive: false });
|
|
this.props.dispatch(createIssue(
|
|
newCard.project,
|
|
newCard.title,
|
|
newCard.body,
|
|
board.lanes[newCardLaneID].issueLabel
|
|
));
|
|
}
|
|
|
|
componentDidUpdate(prevProps: any) {
|
|
if (prevProps.board !== this.props.board) this.requestBuildKanboard();
|
|
}
|
|
|
|
requestBoardsUpdate() {
|
|
this.props.dispatch(fetchBoards());
|
|
}
|
|
|
|
requestBuildKanboard() {
|
|
const { board } = this.props;
|
|
const { selectedMilestone } = this.state;
|
|
if (!board) return;
|
|
this.props.dispatch(fetchProjectsMilestones(board.projects));
|
|
this.props.dispatch(buildKanboard(board, selectedMilestone));
|
|
}
|
|
|
|
onCompactModeChange(evt: React.ChangeEvent) {
|
|
const checked = (evt.currentTarget as HTMLInputElement).checked;
|
|
this.setState(state => ({ ...state, compactMode: checked }));
|
|
}
|
|
|
|
}
|
|
|
|
export const ConnectedBoardPage = connect(function (state: any, props: any) {
|
|
const boardID = props.match.params.id;
|
|
return {
|
|
board: state.boards.byID[boardID],
|
|
kanboard: state.kanboards.byID[boardID],
|
|
milestones: state.projects.milestones
|
|
};
|
|
})(BoardPage); |