Добавлен список задач по тегам

This commit is contained in:
Pavel Gnedov 2023-06-19 15:29:36 +07:00
parent ad61df57b9
commit d60a082327
16 changed files with 344 additions and 16 deletions

View file

@ -0,0 +1,6 @@
.listContainer {
display: flex;
flex-direction: column;
width: 100%;
background-color: rgb(225, 225, 225);
}

View 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>
</>
);
});

View 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}/>
);
};

View 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}</>;
});

View file

@ -0,0 +1,14 @@
.listItem {
margin: 5px;
width: 100%;
display: flex;
flex-direction: column;
}
.issueSubject {}
.issueStatus {}
.issueTime {}
.tagsContainer {}

View 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>
);
});

View 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> {}

View file

@ -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>

View file

@ -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(

View 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>
);
};

View file

@ -0,0 +1,5 @@
.tag {
font-size: 8pt;
border-radius: 4px;
padding: 2px;
}

View 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>
);
}

View 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>
);
}

View 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);
}

View 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 '';
}
}

View file

@ -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/>)