Kanban доска перенесена в react и mobx
This commit is contained in:
parent
e885128501
commit
6e2d8c25f9
15 changed files with 610 additions and 4 deletions
84
frontend/package-lock.json
generated
84
frontend/package-lock.json
generated
|
|
@ -15,7 +15,9 @@
|
||||||
"@types/node": "^16.18.14",
|
"@types/node": "^16.18.14",
|
||||||
"@types/react": "^18.0.28",
|
"@types/react": "^18.0.28",
|
||||||
"@types/react-dom": "^18.0.11",
|
"@types/react-dom": "^18.0.11",
|
||||||
|
"axios": "^1.4.0",
|
||||||
"mobx": "^6.9.0",
|
"mobx": "^6.9.0",
|
||||||
|
"mobx-react-lite": "^3.4.3",
|
||||||
"mobx-state-tree": "^5.1.8",
|
"mobx-state-tree": "^5.1.8",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
|
|
@ -4826,6 +4828,29 @@
|
||||||
"node": ">=4"
|
"node": ">=4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/axios": {
|
||||||
|
"version": "1.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/axios/-/axios-1.4.0.tgz",
|
||||||
|
"integrity": "sha512-S4XCWMEmzvo64T9GfvQDOXgYRDJ/wsSZc7Jvdgx5u1sd0JwsuPLqb3SYmusag+edF6ziyMensPVqLTSc1PiSEA==",
|
||||||
|
"dependencies": {
|
||||||
|
"follow-redirects": "^1.15.0",
|
||||||
|
"form-data": "^4.0.0",
|
||||||
|
"proxy-from-env": "^1.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/axios/node_modules/form-data": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
|
||||||
|
"dependencies": {
|
||||||
|
"asynckit": "^0.4.0",
|
||||||
|
"combined-stream": "^1.0.8",
|
||||||
|
"mime-types": "^2.1.12"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/axobject-query": {
|
"node_modules/axobject-query": {
|
||||||
"version": "3.1.1",
|
"version": "3.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-3.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-3.1.1.tgz",
|
||||||
|
|
@ -11869,6 +11894,27 @@
|
||||||
"url": "https://opencollective.com/mobx"
|
"url": "https://opencollective.com/mobx"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/mobx-react-lite": {
|
||||||
|
"version": "3.4.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/mobx-react-lite/-/mobx-react-lite-3.4.3.tgz",
|
||||||
|
"integrity": "sha512-NkJREyFTSUXR772Qaai51BnE1voWx56LOL80xG7qkZr6vo8vEaLF3sz1JNUVh+rxmUzxYaqOhfuxTfqUh0FXUg==",
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/mobx"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"mobx": "^6.1.0",
|
||||||
|
"react": "^16.8.0 || ^17 || ^18"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"react-dom": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"react-native": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/mobx-state-tree": {
|
"node_modules/mobx-state-tree": {
|
||||||
"version": "5.1.8",
|
"version": "5.1.8",
|
||||||
"resolved": "https://registry.npmjs.org/mobx-state-tree/-/mobx-state-tree-5.1.8.tgz",
|
"resolved": "https://registry.npmjs.org/mobx-state-tree/-/mobx-state-tree-5.1.8.tgz",
|
||||||
|
|
@ -13820,6 +13866,11 @@
|
||||||
"node": ">= 0.10"
|
"node": ">= 0.10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/proxy-from-env": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
|
||||||
|
},
|
||||||
"node_modules/psl": {
|
"node_modules/psl": {
|
||||||
"version": "1.9.0",
|
"version": "1.9.0",
|
||||||
"resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz",
|
"resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz",
|
||||||
|
|
@ -20369,6 +20420,28 @@
|
||||||
"resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.6.3.tgz",
|
"resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.6.3.tgz",
|
||||||
"integrity": "sha512-/BQzOX780JhsxDnPpH4ZiyrJAzcd8AfzFPkv+89veFSr1rcMjuq2JDCwypKaPeB6ljHp9KjXhPpjgCvQlWYuqg=="
|
"integrity": "sha512-/BQzOX780JhsxDnPpH4ZiyrJAzcd8AfzFPkv+89veFSr1rcMjuq2JDCwypKaPeB6ljHp9KjXhPpjgCvQlWYuqg=="
|
||||||
},
|
},
|
||||||
|
"axios": {
|
||||||
|
"version": "1.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/axios/-/axios-1.4.0.tgz",
|
||||||
|
"integrity": "sha512-S4XCWMEmzvo64T9GfvQDOXgYRDJ/wsSZc7Jvdgx5u1sd0JwsuPLqb3SYmusag+edF6ziyMensPVqLTSc1PiSEA==",
|
||||||
|
"requires": {
|
||||||
|
"follow-redirects": "^1.15.0",
|
||||||
|
"form-data": "^4.0.0",
|
||||||
|
"proxy-from-env": "^1.1.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"form-data": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
|
||||||
|
"requires": {
|
||||||
|
"asynckit": "^0.4.0",
|
||||||
|
"combined-stream": "^1.0.8",
|
||||||
|
"mime-types": "^2.1.12"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"axobject-query": {
|
"axobject-query": {
|
||||||
"version": "3.1.1",
|
"version": "3.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-3.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-3.1.1.tgz",
|
||||||
|
|
@ -25482,6 +25555,12 @@
|
||||||
"resolved": "https://registry.npmjs.org/mobx/-/mobx-6.9.0.tgz",
|
"resolved": "https://registry.npmjs.org/mobx/-/mobx-6.9.0.tgz",
|
||||||
"integrity": "sha512-HdKewQEREEJgsWnErClfbFoVebze6rGazxFLU/XUyrII8dORfVszN1V0BMRnQSzcgsNNtkX8DHj3nC6cdWE9YQ=="
|
"integrity": "sha512-HdKewQEREEJgsWnErClfbFoVebze6rGazxFLU/XUyrII8dORfVszN1V0BMRnQSzcgsNNtkX8DHj3nC6cdWE9YQ=="
|
||||||
},
|
},
|
||||||
|
"mobx-react-lite": {
|
||||||
|
"version": "3.4.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/mobx-react-lite/-/mobx-react-lite-3.4.3.tgz",
|
||||||
|
"integrity": "sha512-NkJREyFTSUXR772Qaai51BnE1voWx56LOL80xG7qkZr6vo8vEaLF3sz1JNUVh+rxmUzxYaqOhfuxTfqUh0FXUg==",
|
||||||
|
"requires": {}
|
||||||
|
},
|
||||||
"mobx-state-tree": {
|
"mobx-state-tree": {
|
||||||
"version": "5.1.8",
|
"version": "5.1.8",
|
||||||
"resolved": "https://registry.npmjs.org/mobx-state-tree/-/mobx-state-tree-5.1.8.tgz",
|
"resolved": "https://registry.npmjs.org/mobx-state-tree/-/mobx-state-tree-5.1.8.tgz",
|
||||||
|
|
@ -26702,6 +26781,11 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"proxy-from-env": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
|
||||||
|
},
|
||||||
"psl": {
|
"psl": {
|
||||||
"version": "1.9.0",
|
"version": "1.9.0",
|
||||||
"resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz",
|
"resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz",
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,9 @@
|
||||||
"@types/node": "^16.18.14",
|
"@types/node": "^16.18.14",
|
||||||
"@types/react": "^18.0.28",
|
"@types/react": "^18.0.28",
|
||||||
"@types/react-dom": "^18.0.11",
|
"@types/react-dom": "^18.0.11",
|
||||||
|
"axios": "^1.4.0",
|
||||||
"mobx": "^6.9.0",
|
"mobx": "^6.9.0",
|
||||||
|
"mobx-react-lite": "^3.4.3",
|
||||||
"mobx-state-tree": "^5.1.8",
|
"mobx-state-tree": "^5.1.8",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
|
|
|
||||||
11
frontend/src/kanban-board/column.module.css
Normal file
11
frontend/src/kanban-board/column.module.css
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
.kanbanColumn {
|
||||||
|
width: 200px;
|
||||||
|
background-color: rgb(225, 225, 225);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kanbanHeader {
|
||||||
|
text-align: center;
|
||||||
|
width: 200px;
|
||||||
|
}
|
||||||
27
frontend/src/kanban-board/column.tsx
Normal file
27
frontend/src/kanban-board/column.tsx
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
import React from 'react';
|
||||||
|
import ColumnCss from './column.module.css';
|
||||||
|
import * as Stores from './store';
|
||||||
|
import { observer } from 'mobx-react-lite';
|
||||||
|
import * as KanbanCard from './kanban-card';
|
||||||
|
|
||||||
|
export type Props = {
|
||||||
|
store: Stores.IColumnStore
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Column = observer((props: Props) => {
|
||||||
|
const cards = props.store.cards.map((card) => {
|
||||||
|
return (
|
||||||
|
<KanbanCard.KanbanCard store={card} key={card.issue.id}></KanbanCard.KanbanCard>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
return (
|
||||||
|
<div className={ColumnCss.kanbanColumn}>
|
||||||
|
<div className={ColumnCss.kanbanHeader}>
|
||||||
|
{props.store.status} ({props.store.count})
|
||||||
|
</div>
|
||||||
|
{cards}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
export default Column;
|
||||||
5
frontend/src/kanban-board/kanban-board.module.css
Normal file
5
frontend/src/kanban-board/kanban-board.module.css
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
.kanbanContainer {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
33
frontend/src/kanban-board/kanban-board.tsx
Normal file
33
frontend/src/kanban-board/kanban-board.tsx
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
import React from 'react';
|
||||||
|
import KanbanBoardCss from './kanban-board.module.css';
|
||||||
|
import { IBoardStore } from './store';
|
||||||
|
import { observer } from 'mobx-react-lite';
|
||||||
|
import Column from './column';
|
||||||
|
|
||||||
|
export type Props = {
|
||||||
|
store: IBoardStore
|
||||||
|
};
|
||||||
|
|
||||||
|
export const KanbanBoard = observer((props: Props) => {
|
||||||
|
let title: any;
|
||||||
|
if (props.store.metainfo.url) {
|
||||||
|
title = <a href={props.store.metainfo.url}>{props.store.metainfo.title}</a>;
|
||||||
|
} else {
|
||||||
|
title = <>{props.store.metainfo.title}</>;
|
||||||
|
}
|
||||||
|
const columns = [];
|
||||||
|
for (let i = 0; i < props.store.data.length; i++) {
|
||||||
|
const column = props.store.data[i];
|
||||||
|
columns.push(<Column store={column}/>)
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<h1 id={props.store.metainfo.title}>{title}</h1>
|
||||||
|
<div className={KanbanBoardCss.kanbanContainer}>
|
||||||
|
{columns}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
export default KanbanBoard;
|
||||||
22
frontend/src/kanban-board/kanban-boards-page.tsx
Normal file
22
frontend/src/kanban-board/kanban-boards-page.tsx
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
import React, { useEffect } from 'react';
|
||||||
|
import { useParams } from 'react-router-dom';
|
||||||
|
import * as Stores from './store';
|
||||||
|
import * as KBS from './kanban-boards';
|
||||||
|
|
||||||
|
export const KanbanBoardsPage = (): JSX.Element => {
|
||||||
|
const params = useParams();
|
||||||
|
const name = params.name as string;
|
||||||
|
const type = params.type as string;
|
||||||
|
|
||||||
|
// DEBUG: begin
|
||||||
|
console.debug(`KanbanBoardsPage: type=${type}; name=${name}`);
|
||||||
|
useEffect(() => {
|
||||||
|
console.debug(`KanbanBoardsPage: type=${type}; name=${name}`);
|
||||||
|
});
|
||||||
|
// DEBUG: end
|
||||||
|
|
||||||
|
const store = Stores.PageStore.create({loaded: false, type: type, name: name, data: null});
|
||||||
|
Stores.PageStoreLoadData(store);
|
||||||
|
|
||||||
|
return <KBS.KanbanBoards store={store}/>;
|
||||||
|
}
|
||||||
29
frontend/src/kanban-board/kanban-boards.tsx
Normal file
29
frontend/src/kanban-board/kanban-boards.tsx
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
import React from 'react';
|
||||||
|
import * as KB from './kanban-board';
|
||||||
|
import { IPageStore } from './store';
|
||||||
|
import { observer } from 'mobx-react-lite';
|
||||||
|
|
||||||
|
export type Props = {
|
||||||
|
store: IPageStore
|
||||||
|
}
|
||||||
|
|
||||||
|
export const KanbanBoards = observer((props: Props) => {
|
||||||
|
const data = props.store.data;
|
||||||
|
if (!props.store.loaded || !data) {
|
||||||
|
return <div>Loading...</div>
|
||||||
|
}
|
||||||
|
const list: any[] = [];
|
||||||
|
for (let i = 0; i < data.length; i++) {
|
||||||
|
const boardData = data[i];
|
||||||
|
const key = boardData.metainfo.title;
|
||||||
|
const board = <KB.KanbanBoard store={boardData} key={key} />;
|
||||||
|
list.push(board);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{list}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
export default KanbanBoards;
|
||||||
56
frontend/src/kanban-board/kanban-card.module.css
Normal file
56
frontend/src/kanban-board/kanban-card.module.css
Normal file
|
|
@ -0,0 +1,56 @@
|
||||||
|
.kanbanCard {
|
||||||
|
background-color: rgb(196, 196, 196);
|
||||||
|
border-width: 1px;
|
||||||
|
border-color: rgba(255, 255, 255, 0.2) rgba(96, 96, 96, 0.2) rgba(96, 96, 96, 0.2) rgba(255, 255, 255, 0.2);
|
||||||
|
/*border-color: black;*/
|
||||||
|
border-style: solid;
|
||||||
|
margin: 2px;
|
||||||
|
padding: 3px;
|
||||||
|
width: 190px;
|
||||||
|
/*display: flex;*/
|
||||||
|
box-shadow: 0 0 3px rgba(0, 0, 0, 0.5);
|
||||||
|
border-radius: 3px;
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kanbanCard div {
|
||||||
|
font-size: small;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kanbanCard .kanbanCardTitle {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timepassedDot {
|
||||||
|
height: 10px;
|
||||||
|
width: 10px;
|
||||||
|
background-color: #bbb;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timepassedDot.hot {
|
||||||
|
background-color: red;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timepassedDot.warm {
|
||||||
|
background-color: orange;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timepassedDot.comfort {
|
||||||
|
background-color: rgba(255, 255, 0, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.timepassedDot.breezy {
|
||||||
|
background-color: rgba(0, 255, 0, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.timepassedDot.cold {
|
||||||
|
background-color: rgba(0, 0, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.kanbanCardTag {
|
||||||
|
font-size: 8pt;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 2px;
|
||||||
|
}
|
||||||
67
frontend/src/kanban-board/kanban-card.tsx
Normal file
67
frontend/src/kanban-board/kanban-card.tsx
Normal file
|
|
@ -0,0 +1,67 @@
|
||||||
|
import { observer } from 'mobx-react-lite';
|
||||||
|
import KanbanCardCss from './kanban-card.module.css';
|
||||||
|
import React from 'react';
|
||||||
|
import { ICardStore } from './store';
|
||||||
|
import { getStyleObjectFromString } from '../utils/style';
|
||||||
|
|
||||||
|
export type Props = {
|
||||||
|
store: ICardStore
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TagProps = {
|
||||||
|
style?: string;
|
||||||
|
tag: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const KanbanCardTag = (props: TagProps): JSX.Element => {
|
||||||
|
const inlineStyle = getStyleObjectFromString(props.style || '');
|
||||||
|
return (
|
||||||
|
<span className={KanbanCardCss.kanbanCardTag} style={inlineStyle}>
|
||||||
|
{props.tag}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Какие дальше требования к карточкам?
|
||||||
|
*
|
||||||
|
* 1. Отобразить как было в статичной доске
|
||||||
|
* 2. Переделать отображение с учётом store.params
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const KanbanCard = observer((props: Props) => {
|
||||||
|
let timePassedColorClassName = '';
|
||||||
|
const timePassedIssueValue: string = props.store.issue.timePassedClass;
|
||||||
|
if (timePassedIssueValue && KanbanCardCss[timePassedIssueValue]) {
|
||||||
|
timePassedColorClassName = KanbanCardCss[timePassedIssueValue];
|
||||||
|
}
|
||||||
|
const timePassedClassName = `${KanbanCardCss.timepassedDot} ${timePassedColorClassName}`;
|
||||||
|
let tagsSection = <></>;
|
||||||
|
const tagsParams = props.store.params?.fields.find((field) => {
|
||||||
|
return field.component === 'tags';
|
||||||
|
});
|
||||||
|
if (tagsParams && props.store.issue[tagsParams.path]) {
|
||||||
|
const tags = props.store.issue[tagsParams.path] as TagProps[];
|
||||||
|
tagsSection = (
|
||||||
|
<div>
|
||||||
|
{tagsParams.label || 'Tags'}: {tags.map(tag => <KanbanCardTag tag={tag.tag} style={tag.style} />)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div className={KanbanCardCss.kanbanCard}>
|
||||||
|
<div className={KanbanCardCss.kanbanCardTitle}>
|
||||||
|
<span className={timePassedClassName}></span>
|
||||||
|
<a href={props.store.issue.url.url}>{props.store.issue.tracker.name} #{props.store.issue.id} - {props.store.issue.subject}</a>
|
||||||
|
<div>Исп.: {props.store.issue.current_user.name}</div>
|
||||||
|
<div>Прио.: {props.store.issue.priority.name}</div>
|
||||||
|
<div>Версия: {props.store.issue.fixed_version?.name || ''}</div>
|
||||||
|
<div>Прогресс: {props.store.issue.done_ratio}</div>
|
||||||
|
<div>Трудозатраты: {props.store.issue.total_spent_hours} / {props.store.issue.total_estimated_hours}</div>
|
||||||
|
{tagsSection}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
export default KanbanCard;
|
||||||
101
frontend/src/kanban-board/store.ts
Normal file
101
frontend/src/kanban-board/store.ts
Normal file
|
|
@ -0,0 +1,101 @@
|
||||||
|
import { Instance, types } from 'mobx-state-tree';
|
||||||
|
import { RedmineTypes } from '../redmine-types';
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
export const IssueStore = types.frozen<RedmineTypes.ExtendedIssue>()
|
||||||
|
|
||||||
|
export interface IIssueStore extends Instance<typeof IssueStore> {}
|
||||||
|
|
||||||
|
export const ColumnStore = types.model({
|
||||||
|
status: '',
|
||||||
|
count: 0,
|
||||||
|
issues: types.array(IssueStore)
|
||||||
|
}).views((self) => {
|
||||||
|
return {
|
||||||
|
get cards(): ICardStore[] {
|
||||||
|
return self.issues.map(issue => {
|
||||||
|
return {
|
||||||
|
issue: issue
|
||||||
|
} as ICardStore;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export interface IColumnStore extends Instance<typeof ColumnStore> {}
|
||||||
|
|
||||||
|
export const MetaInfoStore = types.model({
|
||||||
|
title: '',
|
||||||
|
url: types.maybe(types.string),
|
||||||
|
rootIssue: types.model({
|
||||||
|
id: 0,
|
||||||
|
tracker: types.model({
|
||||||
|
id: 0,
|
||||||
|
name: ''
|
||||||
|
}),
|
||||||
|
subject: ''
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
export interface IMetaInfoStore extends Instance<typeof MetaInfoStore> {}
|
||||||
|
|
||||||
|
export const BoardStore = types.model({
|
||||||
|
data: types.array(ColumnStore),
|
||||||
|
metainfo: MetaInfoStore
|
||||||
|
});
|
||||||
|
|
||||||
|
export interface IBoardStore extends Instance<typeof BoardStore> {}
|
||||||
|
|
||||||
|
export const PageStore = types.model({
|
||||||
|
loaded: false,
|
||||||
|
type: '',
|
||||||
|
name: '',
|
||||||
|
data: types.maybeNull(
|
||||||
|
types.array(BoardStore)
|
||||||
|
)
|
||||||
|
}).actions(self => {
|
||||||
|
return {
|
||||||
|
setData: (data: any) => {
|
||||||
|
self.data = data;
|
||||||
|
self.loaded = true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
export async function PageStoreLoadData(store: IPageStore): Promise<void> {
|
||||||
|
const url = `/simple-kanban-board/${store.type}/${store.name}/raw`;
|
||||||
|
const resp = await axios.get(url);
|
||||||
|
if (!(resp?.data)) return;
|
||||||
|
store.setData(resp.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IPageStore extends Instance<typeof PageStore> { }
|
||||||
|
|
||||||
|
export type CardField = {
|
||||||
|
component: string;
|
||||||
|
} & Record<string, any>;
|
||||||
|
|
||||||
|
export const CardSettings = types.model({});
|
||||||
|
|
||||||
|
export const CardStore = types.model({
|
||||||
|
issue: IssueStore,
|
||||||
|
params: types.maybe(types.model({
|
||||||
|
fields: types.optional(
|
||||||
|
types.array(
|
||||||
|
types.frozen<CardField>()
|
||||||
|
),
|
||||||
|
[
|
||||||
|
{ component: 'text', label: 'Исп.', path: 'current_user.name' },
|
||||||
|
{ component: 'text', label: 'Прио.', path: 'priority.name' },
|
||||||
|
{ component: 'text', label: 'Версия', path: 'fixed_version.name' },
|
||||||
|
{ component: 'text', label: 'Прогресс', path: 'done_ratio' },
|
||||||
|
{ component: 'labor_costs' },
|
||||||
|
{ component: 'tags', label: 'Tags', path: 'styledTags' }
|
||||||
|
]
|
||||||
|
),
|
||||||
|
autoCollapse: types.boolean
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
|
||||||
|
export interface ICardStore extends Instance<typeof CardStore> {}
|
||||||
|
|
||||||
140
frontend/src/redmine-types.ts
Normal file
140
frontend/src/redmine-types.ts
Normal file
|
|
@ -0,0 +1,140 @@
|
||||||
|
// eslint-disable-next-line @typescript-eslint/prefer-namespace-keyword, @typescript-eslint/no-namespace
|
||||||
|
export module RedmineTypes {
|
||||||
|
export type IdAndName = {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type CustomField = {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
value: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type JournalDetail = {
|
||||||
|
property: string;
|
||||||
|
name: string;
|
||||||
|
old_value?: string;
|
||||||
|
new_value?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Journal = {
|
||||||
|
id: number;
|
||||||
|
user: IdAndName;
|
||||||
|
notes?: string;
|
||||||
|
created_on: string;
|
||||||
|
details?: JournalDetail[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ChildIssue = {
|
||||||
|
id: number;
|
||||||
|
tracker: IdAndName;
|
||||||
|
subject: string;
|
||||||
|
children?: Children;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Children = ChildIssue[];
|
||||||
|
|
||||||
|
export type Issue = {
|
||||||
|
id: number;
|
||||||
|
project: IdAndName;
|
||||||
|
tracker: IdAndName;
|
||||||
|
status: IdAndName;
|
||||||
|
priority: IdAndName;
|
||||||
|
author: IdAndName;
|
||||||
|
assigned_to?: IdAndName;
|
||||||
|
category: IdAndName;
|
||||||
|
fixed_version?: IdAndName;
|
||||||
|
subject: string;
|
||||||
|
description: string;
|
||||||
|
start_date: string;
|
||||||
|
done_ratio: number;
|
||||||
|
spent_hours: number;
|
||||||
|
total_spent_hours: number;
|
||||||
|
custom_fields: CustomField[];
|
||||||
|
created_on: string;
|
||||||
|
updated_on?: string;
|
||||||
|
closed_on?: string;
|
||||||
|
relations?: Record<string, any>[];
|
||||||
|
journals?: Journal[];
|
||||||
|
children?: Children;
|
||||||
|
parent?: { id: number };
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ExtendedIssue = Issue & Record<string, any>;
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/prefer-namespace-keyword, @typescript-eslint/no-namespace
|
||||||
|
export module Unknown {
|
||||||
|
export const num = -1;
|
||||||
|
export const str = '';
|
||||||
|
export const idAndName: IdAndName = {
|
||||||
|
id: -1,
|
||||||
|
name: str,
|
||||||
|
};
|
||||||
|
export const unknownName = 'Unknown';
|
||||||
|
export const subject = 'Unknown';
|
||||||
|
export const date = '1970-01-01T00:00:00Z';
|
||||||
|
export const issue: Issue = {
|
||||||
|
id: num,
|
||||||
|
project: idAndName,
|
||||||
|
tracker: idAndName,
|
||||||
|
status: idAndName,
|
||||||
|
priority: idAndName,
|
||||||
|
author: idAndName,
|
||||||
|
category: idAndName,
|
||||||
|
fixed_version: idAndName,
|
||||||
|
subject: subject,
|
||||||
|
description: str,
|
||||||
|
start_date: date,
|
||||||
|
done_ratio: num,
|
||||||
|
spent_hours: num,
|
||||||
|
total_spent_hours: num,
|
||||||
|
custom_fields: [],
|
||||||
|
created_on: date,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const user: User = {
|
||||||
|
id: num,
|
||||||
|
login: str,
|
||||||
|
firstname: unknownName,
|
||||||
|
lastname: unknownName,
|
||||||
|
mail: str,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export type User = {
|
||||||
|
id: number;
|
||||||
|
login: string;
|
||||||
|
firstname: string;
|
||||||
|
lastname: string;
|
||||||
|
mail: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type PublicUser = {
|
||||||
|
id: number;
|
||||||
|
firstname: string;
|
||||||
|
lastname: string;
|
||||||
|
login: string;
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function CreatePublicUserFromUser(obj: User): PublicUser {
|
||||||
|
return {
|
||||||
|
id: obj.id,
|
||||||
|
login: obj.login,
|
||||||
|
firstname: obj.firstname,
|
||||||
|
lastname: obj.lastname,
|
||||||
|
name: `${obj.firstname} ${obj.lastname}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CreateUser(obj: User): User {
|
||||||
|
return {
|
||||||
|
id: obj.id,
|
||||||
|
login: obj.login,
|
||||||
|
firstname: obj.firstname,
|
||||||
|
lastname: obj.lastname,
|
||||||
|
mail: obj.mail,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2,12 +2,17 @@ import React from "react";
|
||||||
import { createBrowserRouter } from "react-router-dom";
|
import { createBrowserRouter } from "react-router-dom";
|
||||||
import StartPage from "./start-page/start-page";
|
import StartPage from "./start-page/start-page";
|
||||||
import UnknownPage from "./unknown-page";
|
import UnknownPage from "./unknown-page";
|
||||||
|
import { KanbanBoardsPage } from "./kanban-board/kanban-boards-page";
|
||||||
|
|
||||||
export const router = createBrowserRouter([
|
export const router = createBrowserRouter([
|
||||||
{
|
{
|
||||||
path: "/",
|
path: "/",
|
||||||
element: (<StartPage/>),
|
element: (<StartPage/>),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "/kanban-board/:type/:name",
|
||||||
|
element: (<KanbanBoardsPage/>)
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "*",
|
path: "*",
|
||||||
element: (<UnknownPage/>)
|
element: (<UnknownPage/>)
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@
|
||||||
border: 0;
|
border: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
h1 {
|
.cover h1 {
|
||||||
left: 0%;
|
left: 0%;
|
||||||
right: 0%;
|
right: 0%;
|
||||||
top: 18.22%;
|
top: 18.22%;
|
||||||
|
|
@ -23,7 +23,7 @@ h1 {
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
}
|
}
|
||||||
|
|
||||||
h3 {
|
.cover h3 {
|
||||||
color: #F5B14E;
|
color: #F5B14E;
|
||||||
font-family: 'Source Code Pro';
|
font-family: 'Source Code Pro';
|
||||||
letter-spacing: 0.12em;
|
letter-spacing: 0.12em;
|
||||||
|
|
@ -33,7 +33,7 @@ h3 {
|
||||||
line-height: 34px;
|
line-height: 34px;
|
||||||
}
|
}
|
||||||
|
|
||||||
h4 {
|
.cover h4 {
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-size: 40px;
|
font-size: 40px;
|
||||||
|
|
@ -43,7 +43,7 @@ h4 {
|
||||||
font-feature-settings: 'pnum' on, 'lnum' on;
|
font-feature-settings: 'pnum' on, 'lnum' on;
|
||||||
}
|
}
|
||||||
|
|
||||||
h4:hover {
|
.cover h4:hover {
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
color: #F5B14E;
|
color: #F5B14E;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
24
frontend/src/utils/style.ts
Normal file
24
frontend/src/utils/style.ts
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
const formatStringToCamelCase = (str: string): string => {
|
||||||
|
const splitted = str.split("-");
|
||||||
|
if (splitted.length === 1) return splitted[0];
|
||||||
|
return (
|
||||||
|
splitted[0] +
|
||||||
|
splitted
|
||||||
|
.slice(1)
|
||||||
|
.map(word => word[0].toUpperCase() + word.slice(1))
|
||||||
|
.join("")
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getStyleObjectFromString = (str: string): Record<string, string> => {
|
||||||
|
const style = {} as Record<string, string>;
|
||||||
|
str.split(";").forEach(el => {
|
||||||
|
const [property, value] = el.split(":");
|
||||||
|
if (!property) return;
|
||||||
|
|
||||||
|
const formattedProperty = formatStringToCamelCase(property.trim());
|
||||||
|
style[formattedProperty] = value.trim();
|
||||||
|
});
|
||||||
|
|
||||||
|
return style;
|
||||||
|
};
|
||||||
Loading…
Reference in a new issue