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
This commit is contained in:
parent
647c5c0806
commit
b4ce7c3777
|
@ -1,6 +1,6 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Page } from '../Page';
|
import { Page } from '../Page';
|
||||||
import { connect } from 'react-redux';
|
import { connect, DispatchProp } from 'react-redux';
|
||||||
import Board from '@lourenci/react-kanban';
|
import Board from '@lourenci/react-kanban';
|
||||||
import { fetchBoards } from '../../store/actions/boards';
|
import { fetchBoards } from '../../store/actions/boards';
|
||||||
import { createIssue } from '../../store/actions/issues';
|
import { createIssue } from '../../store/actions/issues';
|
||||||
|
@ -9,7 +9,11 @@ import { Loader } from '../Loader';
|
||||||
import { IssueCard } from './IssueCard';
|
import { IssueCard } from './IssueCard';
|
||||||
import { Modal } from '../Modal';
|
import { Modal } from '../Modal';
|
||||||
|
|
||||||
export class BoardPage extends React.Component {
|
export interface BoardPageProps extends DispatchProp {
|
||||||
|
board: any
|
||||||
|
kanboard: any
|
||||||
|
}
|
||||||
|
export class BoardPage extends React.Component<BoardPageProps> {
|
||||||
|
|
||||||
state = {
|
state = {
|
||||||
newCardModalActive: false,
|
newCardModalActive: false,
|
||||||
|
@ -21,7 +25,11 @@ export class BoardPage extends React.Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(props) {
|
onNewCardTitleChange: (evt: any) => void;
|
||||||
|
onNewCardBodyChange: (evt: any) => void;
|
||||||
|
onNewCardProjectChange: (evt: any) => void;
|
||||||
|
|
||||||
|
constructor(props: BoardPageProps) {
|
||||||
super(props);
|
super(props);
|
||||||
this.renderLaneHeader = this.renderLaneHeader.bind(this);
|
this.renderLaneHeader = this.renderLaneHeader.bind(this);
|
||||||
this.onNewCardClick = this.onNewCardClick.bind(this);
|
this.onNewCardClick = this.onNewCardClick.bind(this);
|
||||||
|
@ -90,18 +98,18 @@ export class BoardPage extends React.Component {
|
||||||
placeholder="Description du nouveau ticket..."
|
placeholder="Description du nouveau ticket..."
|
||||||
value={this.state.newCard.body}
|
value={this.state.newCard.body}
|
||||||
onChange={this.onNewCardBodyChange}
|
onChange={this.onNewCardBodyChange}
|
||||||
rows="10">
|
rows={10}>
|
||||||
</textarea>
|
</textarea>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="field">
|
<div className="field">
|
||||||
<div className="control is-expanded">
|
<div className="control is-expanded">
|
||||||
<div className="select is-fullwidth"
|
<div className="select is-fullwidth">
|
||||||
|
<select
|
||||||
value={this.state.newCard.project}
|
value={this.state.newCard.project}
|
||||||
onChange={this.onNewCardProjectChange}>
|
onChange={this.onNewCardProjectChange}>
|
||||||
<select>
|
|
||||||
{
|
{
|
||||||
board.projects.map((p, i) => {
|
board.projects.map((p: any, i: number) => {
|
||||||
return <option key={`new-card-project-${i}`} value={p}>{p}</option>
|
return <option key={`new-card-project-${i}`} value={p}>{p}</option>
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -130,11 +138,11 @@ export class BoardPage extends React.Component {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
renderCard(card) {
|
renderCard(card: any) {
|
||||||
return <IssueCard card={card} />;
|
return <IssueCard card={card} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
renderLaneHeader(lane) {
|
renderLaneHeader(lane: any) {
|
||||||
return (
|
return (
|
||||||
<div className="kanboard-lane">
|
<div className="kanboard-lane">
|
||||||
<div className="level">
|
<div className="level">
|
||||||
|
@ -154,7 +162,7 @@ export class BoardPage extends React.Component {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
onCardDragEnd(source, dest) {
|
onCardDragEnd(source: any, dest: any) {
|
||||||
const { board } = this.props;
|
const { board } = this.props;
|
||||||
this.props.dispatch(moveCard(
|
this.props.dispatch(moveCard(
|
||||||
board.id,
|
board.id,
|
||||||
|
@ -175,7 +183,7 @@ export class BoardPage extends React.Component {
|
||||||
this.requestBuildKanboard();
|
this.requestBuildKanboard();
|
||||||
}
|
}
|
||||||
|
|
||||||
onNewCardClick(laneID) {
|
onNewCardClick(laneID: string) {
|
||||||
const { board } = this.props;
|
const { board } = this.props;
|
||||||
this.setState({
|
this.setState({
|
||||||
newCardModalActive: true,
|
newCardModalActive: true,
|
||||||
|
@ -192,9 +200,9 @@ export class BoardPage extends React.Component {
|
||||||
this.setState({ newCardModalActive: false });
|
this.setState({ newCardModalActive: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
onNewCardAttrChange(attrName, evt) {
|
onNewCardAttrChange(attrName: string, evt: React.ChangeEvent) {
|
||||||
const value = evt.target.value;
|
const value = (evt.target as HTMLInputElement).value;
|
||||||
this.setState(state => {
|
this.setState((state: any) => {
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
newCard: {
|
newCard: {
|
||||||
|
@ -217,7 +225,7 @@ export class BoardPage extends React.Component {
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
componentDidUpdate(prevProps: any) {
|
||||||
if (prevProps.board !== this.props.board) this.requestBuildKanboard();
|
if (prevProps.board !== this.props.board) this.requestBuildKanboard();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -233,7 +241,7 @@ export class BoardPage extends React.Component {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ConnectedBoardPage = connect(function(state, props) {
|
export const ConnectedBoardPage = connect(function(state: any, props: any) {
|
||||||
const boardID = props.match.params.id;
|
const boardID = props.match.params.id;
|
||||||
return {
|
return {
|
||||||
board: state.boards.byID[boardID],
|
board: state.boards.byID[boardID],
|
||||||
|
|
|
@ -30,6 +30,7 @@ export class EditBoardPage extends React.Component<EditorBoardPageProps> {
|
||||||
onBoardDescriptionChange: (evt: any) => void;
|
onBoardDescriptionChange: (evt: any) => void;
|
||||||
onBoardLaneTitleChange: (laneIndex: any, evt: any) => void;
|
onBoardLaneTitleChange: (laneIndex: any, evt: any) => void;
|
||||||
onBoardLaneIssueLabelChange: (laneIndex: any, evt: any) => void;
|
onBoardLaneIssueLabelChange: (laneIndex: any, evt: any) => void;
|
||||||
|
onBoardLaneIssueCollectRemainingIssuesChange: (laneIndex: any, evt: any) => void;
|
||||||
|
|
||||||
static getDerivedStateFromProps(props: any, state: any) {
|
static getDerivedStateFromProps(props: any, state: any) {
|
||||||
const { board, isLoading } = props;
|
const { board, isLoading } = props;
|
||||||
|
@ -54,6 +55,7 @@ export class EditBoardPage extends React.Component<EditorBoardPageProps> {
|
||||||
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.onBoardLaneIssueCollectRemainingIssuesChange = this.onBoardLaneAttrChange.bind(this, 'collectRemainingIssues');
|
||||||
this.onDeleteBoardClick = this.onDeleteBoardClick.bind(this);
|
this.onDeleteBoardClick = this.onDeleteBoardClick.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -223,6 +225,21 @@ export class EditBoardPage extends React.Component<EditorBoardPageProps> {
|
||||||
</div>
|
</div>
|
||||||
</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>
|
||||||
<div className="column is-2 is-flex">
|
<div className="column is-2 is-flex">
|
||||||
<div className="buttons">
|
<div className="buttons">
|
||||||
|
@ -378,13 +395,15 @@ export class EditBoardPage extends React.Component<EditorBoardPageProps> {
|
||||||
}
|
}
|
||||||
|
|
||||||
onBoardLaneAttrChange(attrName: string, laneIndex: number, evt: React.ChangeEvent) {
|
onBoardLaneAttrChange(attrName: string, laneIndex: number, evt: React.ChangeEvent) {
|
||||||
const value = (evt.target as HTMLInputElement).value;
|
const input = evt.target as HTMLInputElement;
|
||||||
|
const value = input.type === "checkbox" ? input.checked : input.value;
|
||||||
this.setState((state: any) => {
|
this.setState((state: any) => {
|
||||||
const lanes = [ ...state.board.lanes ];
|
const lanes = [ ...state.board.lanes ];
|
||||||
lanes[laneIndex] = {
|
lanes[laneIndex] = {
|
||||||
...state.board.lanes[laneIndex],
|
...state.board.lanes[laneIndex],
|
||||||
[attrName]: value
|
[attrName]: value
|
||||||
};
|
};
|
||||||
|
console.log(lanes);
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
edited: true,
|
edited: true,
|
||||||
|
|
|
@ -1,6 +1,10 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
export class IssueCard extends React.PureComponent {
|
export interface IssueCardProps {
|
||||||
|
card: any
|
||||||
|
}
|
||||||
|
|
||||||
|
export class IssueCard extends React.PureComponent<IssueCardProps> {
|
||||||
render() {
|
render() {
|
||||||
const { card } = this.props;
|
const { card } = this.props;
|
||||||
const issueURLInfo = extractInfoFromIssueURL(card.issue.url);
|
const issueURLInfo = extractInfoFromIssueURL(card.issue.url);
|
||||||
|
@ -49,9 +53,12 @@ export class IssueCard extends React.PureComponent {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function extractInfoFromIssueURL(issueURL) {
|
function extractInfoFromIssueURL(issueURL: string): any|void {
|
||||||
const pattern = /^(https?:\/\/[^\/]+)\/api\/v1\/repos\/([^\/]+)\/([^\/]+)\/.*$/;
|
const pattern = /^(https?:\/\/[^\/]+)\/api\/v1\/repos\/([^\/]+)\/([^\/]+)\/.*$/;
|
||||||
const matches = pattern.exec(issueURL);
|
const matches = pattern.exec(issueURL);
|
||||||
|
|
||||||
|
if (!matches) return;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
baseURL: matches[1],
|
baseURL: matches[1],
|
||||||
owner: matches[2],
|
owner: matches[2],
|
||||||
|
|
|
@ -2,8 +2,8 @@ import React, { PropsWithChildren } from 'react';
|
||||||
|
|
||||||
export interface ModalProps {
|
export interface ModalProps {
|
||||||
active: boolean
|
active: boolean
|
||||||
showCloseButton: boolean
|
showCloseButton?: boolean
|
||||||
onClose: (evt: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void
|
onClose?: (evt: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Modal extends React.PureComponent<PropsWithChildren<ModalProps>> {
|
export class Modal extends React.PureComponent<PropsWithChildren<ModalProps>> {
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
declare module "*.svg" {
|
declare module "*.svg" {
|
||||||
const content: any;
|
const content: any;
|
||||||
export default content;
|
export default content;
|
||||||
}
|
}
|
||||||
|
declare module '@lourenci/react-kanban';
|
|
@ -2,6 +2,9 @@ import { select, put } from 'redux-saga/effects';
|
||||||
import { fetchIssues, addLabel, removeLabel } from '../actions/issues';
|
import { fetchIssues, addLabel, removeLabel } from '../actions/issues';
|
||||||
import { fetchIssuesSaga } from './issues';
|
import { fetchIssuesSaga } from './issues';
|
||||||
import { BUILD_KANBOARD_SUCCESS, buildKanboard, BUILD_KANBOARD_FAILURE } from '../actions/kanboards';
|
import { BUILD_KANBOARD_SUCCESS, buildKanboard, BUILD_KANBOARD_FAILURE } from '../actions/kanboards';
|
||||||
|
import { Project, Issue } from '../../types/gitea';
|
||||||
|
import { Board, BoardLane } from '../../types/board';
|
||||||
|
import { KanboardLane, Kanboard, KanboardCard } from '../../types/kanboard';
|
||||||
|
|
||||||
export function* moveCardSaga(action: any) {
|
export function* moveCardSaga(action: any) {
|
||||||
const {
|
const {
|
||||||
|
@ -64,21 +67,21 @@ export function* refreshKanboardSaga(action: any) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function createCards(projects: any[], issues: any, lane: any) {
|
function createCards(projects: Project[], issues: any, lane: BoardLane, rest: Set<KanboardCard>) {
|
||||||
return projects.reduce((laneCards, p) => {
|
const cards: KanboardCard[] = projects.reduce((laneCards, p) => {
|
||||||
|
|
||||||
const projectIssues = p in issues.byProject ? issues.byProject[p] : [];
|
const projectIssues = p in issues.byProject ? issues.byProject[p] : [];
|
||||||
|
|
||||||
return projectIssues.reduce((projectCards: any, issue: any) => {
|
return projectIssues.reduce((projectCards: KanboardCard[], issue: any) => {
|
||||||
|
|
||||||
const hasLabel = issue.labels.some((l: any) => l.name === lane.issueLabel);
|
const hasLabel = issue.labels.some((l: any) => l.name === lane.issueLabel);
|
||||||
|
const card = getMemoizedKanboardCard(issue.id, issue.title, p, issue);
|
||||||
|
|
||||||
if (hasLabel) {
|
if (hasLabel) {
|
||||||
projectCards.push({
|
projectCards.push(card);
|
||||||
id: issue.id,
|
rest.delete(card);
|
||||||
title: issue.title,
|
} else {
|
||||||
project: p,
|
rest.add(card);
|
||||||
issue: issue,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return projectCards;
|
return projectCards;
|
||||||
|
@ -86,22 +89,55 @@ function createCards(projects: any[], issues: any, lane: any) {
|
||||||
}, laneCards);
|
}, laneCards);
|
||||||
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
return cards;
|
||||||
}
|
}
|
||||||
|
|
||||||
function createLane(projects: any, issues: any, lane: any, index: any) {
|
const kanboardCardMemo: {[key: string]: KanboardCard} = {};
|
||||||
return {
|
|
||||||
id: index,
|
function getMemoizedKanboardCard(id: number, title: string, project: Project, issue: Issue): KanboardCard {
|
||||||
title: lane.title,
|
const key = `${project.id}-${issue.id}-${id}`;
|
||||||
cards: createCards(projects, issues, lane)
|
if (kanboardCardMemo.hasOwnProperty(key)) return kanboardCardMemo[key];
|
||||||
}
|
kanboardCardMemo[key] = { id, title, project, issue };
|
||||||
|
return kanboardCardMemo[key];
|
||||||
}
|
}
|
||||||
|
|
||||||
function createKanboard(board: any, issues: any) {
|
function resetKandboarCardMemo() {
|
||||||
|
Object.keys(kanboardCardMemo).forEach(k => delete kanboardCardMemo[k]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function createKanboardLanes(board: Board, issues: any): KanboardLane[] {
|
||||||
|
const lanes: KanboardLane[] = [];
|
||||||
|
const rest = new Set<KanboardCard>();
|
||||||
|
|
||||||
|
resetKandboarCardMemo();
|
||||||
|
|
||||||
|
board.lanes.forEach((l: BoardLane, i: number) => {
|
||||||
|
const cards = createCards(board.projects, issues, l, rest);
|
||||||
|
lanes.push({
|
||||||
|
id: i,
|
||||||
|
title: l.title,
|
||||||
|
cards,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Assign remaining issues
|
||||||
|
board.lanes.forEach((l: BoardLane, i: number) => {
|
||||||
|
if (!l.collectRemainingIssues) return;
|
||||||
|
lanes[i].cards.push(...Array.from(rest.values()));
|
||||||
|
});
|
||||||
|
|
||||||
|
resetKandboarCardMemo();
|
||||||
|
|
||||||
|
return lanes;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createKanboard(board: Board, issues: any) {
|
||||||
if (!board) return null;
|
if (!board) return null;
|
||||||
|
|
||||||
const kanboard = {
|
const kanboard = {
|
||||||
id: board.id,
|
id: board.id,
|
||||||
lanes: board.lanes.map(createLane.bind(null, board.projects, issues)),
|
lanes: createKanboardLanes(board, issues),
|
||||||
};
|
};
|
||||||
|
|
||||||
return kanboard;
|
return kanboard;
|
||||||
|
|
|
@ -0,0 +1,19 @@
|
||||||
|
import { Project } from "./gitea"
|
||||||
|
|
||||||
|
export type BoardLaneId = string
|
||||||
|
export type BoardId = string
|
||||||
|
|
||||||
|
export interface Board {
|
||||||
|
id: BoardId
|
||||||
|
title: string
|
||||||
|
description: string
|
||||||
|
lanes: BoardLane[]
|
||||||
|
projects: Project[]
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface BoardLane {
|
||||||
|
id: BoardLaneId
|
||||||
|
title: string
|
||||||
|
issueLabel: string
|
||||||
|
collectRemainingIssues: boolean
|
||||||
|
};
|
|
@ -0,0 +1,2 @@
|
||||||
|
export type Project = any
|
||||||
|
export type Issue = any
|
|
@ -0,0 +1,21 @@
|
||||||
|
import { Project, Issue } from "./gitea";
|
||||||
|
import { BoardId } from "./board";
|
||||||
|
|
||||||
|
export interface Kanboard {
|
||||||
|
id: BoardId
|
||||||
|
lanes: KanboardLane[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface KanboardLane {
|
||||||
|
id: number
|
||||||
|
title: string
|
||||||
|
cards: KanboardCard[]
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export interface KanboardCard {
|
||||||
|
id: number
|
||||||
|
title: string
|
||||||
|
project: Project
|
||||||
|
issue: Issue
|
||||||
|
}
|
|
@ -23,4 +23,5 @@ type BoardLane struct {
|
||||||
ID BoardLaneID `json:"id"`
|
ID BoardLaneID `json:"id"`
|
||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
IssueLabel string `json:"issueLabel"`
|
IssueLabel string `json:"issueLabel"`
|
||||||
|
CollectRemainingIssues bool `json:"collectRemainingIssues"`
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue