Добавлен список задач по тегам
This commit is contained in:
parent
ad61df57b9
commit
d60a082327
16 changed files with 344 additions and 16 deletions
|
|
@ -0,0 +1,6 @@
|
|||
.listContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
background-color: rgb(225, 225, 225);
|
||||
}
|
||||
25
frontend/src/issues-list-board/issues-list-board.tsx
Normal file
25
frontend/src/issues-list-board/issues-list-board.tsx
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
import { observer } from 'mobx-react-lite';
|
||||
import React from 'react';
|
||||
import { IBoardStore } from './store';
|
||||
import Css from './issues-list-board.module.css';
|
||||
import * as IssuesListCardNs from './issues-list-card';
|
||||
|
||||
export type Props = {
|
||||
store: IBoardStore
|
||||
}
|
||||
|
||||
export const IssuesListBoard = observer((props: Props): JSX.Element => {
|
||||
const list: JSX.Element[] = props.store.data.map((issue) => {
|
||||
return (
|
||||
<IssuesListCardNs.IssuesListCard store={issue} key={issue.id}/>
|
||||
);
|
||||
});
|
||||
return (
|
||||
<>
|
||||
<h1 id={props.store.metainfo.title}>{props.store.metainfo.title} <a href={`#${props.store.metainfo.title}`}>#</a></h1>
|
||||
<div className={Css.listContainer}>
|
||||
{list}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
});
|
||||
24
frontend/src/issues-list-board/issues-list-boards-page.tsx
Normal file
24
frontend/src/issues-list-board/issues-list-boards-page.tsx
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
import React, { useEffect } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import * as IssuesListStoreNs from './store';
|
||||
import * as IssuesListBoardsNs from './issues-list-boards';
|
||||
|
||||
export const IssuesListBoardPage = (): JSX.Element => {
|
||||
const params = useParams();
|
||||
const name = params.name as string;
|
||||
const type = params.type as string;
|
||||
|
||||
// DEBUG: begin
|
||||
console.debug(`Issues list page: type=${type}; name=${name}`);
|
||||
useEffect(() => {
|
||||
console.debug(`Issues list page: type=${type}; name=${name}`);
|
||||
});
|
||||
// DEBUG: end
|
||||
|
||||
const store = IssuesListStoreNs.PageStore.create({loaded: false, type: type, name: name});
|
||||
IssuesListStoreNs.PageStoreLoadData(store);
|
||||
|
||||
return (
|
||||
<IssuesListBoardsNs.IssuesListBoards store={store}/>
|
||||
);
|
||||
};
|
||||
23
frontend/src/issues-list-board/issues-list-boards.tsx
Normal file
23
frontend/src/issues-list-board/issues-list-boards.tsx
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
import { observer } from 'mobx-react-lite';
|
||||
import React from 'react';
|
||||
import * as IssuesListBoardStore from './store';
|
||||
import * as IssuesListBoardNs from './issues-list-board';
|
||||
|
||||
export type Props = {
|
||||
store: IssuesListBoardStore.IPageStore
|
||||
};
|
||||
|
||||
export const IssuesListBoards = observer((props: Props): JSX.Element => {
|
||||
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 = <IssuesListBoardNs.IssuesListBoard store={boardData} key={key}/>
|
||||
list.push(board);
|
||||
}
|
||||
return <>{list}</>;
|
||||
});
|
||||
14
frontend/src/issues-list-board/issues-list-card.module.css
Normal file
14
frontend/src/issues-list-board/issues-list-card.module.css
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
.listItem {
|
||||
margin: 5px;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.issueSubject {}
|
||||
|
||||
.issueStatus {}
|
||||
|
||||
.issueTime {}
|
||||
|
||||
.tagsContainer {}
|
||||
34
frontend/src/issues-list-board/issues-list-card.tsx
Normal file
34
frontend/src/issues-list-board/issues-list-card.tsx
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
import { observer } from 'mobx-react-lite';
|
||||
import React from 'react';
|
||||
import { IIssueStore } from './store';
|
||||
import Css from './issues-list-card.module.css';
|
||||
import * as TimePassedNs from '../misc-components/time-passed';
|
||||
import * as TagsNs from '../misc-components/tags';
|
||||
import * as IssueHrefNs from '../misc-components/issue-href';
|
||||
|
||||
export type Props = {
|
||||
store: IIssueStore
|
||||
};
|
||||
|
||||
export const IssuesListCard = observer((props: Props): JSX.Element => {
|
||||
return (
|
||||
<div className={Css.listItem}>
|
||||
<div>
|
||||
<TimePassedNs.TimePassed params={{ fromIssue: { issue: props.store, keyName: 'timePassedClass' } }}/>
|
||||
<span className={Css.issueSubject}>
|
||||
<IssueHrefNs.IssueHref
|
||||
url={props.store.url?.url || ''}
|
||||
subject={props.store.subject}
|
||||
tracker={props.store.tracker?.name || ''}
|
||||
id={props.store.id}
|
||||
/>
|
||||
</span>
|
||||
<span className={Css.issueStatus}>| {props.store.status.name}</span>
|
||||
<span className={Css.issueTime}>| {props.store.total_spent_hours} / {props.store.total_estimated_hours}</span>
|
||||
</div>
|
||||
<div className={Css.tagsContainer}>
|
||||
<TagsNs.Tags params={{ tags: props.store.styledTags }}/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
64
frontend/src/issues-list-board/store.ts
Normal file
64
frontend/src/issues-list-board/store.ts
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
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 MetaInfoStore = types.model({
|
||||
title: types.string,
|
||||
url: types.maybe(types.string),
|
||||
rootIssue: types.maybe(types.model({
|
||||
id: 0,
|
||||
tracker: types.model({
|
||||
id: 0,
|
||||
name: ''
|
||||
}),
|
||||
subject: ''
|
||||
}))
|
||||
});
|
||||
|
||||
export const BoardStore = types.model({
|
||||
data: types.array(IssueStore),
|
||||
metainfo: MetaInfoStore
|
||||
});
|
||||
|
||||
export interface IBoardStore extends Instance<typeof BoardStore> {}
|
||||
|
||||
export const PageStore = types.model({
|
||||
loaded: types.boolean,
|
||||
type: types.string,
|
||||
name: types.string,
|
||||
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-issues-list/${store.type}/${store.name}/raw`;
|
||||
const resp = await axios.get(url);
|
||||
if (!(resp?.data)) return;
|
||||
|
||||
const data = [];
|
||||
for (let i = 0; i < resp.data.length; i++) {
|
||||
const item = resp.data[i] as {data: any[], metainfo: Record<string, any>};
|
||||
data.push({
|
||||
metainfo: item.metainfo,
|
||||
data: item.data.map((group: {status: string, count: number, issues: any[]}) => {
|
||||
return group.issues
|
||||
}).flat()
|
||||
});
|
||||
}
|
||||
|
||||
store.setData(data);
|
||||
}
|
||||
|
||||
export interface IPageStore extends Instance<typeof PageStore> {}
|
||||
|
|
@ -3,6 +3,8 @@ import KanbanCardCss from './kanban-card.module.css';
|
|||
import React from 'react';
|
||||
import { ICardStore } from './store';
|
||||
import { getStyleObjectFromString } from '../utils/style';
|
||||
import * as TimePassedNs from '../misc-components/time-passed';
|
||||
import * as TagsNs from '../misc-components/tags';
|
||||
|
||||
export type Props = {
|
||||
store: ICardStore
|
||||
|
|
@ -30,12 +32,6 @@ export const KanbanCardTag = (props: TagProps): JSX.Element => {
|
|||
*/
|
||||
|
||||
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';
|
||||
|
|
@ -45,16 +41,18 @@ export const KanbanCard = observer((props: Props) => {
|
|||
if (tagsParams && props.store.issue[tagsParams.path]) {
|
||||
const tags = props.store.issue[tagsParams.path] as TagProps[];
|
||||
console.debug(`Tags:`, tags); // DEBUG
|
||||
tagsSection = (
|
||||
<div>
|
||||
{tagsParams.label || 'Tags'}: {tags.map(tag => <KanbanCardTag tag={tag.tag} style={tag.style} />)}
|
||||
</div>
|
||||
);
|
||||
tagsSection = <TagsNs.Tags params={{tags: tags}}/>
|
||||
}
|
||||
const timePassedParams: TimePassedNs.Params = {
|
||||
fromIssue: {
|
||||
issue: props.store.issue,
|
||||
keyName: 'timePassedClass'
|
||||
}
|
||||
}
|
||||
return (
|
||||
<div className={KanbanCardCss.kanbanCard}>
|
||||
<div className={KanbanCardCss.kanbanCardTitle}>
|
||||
<span className={timePassedClassName}></span>
|
||||
<TimePassedNs.TimePassed params={timePassedParams}/>
|
||||
<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>
|
||||
|
|
|
|||
|
|
@ -27,14 +27,14 @@ export interface IColumnStore extends Instance<typeof ColumnStore> {}
|
|||
export const MetaInfoStore = types.model({
|
||||
title: '',
|
||||
url: types.maybe(types.string),
|
||||
rootIssue: types.model({
|
||||
rootIssue: types.maybe(types.model({
|
||||
id: 0,
|
||||
tracker: types.model({
|
||||
id: 0,
|
||||
name: ''
|
||||
}),
|
||||
subject: ''
|
||||
})
|
||||
}))
|
||||
});
|
||||
|
||||
export interface IMetaInfoStore extends Instance<typeof MetaInfoStore> {}
|
||||
|
|
@ -75,8 +75,6 @@ export type CardField = {
|
|||
component: string;
|
||||
} & Record<string, any>;
|
||||
|
||||
export const CardSettings = types.model({});
|
||||
|
||||
export const CardParamsStore = types.optional(
|
||||
types.model({
|
||||
fields: types.array(
|
||||
|
|
|
|||
14
frontend/src/misc-components/issue-href.tsx
Normal file
14
frontend/src/misc-components/issue-href.tsx
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
import React from 'react';
|
||||
|
||||
export type Props = {
|
||||
url: string;
|
||||
id: number;
|
||||
subject: string;
|
||||
tracker: string;
|
||||
};
|
||||
|
||||
export const IssueHref = (props: Props): JSX.Element => {
|
||||
return (
|
||||
<a href={props.url}>{props.tracker} #{props.id} - {props.subject}</a>
|
||||
);
|
||||
};
|
||||
5
frontend/src/misc-components/tag.module.css
Normal file
5
frontend/src/misc-components/tag.module.css
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
.tag {
|
||||
font-size: 8pt;
|
||||
border-radius: 4px;
|
||||
padding: 2px;
|
||||
}
|
||||
17
frontend/src/misc-components/tag.tsx
Normal file
17
frontend/src/misc-components/tag.tsx
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
import React from 'react';
|
||||
import { getStyleObjectFromString } from '../utils/style';
|
||||
import Css from './tag.module.css';
|
||||
|
||||
export type Props = {
|
||||
style?: string;
|
||||
tag: string;
|
||||
};
|
||||
|
||||
export const Tag = (props: Props): JSX.Element => {
|
||||
const inlineStyle = getStyleObjectFromString(props.style || '');
|
||||
return (
|
||||
<span className={Css.tag} style={inlineStyle}>
|
||||
{props.tag}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
26
frontend/src/misc-components/tags.tsx
Normal file
26
frontend/src/misc-components/tags.tsx
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
import React from 'react';
|
||||
import * as TagNs from './tag';
|
||||
|
||||
export type Params = {
|
||||
label?: string;
|
||||
tags: TagNs.Props[];
|
||||
};
|
||||
|
||||
export type Props = {
|
||||
params: Params
|
||||
};
|
||||
|
||||
export const Tags = (props: Props): JSX.Element => {
|
||||
if (!props.params.tags) {
|
||||
return (<></>);
|
||||
}
|
||||
const label = props.params.label || 'Tags';
|
||||
const tags = props.params.tags.map((tag) => {
|
||||
return <TagNs.Tag tag={tag.tag} style={tag.style} key={tag.tag}/>;
|
||||
}) || [];
|
||||
return (
|
||||
<div>
|
||||
{label}: {tags}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
27
frontend/src/misc-components/time-passed.module.css
Normal file
27
frontend/src/misc-components/time-passed.module.css
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
.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);
|
||||
}
|
||||
48
frontend/src/misc-components/time-passed.tsx
Normal file
48
frontend/src/misc-components/time-passed.tsx
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
import React from 'react';
|
||||
import Css from './time-passed.module.css';
|
||||
import { RedmineTypes } from '../redmine-types';
|
||||
|
||||
export type Params = {
|
||||
fromIssue?: {
|
||||
issue: RedmineTypes.ExtendedIssue,
|
||||
keyName: string,
|
||||
},
|
||||
fromValue?: string
|
||||
};
|
||||
|
||||
export type Props = {
|
||||
params: Params
|
||||
};
|
||||
|
||||
export const TimePassed = (props: Props): JSX.Element => {
|
||||
if (!props.params.fromIssue && !props.params.fromValue) {
|
||||
return <></>;
|
||||
}
|
||||
let timePassedClassName = ''; // TODO
|
||||
if (props.params.fromIssue) {
|
||||
const { issue, keyName } = props.params.fromIssue;
|
||||
timePassedClassName = `${Css.timepassedDot} ${getClassName(issue[keyName])}`;
|
||||
} else if (props.params.fromValue) {
|
||||
timePassedClassName = `${Css.timepassedDot} ${getClassName(props.params.fromValue)}`;
|
||||
}
|
||||
return (
|
||||
<span className={timePassedClassName}></span>
|
||||
);
|
||||
};
|
||||
|
||||
function getClassName(value: string): string {
|
||||
switch (value) {
|
||||
case 'hot':
|
||||
return Css.hot;
|
||||
case 'warm':
|
||||
return Css.warm;
|
||||
case 'comfort':
|
||||
return Css.comfort;
|
||||
case 'breezy':
|
||||
return Css.breezy;
|
||||
case 'cold':
|
||||
return Css.cold;
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@ 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";
|
||||
import { IssuesListBoardPage } from "./issues-list-board/issues-list-boards-page";
|
||||
|
||||
export const router = createBrowserRouter([
|
||||
{
|
||||
|
|
@ -13,6 +14,10 @@ export const router = createBrowserRouter([
|
|||
path: "/kanban-board/:type/:name",
|
||||
element: (<KanbanBoardsPage/>)
|
||||
},
|
||||
{
|
||||
path: "/issues-list-board/:type/:name",
|
||||
element: (<IssuesListBoardPage/>)
|
||||
},
|
||||
{
|
||||
path: "*",
|
||||
element: (<UnknownPage/>)
|
||||
|
|
|
|||
Loading…
Reference in a new issue