diff --git a/client/src/components/BoardPage/BoardPage.tsx b/client/src/components/BoardPage/BoardPage.tsx index cec2bbd..9825c7a 100644 --- a/client/src/components/BoardPage/BoardPage.tsx +++ b/client/src/components/BoardPage/BoardPage.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { Page } from '../Page'; -import { connect } from 'react-redux'; +import { connect, DispatchProp } from 'react-redux'; import Board from '@lourenci/react-kanban'; import { fetchBoards } from '../../store/actions/boards'; import { createIssue } from '../../store/actions/issues'; @@ -9,7 +9,11 @@ import { Loader } from '../Loader'; import { IssueCard } from './IssueCard'; 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 { state = { 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); this.renderLaneHeader = this.renderLaneHeader.bind(this); this.onNewCardClick = this.onNewCardClick.bind(this); @@ -90,18 +98,18 @@ export class BoardPage extends React.Component { placeholder="Description du nouveau ticket..." value={this.state.newCard.body} onChange={this.onNewCardBodyChange} - rows="10"> + rows={10}>
-
- { - board.projects.map((p, i) => { + board.projects.map((p: any, i: number) => { return }) } @@ -130,11 +138,11 @@ export class BoardPage extends React.Component { ) } - renderCard(card) { + renderCard(card: any) { return ; } - renderLaneHeader(lane) { + renderLaneHeader(lane: any) { return (
@@ -154,7 +162,7 @@ export class BoardPage extends React.Component { ) } - onCardDragEnd(source, dest) { + onCardDragEnd(source: any, dest: any) { const { board } = this.props; this.props.dispatch(moveCard( board.id, @@ -175,7 +183,7 @@ export class BoardPage extends React.Component { this.requestBuildKanboard(); } - onNewCardClick(laneID) { + onNewCardClick(laneID: string) { const { board } = this.props; this.setState({ newCardModalActive: true, @@ -192,9 +200,9 @@ export class BoardPage extends React.Component { this.setState({ newCardModalActive: false }); } - onNewCardAttrChange(attrName, evt) { - const value = evt.target.value; - this.setState(state => { + onNewCardAttrChange(attrName: string, evt: React.ChangeEvent) { + const value = (evt.target as HTMLInputElement).value; + this.setState((state: any) => { return { ...state, newCard: { @@ -217,7 +225,7 @@ export class BoardPage extends React.Component { )); } - componentDidUpdate(prevProps) { + componentDidUpdate(prevProps: any) { 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; return { board: state.boards.byID[boardID], diff --git a/client/src/components/BoardPage/EditBoardPage.tsx b/client/src/components/BoardPage/EditBoardPage.tsx index 2f35ce4..856dadc 100644 --- a/client/src/components/BoardPage/EditBoardPage.tsx +++ b/client/src/components/BoardPage/EditBoardPage.tsx @@ -30,6 +30,7 @@ export class EditBoardPage extends React.Component { 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; @@ -54,6 +55,7 @@ export class EditBoardPage extends React.Component { 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); } @@ -223,6 +225,21 @@ export class EditBoardPage extends React.Component {
+
+
+
+
+
+ +
+
+
+
@@ -378,13 +395,15 @@ export class EditBoardPage extends React.Component { } 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) => { const lanes = [ ...state.board.lanes ]; lanes[laneIndex] = { ...state.board.lanes[laneIndex], [attrName]: value }; + console.log(lanes); return { ...state, edited: true, diff --git a/client/src/components/BoardPage/IssueCard.tsx b/client/src/components/BoardPage/IssueCard.tsx index cea893d..c865a8a 100644 --- a/client/src/components/BoardPage/IssueCard.tsx +++ b/client/src/components/BoardPage/IssueCard.tsx @@ -1,6 +1,10 @@ import React from 'react'; -export class IssueCard extends React.PureComponent { +export interface IssueCardProps { + card: any +} + +export class IssueCard extends React.PureComponent { render() { const { card } = this.props; 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 matches = pattern.exec(issueURL); + + if (!matches) return; + return { baseURL: matches[1], owner: matches[2], diff --git a/client/src/components/Modal.tsx b/client/src/components/Modal.tsx index 48628e7..39e12c7 100644 --- a/client/src/components/Modal.tsx +++ b/client/src/components/Modal.tsx @@ -2,8 +2,8 @@ import React, { PropsWithChildren } from 'react'; export interface ModalProps { active: boolean - showCloseButton: boolean - onClose: (evt: React.MouseEvent) => void + showCloseButton?: boolean + onClose?: (evt: React.MouseEvent) => void } export class Modal extends React.PureComponent> { diff --git a/client/src/custom.d.ts b/client/src/custom.d.ts index c1c6de0..92fcda0 100644 --- a/client/src/custom.d.ts +++ b/client/src/custom.d.ts @@ -1,4 +1,5 @@ declare module "*.svg" { const content: any; export default content; - } \ No newline at end of file +} +declare module '@lourenci/react-kanban'; \ No newline at end of file diff --git a/client/src/store/sagas/kanboards.ts b/client/src/store/sagas/kanboards.ts index 2ec33af..83162cb 100644 --- a/client/src/store/sagas/kanboards.ts +++ b/client/src/store/sagas/kanboards.ts @@ -2,6 +2,9 @@ import { select, put } from 'redux-saga/effects'; import { fetchIssues, addLabel, removeLabel } from '../actions/issues'; import { fetchIssuesSaga } from './issues'; 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) { const { @@ -64,21 +67,21 @@ export function* refreshKanboardSaga(action: any) { } } -function createCards(projects: any[], issues: any, lane: any) { - return projects.reduce((laneCards, p) => { +function createCards(projects: Project[], issues: any, lane: BoardLane, rest: Set) { + const cards: KanboardCard[] = projects.reduce((laneCards, 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 card = getMemoizedKanboardCard(issue.id, issue.title, p, issue); if (hasLabel) { - projectCards.push({ - id: issue.id, - title: issue.title, - project: p, - issue: issue, - }); + projectCards.push(card); + rest.delete(card); + } else { + rest.add(card); } return projectCards; @@ -86,22 +89,55 @@ function createCards(projects: any[], issues: any, lane: any) { }, laneCards); }, []); + + return cards; } -function createLane(projects: any, issues: any, lane: any, index: any) { - return { - id: index, - title: lane.title, - cards: createCards(projects, issues, lane) - } +const kanboardCardMemo: {[key: string]: KanboardCard} = {}; + +function getMemoizedKanboardCard(id: number, title: string, project: Project, issue: Issue): KanboardCard { + const key = `${project.id}-${issue.id}-${id}`; + 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(); + + 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; const kanboard = { id: board.id, - lanes: board.lanes.map(createLane.bind(null, board.projects, issues)), + lanes: createKanboardLanes(board, issues), }; return kanboard; diff --git a/client/src/types/board.ts b/client/src/types/board.ts new file mode 100644 index 0000000..e7a96ab --- /dev/null +++ b/client/src/types/board.ts @@ -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 +}; \ No newline at end of file diff --git a/client/src/types/gitea.ts b/client/src/types/gitea.ts new file mode 100644 index 0000000..852cd27 --- /dev/null +++ b/client/src/types/gitea.ts @@ -0,0 +1,2 @@ +export type Project = any +export type Issue = any \ No newline at end of file diff --git a/client/src/types/kanboard.ts b/client/src/types/kanboard.ts new file mode 100644 index 0000000..ad2668b --- /dev/null +++ b/client/src/types/kanboard.ts @@ -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 +} \ No newline at end of file diff --git a/internal/repository/board.go b/internal/repository/board.go index c7c1a37..1dcbe4d 100644 --- a/internal/repository/board.go +++ b/internal/repository/board.go @@ -20,7 +20,8 @@ type Board struct { type BoardLaneID string type BoardLane struct { - ID BoardLaneID `json:"id"` - Title string `json:"title"` - IssueLabel string `json:"issueLabel"` + ID BoardLaneID `json:"id"` + Title string `json:"title"` + IssueLabel string `json:"issueLabel"` + CollectRemainingIssues bool `json:"collectRemainingIssues"` }