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/react": "^18.0.28",
|
||||
"@types/react-dom": "^18.0.11",
|
||||
"axios": "^1.4.0",
|
||||
"mobx": "^6.9.0",
|
||||
"mobx-react-lite": "^3.4.3",
|
||||
"mobx-state-tree": "^5.1.8",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
|
|
@ -4826,6 +4828,29 @@
|
|||
"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": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-3.1.1.tgz",
|
||||
|
|
@ -11869,6 +11894,27 @@
|
|||
"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": {
|
||||
"version": "5.1.8",
|
||||
"resolved": "https://registry.npmjs.org/mobx-state-tree/-/mobx-state-tree-5.1.8.tgz",
|
||||
|
|
@ -13820,6 +13866,11 @@
|
|||
"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": {
|
||||
"version": "1.9.0",
|
||||
"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",
|
||||
"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": {
|
||||
"version": "3.1.1",
|
||||
"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",
|
||||
"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": {
|
||||
"version": "5.1.8",
|
||||
"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": {
|
||||
"version": "1.9.0",
|
||||
"resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz",
|
||||
|
|
|
|||
|
|
@ -10,7 +10,9 @@
|
|||
"@types/node": "^16.18.14",
|
||||
"@types/react": "^18.0.28",
|
||||
"@types/react-dom": "^18.0.11",
|
||||
"axios": "^1.4.0",
|
||||
"mobx": "^6.9.0",
|
||||
"mobx-react-lite": "^3.4.3",
|
||||
"mobx-state-tree": "^5.1.8",
|
||||
"react": "^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 StartPage from "./start-page/start-page";
|
||||
import UnknownPage from "./unknown-page";
|
||||
import { KanbanBoardsPage } from "./kanban-board/kanban-boards-page";
|
||||
|
||||
export const router = createBrowserRouter([
|
||||
{
|
||||
path: "/",
|
||||
element: (<StartPage/>),
|
||||
},
|
||||
{
|
||||
path: "/kanban-board/:type/:name",
|
||||
element: (<KanbanBoardsPage/>)
|
||||
},
|
||||
{
|
||||
path: "*",
|
||||
element: (<UnknownPage/>)
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@
|
|||
border: 0;
|
||||
}
|
||||
|
||||
h1 {
|
||||
.cover h1 {
|
||||
left: 0%;
|
||||
right: 0%;
|
||||
top: 18.22%;
|
||||
|
|
@ -23,7 +23,7 @@ h1 {
|
|||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
h3 {
|
||||
.cover h3 {
|
||||
color: #F5B14E;
|
||||
font-family: 'Source Code Pro';
|
||||
letter-spacing: 0.12em;
|
||||
|
|
@ -33,7 +33,7 @@ h3 {
|
|||
line-height: 34px;
|
||||
}
|
||||
|
||||
h4 {
|
||||
.cover h4 {
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
font-size: 40px;
|
||||
|
|
@ -43,7 +43,7 @@ h4 {
|
|||
font-feature-settings: 'pnum' on, 'lnum' on;
|
||||
}
|
||||
|
||||
h4:hover {
|
||||
.cover h4:hover {
|
||||
text-decoration: underline;
|
||||
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