diff --git a/frontend/src/issues-list-board/issues-list-board.module.css b/frontend/src/issues-list-board/issues-list-board.module.css new file mode 100644 index 0000000..04006f5 --- /dev/null +++ b/frontend/src/issues-list-board/issues-list-board.module.css @@ -0,0 +1,6 @@ +.listContainer { + display: flex; + flex-direction: column; + width: 100%; + background-color: rgb(225, 225, 225); +} \ No newline at end of file diff --git a/frontend/src/issues-list-board/issues-list-board.tsx b/frontend/src/issues-list-board/issues-list-board.tsx new file mode 100644 index 0000000..f243f8b --- /dev/null +++ b/frontend/src/issues-list-board/issues-list-board.tsx @@ -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 ( + + ); + }); + return ( + <> +

{props.store.metainfo.title} #

+
+ {list} +
+ + ); +}); \ No newline at end of file diff --git a/frontend/src/issues-list-board/issues-list-boards-page.tsx b/frontend/src/issues-list-board/issues-list-boards-page.tsx new file mode 100644 index 0000000..531ad27 --- /dev/null +++ b/frontend/src/issues-list-board/issues-list-boards-page.tsx @@ -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 ( + + ); +}; \ No newline at end of file diff --git a/frontend/src/issues-list-board/issues-list-boards.tsx b/frontend/src/issues-list-board/issues-list-boards.tsx new file mode 100644 index 0000000..82af229 --- /dev/null +++ b/frontend/src/issues-list-board/issues-list-boards.tsx @@ -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
Loading...
+ } + const list: any[] = []; + for (let i = 0; i < data.length; i++) { + const boardData = data[i]; + const key = boardData.metainfo.title; + const board = + list.push(board); + } + return <>{list}; +}); \ No newline at end of file diff --git a/frontend/src/issues-list-board/issues-list-card.module.css b/frontend/src/issues-list-board/issues-list-card.module.css new file mode 100644 index 0000000..8a8963f --- /dev/null +++ b/frontend/src/issues-list-board/issues-list-card.module.css @@ -0,0 +1,14 @@ +.listItem { + margin: 5px; + width: 100%; + display: flex; + flex-direction: column; +} + +.issueSubject {} + +.issueStatus {} + +.issueTime {} + +.tagsContainer {} \ No newline at end of file diff --git a/frontend/src/issues-list-board/issues-list-card.tsx b/frontend/src/issues-list-board/issues-list-card.tsx new file mode 100644 index 0000000..efaf706 --- /dev/null +++ b/frontend/src/issues-list-board/issues-list-card.tsx @@ -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 ( +
+
+ + + + + | {props.store.status.name} + | {props.store.total_spent_hours} / {props.store.total_estimated_hours} +
+
+ +
+
+ ); +}); \ No newline at end of file diff --git a/frontend/src/issues-list-board/store.ts b/frontend/src/issues-list-board/store.ts new file mode 100644 index 0000000..9773062 --- /dev/null +++ b/frontend/src/issues-list-board/store.ts @@ -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(); + +export interface IIssueStore extends Instance {} + +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 {} + +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 { + 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}; + 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 {} \ No newline at end of file diff --git a/frontend/src/kanban-board/kanban-card.tsx b/frontend/src/kanban-board/kanban-card.tsx index 09d6e17..41824e2 100644 --- a/frontend/src/kanban-board/kanban-card.tsx +++ b/frontend/src/kanban-board/kanban-card.tsx @@ -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 = ( -
- {tagsParams.label || 'Tags'}: {tags.map(tag => )} -
- ); + tagsSection = + } + const timePassedParams: TimePassedNs.Params = { + fromIssue: { + issue: props.store.issue, + keyName: 'timePassedClass' + } } return (
- + {props.store.issue.tracker.name} #{props.store.issue.id} - {props.store.issue.subject}
Исп.: {props.store.issue.current_user.name}
Прио.: {props.store.issue.priority.name}
diff --git a/frontend/src/kanban-board/store.ts b/frontend/src/kanban-board/store.ts index e273b51..6dc0ea8 100644 --- a/frontend/src/kanban-board/store.ts +++ b/frontend/src/kanban-board/store.ts @@ -27,14 +27,14 @@ export interface IColumnStore extends Instance {} 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 {} @@ -75,8 +75,6 @@ export type CardField = { component: string; } & Record; -export const CardSettings = types.model({}); - export const CardParamsStore = types.optional( types.model({ fields: types.array( diff --git a/frontend/src/misc-components/issue-href.tsx b/frontend/src/misc-components/issue-href.tsx new file mode 100644 index 0000000..6d9aad9 --- /dev/null +++ b/frontend/src/misc-components/issue-href.tsx @@ -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 ( + {props.tracker} #{props.id} - {props.subject} + ); +}; \ No newline at end of file diff --git a/frontend/src/misc-components/tag.module.css b/frontend/src/misc-components/tag.module.css new file mode 100644 index 0000000..9dbf1d1 --- /dev/null +++ b/frontend/src/misc-components/tag.module.css @@ -0,0 +1,5 @@ +.tag { + font-size: 8pt; + border-radius: 4px; + padding: 2px; +} \ No newline at end of file diff --git a/frontend/src/misc-components/tag.tsx b/frontend/src/misc-components/tag.tsx new file mode 100644 index 0000000..5d3d606 --- /dev/null +++ b/frontend/src/misc-components/tag.tsx @@ -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 ( + + {props.tag} + + ); +} \ No newline at end of file diff --git a/frontend/src/misc-components/tags.tsx b/frontend/src/misc-components/tags.tsx new file mode 100644 index 0000000..8852a1d --- /dev/null +++ b/frontend/src/misc-components/tags.tsx @@ -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 ; + }) || []; + return ( +
+ {label}: {tags} +
+ ); +} \ No newline at end of file diff --git a/frontend/src/misc-components/time-passed.module.css b/frontend/src/misc-components/time-passed.module.css new file mode 100644 index 0000000..c34f6ba --- /dev/null +++ b/frontend/src/misc-components/time-passed.module.css @@ -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); +} \ No newline at end of file diff --git a/frontend/src/misc-components/time-passed.tsx b/frontend/src/misc-components/time-passed.tsx new file mode 100644 index 0000000..0562b2c --- /dev/null +++ b/frontend/src/misc-components/time-passed.tsx @@ -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 ( + + ); +}; + +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 ''; + } +} \ No newline at end of file diff --git a/frontend/src/router.tsx b/frontend/src/router.tsx index fb6336f..890097e 100644 --- a/frontend/src/router.tsx +++ b/frontend/src/router.tsx @@ -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: () }, + { + path: "/issues-list-board/:type/:name", + element: () + }, { path: "*", element: ()