From 0b82ca564ab1cd60eb0815d8a4fcf470eb330342 Mon Sep 17 00:00:00 2001 From: Pavel Gnedov Date: Tue, 3 Oct 2023 07:48:57 +0700 Subject: [PATCH] =?UTF-8?q?=D0=A4=D0=BE=D1=80=D0=BC=D0=B0=D1=82=D0=B8?= =?UTF-8?q?=D1=80=D0=BE=D0=B2=D0=B0=D0=BD=D0=B8=D0=B5=20frontend-=D0=B0=20?= =?UTF-8?q?=D0=BF=D1=80=D0=B8=D0=B2=D0=B5=D0=B4=D0=B5=D0=BD=D0=BE=20=D0=BA?= =?UTF-8?q?=20=D0=BE=D0=B1=D1=89=D0=B8=D0=BC=20=D0=BF=D1=80=D0=B0=D0=B2?= =?UTF-8?q?=D0=B8=D0=BB=D0=B0=D0=BC=20=D1=81=20backend-=D0=BE=D0=BC=20?= =?UTF-8?q?=D1=81=20=D0=BF=D0=BE=D0=BC=D0=BE=D1=89=D1=8C=D1=8E=20eslint?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/index.tsx | 4 +- .../issues-list-board/issues-list-board.tsx | 56 ++--- .../issues-list-boards-page.tsx | 38 ++-- .../issues-list-board/issues-list-boards.tsx | 60 +++--- .../issues-list-board/issues-list-card.tsx | 91 ++++---- frontend/src/issues-list-board/store.ts | 149 +++++++------ frontend/src/kanban-board/column.tsx | 33 +-- frontend/src/kanban-board/kanban-board.tsx | 42 ++-- frontend/src/kanban-board/kanban-boards.tsx | 84 ++++---- frontend/src/kanban-board/kanban-card.tsx | 116 +++++----- frontend/src/kanban-board/store.ts | 196 ++++++++--------- .../misc-components/issue-details-dialog.tsx | 203 +++++++++--------- frontend/src/misc-components/issue-href.tsx | 18 +- frontend/src/misc-components/tag.tsx | 24 +-- frontend/src/misc-components/tags.tsx | 36 ++-- frontend/src/misc-components/time-passed.tsx | 70 +++--- .../src/misc-components/top-right-menu.tsx | 99 +++++---- .../src/misc-components/unreaded-flag.tsx | 116 +++++----- frontend/src/start-page/basement.tsx | 29 ++- frontend/src/start-page/content-block.tsx | 18 +- frontend/src/start-page/content.tsx | 10 +- frontend/src/start-page/cover.tsx | 32 +-- .../src/start-page/notification-block.tsx | 44 ++-- frontend/src/start-page/start-page.tsx | 201 ++++++++++------- frontend/src/start-page/top-bar.tsx | 57 +++-- frontend/src/unknown-page.tsx | 6 +- .../src/utils/service-actions-buttons.tsx | 15 +- frontend/src/utils/service-actions.ts | 40 ++-- frontend/src/utils/spent-hours-to-fixed.ts | 18 +- frontend/src/utils/style.ts | 40 ++-- frontend/src/utils/unreaded-provider.ts | 26 +-- 31 files changed, 1079 insertions(+), 892 deletions(-) diff --git a/frontend/src/index.tsx b/frontend/src/index.tsx index 032464f..07924fc 100644 --- a/frontend/src/index.tsx +++ b/frontend/src/index.tsx @@ -5,12 +5,12 @@ import App from './App'; import reportWebVitals from './reportWebVitals'; const root = ReactDOM.createRoot( - document.getElementById('root') as HTMLElement + document.getElementById('root') as HTMLElement, ); root.render( - + , ); // If you want to start measuring performance in your app, pass a function diff --git a/frontend/src/issues-list-board/issues-list-board.tsx b/frontend/src/issues-list-board/issues-list-board.tsx index eb51b8c..01ba1c5 100644 --- a/frontend/src/issues-list-board/issues-list-board.tsx +++ b/frontend/src/issues-list-board/issues-list-board.tsx @@ -5,32 +5,34 @@ import Css from './issues-list-board.module.css'; import * as IssuesListCardNs from './issues-list-card'; export type Props = { - store: IBoardStore -} + store: IBoardStore; +}; export const IssuesListBoard = observer((props: Props): JSX.Element => { - const list: JSX.Element[] = props.store.data.map((issue) => { - return ( - - ); - }); - let title: JSX.Element; - if (props.store.metainfo.url) { - title = {props.store.metainfo.title}; - } else { - title = <>{props.store.metainfo.title}; - } - return ( -
-
-

{title}

- - anchor - -
-
- {list} -
-
- ); -}); \ No newline at end of file + const list: JSX.Element[] = props.store.data.map((issue) => { + return ; + }); + let title: JSX.Element; + if (props.store.metainfo.url) { + title = {props.store.metainfo.title}; + } else { + title = <>{props.store.metainfo.title}; + } + return ( +
+
+

+ {title} +

+ + anchor + +
+
{list}
+
+ ); +}); diff --git a/frontend/src/issues-list-board/issues-list-boards-page.tsx b/frontend/src/issues-list-board/issues-list-boards-page.tsx index 531ad27..0474c63 100644 --- a/frontend/src/issues-list-board/issues-list-boards-page.tsx +++ b/frontend/src/issues-list-board/issues-list-boards-page.tsx @@ -4,21 +4,23 @@ 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 + 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 ; +}; diff --git a/frontend/src/issues-list-board/issues-list-boards.tsx b/frontend/src/issues-list-board/issues-list-boards.tsx index c9250bb..b77c2db 100644 --- a/frontend/src/issues-list-board/issues-list-boards.tsx +++ b/frontend/src/issues-list-board/issues-list-boards.tsx @@ -7,35 +7,37 @@ import { SetIssuesReadingTimestamp } from '../utils/unreaded-provider'; import * as ServiceActionsButtons from '../utils/service-actions-buttons'; export type Props = { - store: IssuesListBoardStore.IPageStore + 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); - } - const topRightMenuStore = TopRightMenuNs.Store.create({visible: false}); - const onAllReadItemClick = (e: React.MouseEvent) => { - e.stopPropagation(); - SetIssuesReadingTimestamp(props.store.issueIds); - IssuesListBoardStore.PageStoreLoadData(props.store); - }; - return ( - <> - - - - - - {list} - - ); -}); \ No newline at end of file + 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); + } + const topRightMenuStore = TopRightMenuNs.Store.create({ visible: false }); + const onAllReadItemClick = (e: React.MouseEvent) => { + e.stopPropagation(); + SetIssuesReadingTimestamp(props.store.issueIds); + IssuesListBoardStore.PageStoreLoadData(props.store); + }; + return ( + <> + + + + + + {list} + + ); +}); diff --git a/frontend/src/issues-list-board/issues-list-card.tsx b/frontend/src/issues-list-board/issues-list-card.tsx index 242546d..ea49df0 100644 --- a/frontend/src/issues-list-board/issues-list-card.tsx +++ b/frontend/src/issues-list-board/issues-list-card.tsx @@ -11,44 +11,63 @@ import { SpentHoursToFixed } from '../utils/spent-hours-to-fixed'; import { getStyleObjectFromString } from '../utils/style'; export type Props = { - store: IIssueStore + store: IIssueStore; }; export const defaultPriorityStyleKey = 'priorityStyle'; export const IssuesListCard = observer((props: Props): JSX.Element => { - const unreadedStore = UnreadedFlagNs.CreateStoreFromLocalStorage(props.store); - const detailsStore = IssueDetailsDialogNs.Store.create({ - issue: props.store, - visible: false, - unreadedFlagStore: unreadedStore - }); - const priorityStyle = getStyleObjectFromString(props.store[defaultPriorityStyleKey]); - const tagsNewLine = (props.store.styledTags && props.store.styledTags.length > 0) ?
: null; - return ( -
{ e.stopPropagation(); detailsStore.show(); }}> - -
- -
-
- - - - - {SpentHoursToFixed(props.store.total_spent_hours)} / {SpentHoursToFixed(props.store.total_estimated_hours)} - {tagsNewLine} - -
- {props.store.status.name} - {props.store.priority.name} -
-
-
- ); -}); \ No newline at end of file + const unreadedStore = UnreadedFlagNs.CreateStoreFromLocalStorage(props.store); + const detailsStore = IssueDetailsDialogNs.Store.create({ + issue: props.store, + visible: false, + unreadedFlagStore: unreadedStore, + }); + const priorityStyle = getStyleObjectFromString( + props.store[defaultPriorityStyleKey], + ); + const tagsNewLine = + props.store.styledTags && props.store.styledTags.length > 0 ?
: null; + return ( +
{ + e.stopPropagation(); + detailsStore.show(); + }} + > + +
+ +
+
+ + + + + + {SpentHoursToFixed(props.store.total_spent_hours)} /{' '} + {SpentHoursToFixed(props.store.total_estimated_hours)} + + {tagsNewLine} + +
+ {props.store.status.name} + + + {props.store.priority.name} + +
+
+
+ ); +}); diff --git a/frontend/src/issues-list-board/store.ts b/frontend/src/issues-list-board/store.ts index 2442ebf..6fbfe73 100644 --- a/frontend/src/issues-list-board/store.ts +++ b/frontend/src/issues-list-board/store.ts @@ -1,86 +1,93 @@ -import { Instance, types } from "mobx-state-tree"; -import { RedmineTypes } from "../redmine-types"; -import axios from "axios"; +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 type IIssueStore = 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: '' - })) + 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 + data: types.array(IssueStore), + metainfo: MetaInfoStore, }); -export interface IBoardStore extends Instance {} +export type IBoardStore = 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; - } - }; -}).views((self) => { - return { - get issueIds(): number[] { - if (!self.data) return []; - const data = self.data; - const res = [] as number[]; - for (let i = 0; i < data.length; i++) { - const itemData = data[i]; - for (let j = 0; j < itemData.data.length; j++) { - const issue = itemData.data[j]; - if (res.indexOf(issue.id) < 0) { - res.push(issue.id); - } - } - } - return res; - } - }; -}); +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; + }, + }; + }) + .views((self) => { + return { + get issueIds(): number[] { + if (!self.data) return []; + const data = self.data; + const res = [] as number[]; + for (let i = 0; i < data.length; i++) { + const itemData = data[i]; + for (let j = 0; j < itemData.data.length; j++) { + const issue = itemData.data[j]; + if (res.indexOf(issue.id) < 0) { + res.push(issue.id); + } + } + } + return res; + }, + }; + }); export async function PageStoreLoadData(store: IPageStore): Promise { - const url = `${process.env.REACT_APP_BACKEND}simple-kanban-board/${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 ? item.data.map((group: { status: string, count: number, issues: any[] }) => { - return group.issues - }).flat() : [] - }); - } - - /* DEBUG: begin */ - console.debug(`Issues list board store data: ${JSON.stringify(data)}`); - /* DEBUG: end */ - - store.setData(data); + const url = `${process.env.REACT_APP_BACKEND}simple-kanban-board/${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 + ? item.data + .map((group: { status: string; count: number; issues: any[] }) => { + return group.issues; + }) + .flat() + : [], + }); + } + + /* DEBUG: begin */ + console.debug(`Issues list board store data: ${JSON.stringify(data)}`); + /* DEBUG: end */ + + store.setData(data); } -export interface IPageStore extends Instance {} \ No newline at end of file +export type IPageStore = Instance; diff --git a/frontend/src/kanban-board/column.tsx b/frontend/src/kanban-board/column.tsx index 0694ca8..3c70614 100644 --- a/frontend/src/kanban-board/column.tsx +++ b/frontend/src/kanban-board/column.tsx @@ -5,23 +5,26 @@ import { observer } from 'mobx-react-lite'; import * as KanbanCard from './kanban-card'; export type Props = { - store: Stores.IColumnStore -} + store: Stores.IColumnStore; +}; export const Column = observer((props: Props) => { - const cards = props.store.cards.map((card) => { - return ( - - ); - }); - return ( -
-
- {props.store.status} ({props.store.count}) -
- {cards} -
- ); + const cards = props.store.cards.map((card) => { + return ( + + ); + }); + return ( +
+
+ {props.store.status} ({props.store.count}) +
+ {cards} +
+ ); }); export default Column; diff --git a/frontend/src/kanban-board/kanban-board.tsx b/frontend/src/kanban-board/kanban-board.tsx index e90c13c..c96ef6e 100644 --- a/frontend/src/kanban-board/kanban-board.tsx +++ b/frontend/src/kanban-board/kanban-board.tsx @@ -5,29 +5,29 @@ import { observer } from 'mobx-react-lite'; import Column from './column'; export type Props = { - store: IBoardStore + store: IBoardStore; }; export const KanbanBoard = observer((props: Props) => { - let title: any; - if (props.store.metainfo.url) { - title = {props.store.metainfo.title}; - } 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() - } - return ( - <> -

{title} #

-
- {columns} -
- - ); + let title: any; + if (props.store.metainfo.url) { + title = {props.store.metainfo.title}; + } 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(); + } + return ( + <> +

+ {title} # +

+
{columns}
+ + ); }); -export default KanbanBoard; \ No newline at end of file +export default KanbanBoard; diff --git a/frontend/src/kanban-board/kanban-boards.tsx b/frontend/src/kanban-board/kanban-boards.tsx index b019b0f..acb1898 100644 --- a/frontend/src/kanban-board/kanban-boards.tsx +++ b/frontend/src/kanban-board/kanban-boards.tsx @@ -8,47 +8,51 @@ import axios from 'axios'; import * as ServiceActionsButtons from '../utils/service-actions-buttons'; export type Props = { - store: IPageStore -} + store: IPageStore; +}; export const KanbanBoards = observer((props: Props) => { - 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); - } - const topRightMenuStore = TopRightMenuNs.Store.create({visible: false}); - const onAllReadClick = (e: React.MouseEvent) => { - e.stopPropagation(); - SetIssuesReadingTimestamp(props.store.issueIds); - PageStoreLoadData(props.store); - }; - let treeRefreshMenuItem: JSX.Element = <>; - if (props.store.canTreeRefresh) { - const onTreeRefreshClick = (e: React.MouseEvent) => { - if (e.target !== e.currentTarget) return; - e.stopPropagation(); - axios.get(`${process.env.REACT_APP_BACKEND}simple-kanban-board/tree/${props.store.name}/refresh`); - } - treeRefreshMenuItem = ; - } - return ( - <> - - - {treeRefreshMenuItem} - - - - {list} - - ); + 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); + } + const topRightMenuStore = TopRightMenuNs.Store.create({ visible: false }); + const onAllReadClick = (e: React.MouseEvent) => { + e.stopPropagation(); + SetIssuesReadingTimestamp(props.store.issueIds); + PageStoreLoadData(props.store); + }; + let treeRefreshMenuItem: JSX.Element = <>; + if (props.store.canTreeRefresh) { + const onTreeRefreshClick = (e: React.MouseEvent) => { + if (e.target !== e.currentTarget) return; + e.stopPropagation(); + axios.get( + `${process.env.REACT_APP_BACKEND}simple-kanban-board/tree/${props.store.name}/refresh`, + ); + }; + treeRefreshMenuItem = ( + + ); + } + return ( + <> + + + {treeRefreshMenuItem} + + + + {list} + + ); }); -export default KanbanBoards; \ No newline at end of file +export default KanbanBoards; diff --git a/frontend/src/kanban-board/kanban-card.tsx b/frontend/src/kanban-board/kanban-card.tsx index 16f7b0e..113c015 100644 --- a/frontend/src/kanban-board/kanban-card.tsx +++ b/frontend/src/kanban-board/kanban-card.tsx @@ -9,70 +9,84 @@ import * as IssueDetailsDialogNs from '../misc-components/issue-details-dialog'; import * as UnreadedFlagNs from '../misc-components/unreaded-flag'; export type Props = { - store: ICardStore + store: ICardStore; }; export type TagProps = { - style?: string; - tag: string; + style?: string; + tag: string; }; export const KanbanCardTag = (props: TagProps): JSX.Element => { - const inlineStyle = getStyleObjectFromString(props.style || ''); - return ( - - {props.tag} - - ); -} + const inlineStyle = getStyleObjectFromString(props.style || ''); + return ( + + {props.tag} + + ); +}; /** * Какие дальше требования к карточкам? - * + * * 1. Отобразить как было в статичной доске * 2. Переделать отображение с учётом store.params */ export const KanbanCard = observer((props: Props) => { - let tagsSection = <>; - const tagsParams = props.store.params.fields.find((field) => { - return field.component === 'tags'; - }); - console.debug('Tag params:', tagsParams); // DEBUG - console.debug('Issue:', props.store.issue); // DEBUG - if (tagsParams && props.store.issue[tagsParams.path]) { - const tags = props.store.issue[tagsParams.path] as TagProps[]; - console.debug(`Tags:`, tags); // DEBUG - tagsSection = - } - const timePassedParams: TimePassedNs.Params = { - fromIssue: { - issue: props.store.issue, - keyName: 'timePassedClass' - } - } - const unreadedStore = UnreadedFlagNs.CreateStoreFromLocalStorage(props.store.issue); - const detailsStore = IssueDetailsDialogNs.Store.create({ - issue: props.store.issue, - visible: false, - unreadedFlagStore: unreadedStore - }); - return ( -
{e.stopPropagation(); detailsStore.show();}}> - - -
Исп.: {props.store.issue.current_user.name}
-
Прио.: {props.store.issue.priority.name}
-
Версия: {props.store.issue.fixed_version?.name || ''}
-
Прогресс: {props.store.issue.done_ratio}
-
Трудозатраты: {props.store.issue.total_spent_hours} / {props.store.issue.total_estimated_hours}
- {tagsSection} -
- ); + let tagsSection = <>; + const tagsParams = props.store.params.fields.find((field) => { + return field.component === 'tags'; + }); + console.debug('Tag params:', tagsParams); // DEBUG + console.debug('Issue:', props.store.issue); // DEBUG + if (tagsParams && props.store.issue[tagsParams.path]) { + const tags = props.store.issue[tagsParams.path] as TagProps[]; + console.debug(`Tags:`, tags); // DEBUG + tagsSection = ; + } + const timePassedParams: TimePassedNs.Params = { + fromIssue: { + issue: props.store.issue, + keyName: 'timePassedClass', + }, + }; + const unreadedStore = UnreadedFlagNs.CreateStoreFromLocalStorage( + props.store.issue, + ); + const detailsStore = IssueDetailsDialogNs.Store.create({ + issue: props.store.issue, + visible: false, + unreadedFlagStore: unreadedStore, + }); + return ( +
{ + e.stopPropagation(); + detailsStore.show(); + }} + > + + +
Исп.: {props.store.issue.current_user.name}
+
Прио.: {props.store.issue.priority.name}
+
Версия: {props.store.issue.fixed_version?.name || ''}
+
Прогресс: {props.store.issue.done_ratio}
+
+ Трудозатраты: {props.store.issue.total_spent_hours} /{' '} + {props.store.issue.total_estimated_hours} +
+ {tagsSection} +
+ ); }); -export default KanbanCard; \ No newline at end of file +export default KanbanCard; diff --git a/frontend/src/kanban-board/store.ts b/frontend/src/kanban-board/store.ts index 1a16812..cfaeaf4 100644 --- a/frontend/src/kanban-board/store.ts +++ b/frontend/src/kanban-board/store.ts @@ -2,126 +2,128 @@ import { Instance, types } from 'mobx-state-tree'; import { RedmineTypes } from '../redmine-types'; import axios from 'axios'; -export const IssueStore = types.frozen() +export const IssueStore = types.frozen(); -export interface IIssueStore extends Instance {} +export type IIssueStore = Instance; -export const ColumnStore = types.model({ - status: '', - count: 0, - issues: types.array(IssueStore) -}).views((self) => { - return { - get cards(): ICardStore[] { - return self.issues.map(issue => { - return CardStore.create({ - issue: issue - }) - }); - } - } -}); +export const ColumnStore = types + .model({ + status: '', + count: 0, + issues: types.array(IssueStore), + }) + .views((self) => { + return { + get cards(): ICardStore[] { + return self.issues.map((issue) => { + return CardStore.create({ + issue: issue, + }); + }); + }, + }; + }); -export interface IColumnStore extends Instance {} +export type IColumnStore = Instance; export const MetaInfoStore = types.model({ - title: '', - url: types.maybe(types.string), - rootIssue: types.maybe(types.model({ - id: 0, - tracker: types.model({ - id: 0, - name: '' - }), - subject: '' - })) + title: '', + url: types.maybe(types.string), + rootIssue: types.maybe( + types.model({ + id: 0, + tracker: types.model({ + id: 0, + name: '', + }), + subject: '', + }), + ), }); -export interface IMetaInfoStore extends Instance {} +export type IMetaInfoStore = Instance; export const BoardStore = types.model({ - data: types.array(ColumnStore), - metainfo: MetaInfoStore + data: types.array(ColumnStore), + metainfo: MetaInfoStore, }); -export interface IBoardStore extends Instance {} +export type IBoardStore = Instance; -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; - } - }; -}).views((self) => { - return { - get issueIds(): number[] { - if (!self.data) return []; - const res = [] as number[]; - for (let i = 0; i < self.data.length; i++) { - const iData = self.data[i]; - for (let j = 0; j < iData.data.length; j++) { - const jData = iData.data[j]; - for (let k = 0; k < jData.issues.length; k++) { - const issue = jData.issues[k]; - if (res.indexOf(issue.id) < 0) { - res.push(issue.id); - } - } - } - } - return res; - }, - get canTreeRefresh(): boolean { - return (self.type === 'tree'); - } - }; -}); +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; + }, + }; + }) + .views((self) => { + return { + get issueIds(): number[] { + if (!self.data) return []; + const res = [] as number[]; + for (let i = 0; i < self.data.length; i++) { + const iData = self.data[i]; + for (let j = 0; j < iData.data.length; j++) { + const jData = iData.data[j]; + for (let k = 0; k < jData.issues.length; k++) { + const issue = jData.issues[k]; + if (res.indexOf(issue.id) < 0) { + res.push(issue.id); + } + } + } + } + return res; + }, + get canTreeRefresh(): boolean { + return self.type === 'tree'; + }, + }; + }); export async function PageStoreLoadData(store: IPageStore): Promise { - const url = `${process.env.REACT_APP_BACKEND}simple-kanban-board/${store.type}/${store.name}/raw`; - const resp = await axios.get(url); - if (!(resp?.data)) return; - store.setData(resp.data); + const url = `${process.env.REACT_APP_BACKEND}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 { } +export type IPageStore = Instance; export type CardField = { - component: string; + component: string; } & Record; export const CardParamsStore = types.optional( - types.model({ - fields: types.array( - types.frozen() - ), - autoCollapse: types.boolean - }), - { - fields: [ - { 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: false, - } + types.model({ + fields: types.array(types.frozen()), + autoCollapse: types.boolean, + }), + { + fields: [ + { 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: false, + }, ); export const CardStore = types.model({ - issue: IssueStore, - params: CardParamsStore + issue: IssueStore, + params: CardParamsStore, }); -export interface ICardStore extends Instance {} - +export type ICardStore = Instance; diff --git a/frontend/src/misc-components/issue-details-dialog.tsx b/frontend/src/misc-components/issue-details-dialog.tsx index 3a9e841..f8958ec 100644 --- a/frontend/src/misc-components/issue-details-dialog.tsx +++ b/frontend/src/misc-components/issue-details-dialog.tsx @@ -9,107 +9,118 @@ import { SetIssueReadingTimestamp } from '../utils/unreaded-provider'; import axios from 'axios'; import * as Luxon from 'luxon'; -export const Store = types.model({ - visible: types.boolean, - issue: types.frozen(), - unreadedFlagStore: types.maybe(UnreadedFlagNs.Store) -}).actions((self) => { - return { - hide: () => { - console.debug(`Issue details dialog hide: issue_id=${self.issue.id}`); // DEBUG - self.visible = false; - }, - show: () => { - console.debug(`Issue details dialog show: issue_id=${self.issue.id}`); // DEBUG - self.visible = true; - if (self.unreadedFlagStore) { - self.unreadedFlagStore.read(); - } else { - SetIssueReadingTimestamp(self.issue.id); - } - } - }; -}).views((self) => { - return { - get displayStyle(): React.CSSProperties { - return {display: self.visible ? 'block' : 'none'}; - } - }; -}); +export const Store = types + .model({ + visible: types.boolean, + issue: types.frozen(), + unreadedFlagStore: types.maybe(UnreadedFlagNs.Store), + }) + .actions((self) => { + return { + hide: () => { + console.debug(`Issue details dialog hide: issue_id=${self.issue.id}`); // DEBUG + self.visible = false; + }, + show: () => { + console.debug(`Issue details dialog show: issue_id=${self.issue.id}`); // DEBUG + self.visible = true; + if (self.unreadedFlagStore) { + self.unreadedFlagStore.read(); + } else { + SetIssueReadingTimestamp(self.issue.id); + } + }, + }; + }) + .views((self) => { + return { + get displayStyle(): React.CSSProperties { + return { display: self.visible ? 'block' : 'none' }; + }, + }; + }); export type Props = { - store: Instance + store: Instance; }; export const IssueDetailsDialog = observer((props: Props): JSX.Element => { - const onUpdateClick = (e: React.MouseEvent) => { - const url = `${process.env.REACT_APP_BACKEND}redmine-event-emitter/append-issues`; - axios.post(url, [props.store.issue.id]); - }; - const onCloseClick = (e: React.MouseEvent) => { - if (e.target !== e.currentTarget) return; - e.stopPropagation(); - props.store.hide(); - }; - return ( -
-
-
-

- - - -

-
-
-

Описание:

-
-							{props.store.issue.description}
-						
-
-
-
-

Комментарии:

- -
-
-
-
- ); + const onUpdateClick = (e: React.MouseEvent) => { + const url = `${process.env.REACT_APP_BACKEND}redmine-event-emitter/append-issues`; + axios.post(url, [props.store.issue.id]); + }; + const onCloseClick = (e: React.MouseEvent) => { + if (e.target !== e.currentTarget) return; + e.stopPropagation(); + props.store.hide(); + }; + return ( +
+
+
+

+ + + +

+
+
+

Описание:

+
{props.store.issue.description}
+
+
+
+

Комментарии:

+ +
+
+
+
+ ); }); -export const Comments = (props: {details?: RedmineTypes.Journal[], issue: RedmineTypes.ExtendedIssue}): JSX.Element => { - const comments = props.details?.filter((detail) => { - return Boolean(detail.notes); - }); - if (!comments) { - return <>No comments - } - const list = comments.map((detail) => { - const key = `issueid_${props.issue.id}_commentid_${detail.id}`; - return - }); - return ( - <>{list} - ); -} +export const Comments = (props: { + details?: RedmineTypes.Journal[]; + issue: RedmineTypes.ExtendedIssue; +}): JSX.Element => { + const comments = props.details?.filter((detail) => { + return Boolean(detail.notes); + }); + if (!comments) { + return <>No comments; + } + const list = comments.map((detail) => { + const key = `issueid_${props.issue.id}_commentid_${detail.id}`; + return ; + }); + return <>{list}; +}; -export const Comment = (props: {data: RedmineTypes.Journal}): JSX.Element => { - const date = Luxon.DateTime.fromISO(props.data.created_on).toFormat("dd.MM.yyyy HH:mm"); - return ( - <> -

{date} {props.data.user.name}:

-
-
-					{props.data.notes || '-'}
-				
-
-
- - ); -} \ No newline at end of file +export const Comment = (props: { data: RedmineTypes.Journal }): JSX.Element => { + const date = Luxon.DateTime.fromISO(props.data.created_on).toFormat( + 'dd.MM.yyyy HH:mm', + ); + return ( + <> +

+ {date} {props.data.user.name}: +

+
+
{props.data.notes || '-'}
+
+
+ + ); +}; diff --git a/frontend/src/misc-components/issue-href.tsx b/frontend/src/misc-components/issue-href.tsx index 6d9aad9..aab7cdc 100644 --- a/frontend/src/misc-components/issue-href.tsx +++ b/frontend/src/misc-components/issue-href.tsx @@ -1,14 +1,16 @@ import React from 'react'; export type Props = { - url: string; - id: number; - subject: string; - tracker: string; + 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 + return ( + + {props.tracker} #{props.id} - {props.subject} + + ); +}; diff --git a/frontend/src/misc-components/tag.tsx b/frontend/src/misc-components/tag.tsx index 452dff9..2fd3d1a 100644 --- a/frontend/src/misc-components/tag.tsx +++ b/frontend/src/misc-components/tag.tsx @@ -3,18 +3,18 @@ import { getStyleObjectFromString } from '../utils/style'; import Css from './tag.module.css'; export type Props = { - style?: string; - tag: string; + 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 + const inlineStyle = getStyleObjectFromString(props.style || ''); + return ( + <> + + + {props.tag} + + + ); +}; diff --git a/frontend/src/misc-components/tags.tsx b/frontend/src/misc-components/tags.tsx index ae749ab..82aeaa2 100644 --- a/frontend/src/misc-components/tags.tsx +++ b/frontend/src/misc-components/tags.tsx @@ -2,26 +2,28 @@ import React from 'react'; import * as TagNs from './tag'; export type Params = { - label?: string; - tags: TagNs.Props[]; + label?: string; + tags: TagNs.Props[]; }; export type Props = { - params: Params + params: Params; }; export const Tags = (props: Props): JSX.Element => { - if (!props.params.tags) { - return (<>); - } - let label = props.params.label || ''; - if (label) label = `${label}: `; - const tags = props.params.tags.map((tag) => { - return ; - }) || []; - return ( - <> - {label}{tags} - - ); -} \ No newline at end of file + if (!props.params.tags) { + return <>; + } + let label = props.params.label || ''; + if (label) label = `${label}: `; + const tags = + props.params.tags.map((tag) => { + return ; + }) || []; + return ( + <> + {label} + {tags} + + ); +}; diff --git a/frontend/src/misc-components/time-passed.tsx b/frontend/src/misc-components/time-passed.tsx index 0562b2c..b7731d6 100644 --- a/frontend/src/misc-components/time-passed.tsx +++ b/frontend/src/misc-components/time-passed.tsx @@ -3,46 +3,48 @@ import Css from './time-passed.module.css'; import { RedmineTypes } from '../redmine-types'; export type Params = { - fromIssue?: { - issue: RedmineTypes.ExtendedIssue, - keyName: string, - }, - fromValue?: string + fromIssue?: { + issue: RedmineTypes.ExtendedIssue; + keyName: string; + }; + fromValue?: string; }; export type Props = { - params: Params + 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 ( - - ); + 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 + 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 ''; + } +} diff --git a/frontend/src/misc-components/top-right-menu.tsx b/frontend/src/misc-components/top-right-menu.tsx index 5bdc5d7..7e19926 100644 --- a/frontend/src/misc-components/top-right-menu.tsx +++ b/frontend/src/misc-components/top-right-menu.tsx @@ -3,53 +3,62 @@ import { Instance, types } from 'mobx-state-tree'; import React from 'react'; import Css from './top-right-menu.module.css'; -export const Store = types.model({ - visible: types.boolean -}).views((self) => { - return { - get style(): React.CSSProperties { - return { - display: self.visible ? 'block' : 'none' - }; - } - }; -}).actions((self) => { - return { - show: () => { - self.visible = true; - }, - hide: () => { - self.visible = false; - }, - toggle: () => { - self.visible = !self.visible; - } - }; -}); +export const Store = types + .model({ + visible: types.boolean, + }) + .views((self) => { + return { + get style(): React.CSSProperties { + return { + display: self.visible ? 'block' : 'none', + }; + }, + }; + }) + .actions((self) => { + return { + show: () => { + self.visible = true; + }, + hide: () => { + self.visible = false; + }, + toggle: () => { + self.visible = !self.visible; + }, + }; + }); export type Props = { - store: Instance; - children?: any; + store: Instance; + children?: any; }; export const TopRightMenu = observer((props: Props): JSX.Element => { - const menuItems = []; - if (props.children.length > 1) { - for (let key = 0; key < props.children.length; key++) { - const item = props.children[key]; - menuItems.push(
  • {item}
  • ); - } - } else if (props.children) { - menuItems.push(
  • {props.children}
  • ) - } - return ( - <> - -
    -
      - {menuItems} -
    -
    - - ); -}) \ No newline at end of file + const menuItems = []; + if (props.children.length > 1) { + for (let key = 0; key < props.children.length; key++) { + const item = props.children[key]; + menuItems.push(
  • {item}
  • ); + } + } else if (props.children) { + menuItems.push(
  • {props.children}
  • ); + } + return ( + <> + +
    +
      {menuItems}
    +
    + + ); +}); diff --git a/frontend/src/misc-components/unreaded-flag.tsx b/frontend/src/misc-components/unreaded-flag.tsx index f87da53..81432ba 100644 --- a/frontend/src/misc-components/unreaded-flag.tsx +++ b/frontend/src/misc-components/unreaded-flag.tsx @@ -3,62 +3,76 @@ import Css from './unreaded-flag.module.css'; import { observer } from 'mobx-react-lite'; import { Instance, types } from 'mobx-state-tree'; import { RedmineTypes } from '../redmine-types'; -import { GetIssueReadingTimestamp, SetIssueReadingTimestamp } from '../utils/unreaded-provider'; +import { + GetIssueReadingTimestamp, + SetIssueReadingTimestamp, +} from '../utils/unreaded-provider'; -export const Store = types.model({ - issue: types.frozen(), - readingTimestamp: types.number -}).actions((self) => { - return { - read: () => { - self.readingTimestamp = SetIssueReadingTimestamp(self.issue.id); - } - }; -}).views((self) => { - return { - getUpdatedTimestap(): number { - if (self.issue.journals) { - let lastComment: RedmineTypes.Journal | undefined; - for (let i = self.issue.journals.length - 1; i >= 0; i--) { - const journal = self.issue.journals[i]; - if (journal.notes) { - lastComment = journal; - break; - } - } - if (lastComment) { - return (new Date(lastComment.created_on)).getTime(); - } - } - return 0; - }, - getClassName(): string { - let className = Css.circle; - const updatedTimestamp = this.getUpdatedTimestap(); - if (self.readingTimestamp < updatedTimestamp) { - className += ` ${Css.unreaded}`; - } - console.debug(`Unreaded flag getClassName: issueId=${self.issue.id}; readingTimestamp=${self.readingTimestamp}; updatedTimestamp=${updatedTimestamp}; className=${className}`); // DEBUG - return className; - } - }; -}); +export const Store = types + .model({ + issue: types.frozen(), + readingTimestamp: types.number, + }) + .actions((self) => { + return { + read: () => { + self.readingTimestamp = SetIssueReadingTimestamp(self.issue.id); + }, + }; + }) + .views((self) => { + return { + getUpdatedTimestap(): number { + if (self.issue.journals) { + let lastComment: RedmineTypes.Journal | undefined; + for (let i = self.issue.journals.length - 1; i >= 0; i--) { + const journal = self.issue.journals[i]; + if (journal.notes) { + lastComment = journal; + break; + } + } + if (lastComment) { + return new Date(lastComment.created_on).getTime(); + } + } + return 0; + }, + getClassName(): string { + let className = Css.circle; + const updatedTimestamp = this.getUpdatedTimestap(); + if (self.readingTimestamp < updatedTimestamp) { + className += ` ${Css.unreaded}`; + } + console.debug( + `Unreaded flag getClassName: issueId=${self.issue.id}; readingTimestamp=${self.readingTimestamp}; updatedTimestamp=${updatedTimestamp}; className=${className}`, + ); // DEBUG + return className; + }, + }; + }); export function CreateStoreFromLocalStorage(issue: RedmineTypes.ExtendedIssue) { - const timestamp = GetIssueReadingTimestamp(issue.id); - return Store.create({ - issue: issue, - readingTimestamp: timestamp - }); + const timestamp = GetIssueReadingTimestamp(issue.id); + return Store.create({ + issue: issue, + readingTimestamp: timestamp, + }); } export type Props = { - store: Instance -} + store: Instance; +}; export const UnreadedFlag = observer((props: Props): JSX.Element => { - const className = props.store.getClassName(); - return ( - {e.stopPropagation(); props.store.read();}}> - ); -}) \ No newline at end of file + const className = props.store.getClassName(); + return ( + { + e.stopPropagation(); + props.store.read(); + }} + > + ); +}); diff --git a/frontend/src/start-page/basement.tsx b/frontend/src/start-page/basement.tsx index 1bf12e4..9a402eb 100644 --- a/frontend/src/start-page/basement.tsx +++ b/frontend/src/start-page/basement.tsx @@ -10,18 +10,29 @@ export type Props = { export const Basement = (props: Props): JSX.Element => { console.debug('routes:', router.routes); // DEBUG - + return (
    @@ -30,8 +41,12 @@ export const Basement = (props: Props): JSX.Element => {

    ОБСУДИТЬ

    - Сharacter - + Сharacter
    diff --git a/frontend/src/start-page/content-block.tsx b/frontend/src/start-page/content-block.tsx index 1a53e54..4fc9e25 100644 --- a/frontend/src/start-page/content-block.tsx +++ b/frontend/src/start-page/content-block.tsx @@ -1,17 +1,17 @@ import React from 'react'; export type Props = { - title: string; - children?: any; + title: string; + children?: any; }; export const ContentBlock = (props: Props) => { - return ( - <> -

    {props.title}

    - {props.children} - - ); + return ( + <> +

    {props.title}

    + {props.children} + + ); }; -export default ContentBlock; \ No newline at end of file +export default ContentBlock; diff --git a/frontend/src/start-page/content.tsx b/frontend/src/start-page/content.tsx index b9c96aa..9dd70a9 100644 --- a/frontend/src/start-page/content.tsx +++ b/frontend/src/start-page/content.tsx @@ -2,15 +2,11 @@ import React from 'react'; import ContentCss from './content.module.css'; export type Props = { - children?: any; + children?: any; }; export const Content = (props: Props) => { - return ( -
    - {props.children} -
    - ); + return
    {props.children}
    ; }; -export default Content; \ No newline at end of file +export default Content; diff --git a/frontend/src/start-page/cover.tsx b/frontend/src/start-page/cover.tsx index ede82cb..ff27564 100644 --- a/frontend/src/start-page/cover.tsx +++ b/frontend/src/start-page/cover.tsx @@ -2,22 +2,26 @@ import React from 'react'; import CoverCss from './cover.module.css'; export type CoverProps = { - telegramBotUrl: string; + telegramBotUrl: string; }; export const Cover = (props: CoverProps) => { - return ( -
    - Сharacter -
    -

    Redmine Issue Event Emitter

    -

    ОБРАБОТКА И АНАЛИЗ ЗАДАЧ ИЗ "REDMINE"

    -

    - ссылка на телеграмм бота -

    -
    -
    - ); + return ( +
    + Сharacter +
    +

    Redmine Issue Event Emitter

    +

    ОБРАБОТКА И АНАЛИЗ ЗАДАЧ ИЗ "REDMINE"

    +

    + ссылка на телеграмм бота +

    +
    +
    + ); }; -export default Cover; \ No newline at end of file +export default Cover; diff --git a/frontend/src/start-page/notification-block.tsx b/frontend/src/start-page/notification-block.tsx index 1956af1..76096d4 100644 --- a/frontend/src/start-page/notification-block.tsx +++ b/frontend/src/start-page/notification-block.tsx @@ -2,26 +2,34 @@ import React from 'react'; import NotificationBlockCss from './notification-block.module.css'; export type Props = { - avatarUrl: string; - taskTitle?: string; - children?: any; + avatarUrl: string; + taskTitle?: string; + children?: any; }; export const NotificationBlock = (props: Props) => { - const taskTitle = props?.taskTitle - ? ({props.taskTitle} ) - : (<>); - return ( -
    - event_emitter_eltex_loc -
    -

    - {taskTitle} - {props.children} -

    -
    -
    - ); + const taskTitle = props?.taskTitle ? ( + + {props.taskTitle}{' '} + + ) : ( + <> + ); + return ( +
    + event_emitter_eltex_loc +
    +

    + {taskTitle} + {props.children} +

    +
    +
    + ); }; -export default NotificationBlock; \ No newline at end of file +export default NotificationBlock; diff --git a/frontend/src/start-page/start-page.tsx b/frontend/src/start-page/start-page.tsx index a054d79..896599b 100644 --- a/frontend/src/start-page/start-page.tsx +++ b/frontend/src/start-page/start-page.tsx @@ -9,95 +9,132 @@ import StartPageCss from './start-page.module.css'; import TopBar from './top-bar'; export const StartPageData = { - contact: 'https://t.me/pavelgnedov', - bot: 'https://t.me/eltex_event_emitter_bot' + contact: 'https://t.me/pavelgnedov', + bot: 'https://t.me/eltex_event_emitter_bot', }; export const StartPage = () => { - return ( -
    - - - - -
      -
    • Уведомления в реальном времени о событиях из задач - изменения статусов, упоминания комментариев
    • -
    • Генерация и управления отчётами о задачах
    • -
    • Под капотом приложение фреймворк
    • -
    -
    - -
      -
    • Последний отчёт для дейли проект ECCM
    • -
    • Дополнительные функции для разработчиков - eccm:/current_issues_eccm - список текущих задач по статусам - выбираютсятолько задачи из актуальных версий в статусах, где нужна какая-то реакцияили возможна работа прямо сейчас
    • -
    • Скриншоты уведомления от бота: - Примеры уведомлений о новых задачах и об изменениях статусов:
    • -
    - - - Реализовать поддержку нового протокола:

    - Стив Джобс изменил статус задачи с Feedback на Closed -
    - - - Добавить поддержку новых моделей:

    + return ( +
    + + + + +
      +
    • + Уведомления в реальном времени о событиях из задач - изменения + статусов, упоминания комментариев +
    • +
    • Генерация и управления отчётами о задачах
    • +
    • Под капотом приложение фреймворк
    • +
    +
    + +
      +
    • Последний отчёт для дейли проект ECCM
    • +
    • + Дополнительные функции для разработчиков eccm:/current_issues_eccm + - список текущих задач по статусам - выбираютсятолько задачи из + актуальных версий в статусах, где нужна какая-то реакцияили + возможна работа прямо сейчас +
    • +
    • + Скриншоты уведомления от бота: Примеры уведомлений о новых задачах + и об изменениях статусов: +
    • +
    - Билл Гейтс создал новую задачу и назначил её на вас - - -

    Простые уведомления о движении задач - и больше ничего лишнего. - Пример уведомления по личному упоминанию в задаче: -

    - - - Сергей Брин:

    + + Реализовать поддержку нового протокола:
    +
    + Стив Джобс изменил статус задачи с Feedback на Closed +
    - @Ларри Пейдж@, у меня есть хорошая идея. Посмотри, пожалуйста, по описанию к этой задаче. -
    - - - Исправление уязвимости

    + + Добавить поддержку новых моделей:
    +
    + Билл Гейтс создал новую задачу и назначил её на вас +
    - Линус Торвальдс завершил разработку по задаче и передал вам на ревью

    +

    + Простые уведомления о движении задач - и больше ничего лишнего. + Пример уведомления по личному упоминанию в задаче: +

    - Кажется получилось поправить проблемку. Глянь мой MR. -
    - -

    Можно задавать коллегам вопросы прямо из комментария задачи, неотрываясь от её содержимого. Уведомление доставится в считанные минуты с ссылкой на задачу и информацией от кого это уведомление.

    -

    Пример запроса моих текущих задач с помощью команды - /current_issues_eccm -

    - - - Бьёрн Страуструп:

    + + Сергей Брин: +
    +
    + @Ларри Пейдж@, у меня есть хорошая идея. Посмотри, пожалуйста, по + описанию к этой задаче. +
    - Re-opened:

    - - Feature #223301: - Дополнить stdlib новыми функциями (прио - P4, версия - C++23)

    - In Progress:

    - - Question #223411: - Выпуск релиза C++23 (прио - P4, версия - C++23) -
    -
    -
    - -
    - ); + + Исправление уязвимости +
    +
    + Линус Торвальдс завершил разработку по задаче и передал вам на ревью +
    +
    + Кажется получилось поправить проблемку. Глянь мой MR. +
    + +

    + Можно задавать коллегам вопросы прямо из комментария задачи, + неотрываясь от её содержимого. Уведомление доставится в считанные + минуты с ссылкой на задачу и информацией от кого это уведомление. +

    +

    + Пример запроса моих текущих задач с помощью команды + + /current_issues_eccm + +

    + + + Бьёрн Страуструп: +
    +
    + Re-opened: +
    +
    + + {' '} + - Feature #223301:{' '} + + Дополнить stdlib новыми функциями (прио - P4, версия - C++23) +
    +
    + In Progress: +
    +
    + + {' '} + - Question #223411: + + Выпуск релиза C++23 (прио - P4, версия - C++23) +
    +
    +
    + +
    + ); }; -export default StartPage; \ No newline at end of file +export default StartPage; diff --git a/frontend/src/start-page/top-bar.tsx b/frontend/src/start-page/top-bar.tsx index 365dcee..83588ca 100644 --- a/frontend/src/start-page/top-bar.tsx +++ b/frontend/src/start-page/top-bar.tsx @@ -3,29 +3,48 @@ import TopBarCss from './top-bar.module.css'; import LogoImg from './event_emitter_eltex_loc-32px.png'; export type TopBarProps = { - contact: string; - children?: any; + contact: string; + children?: any; }; const TopBar = (props: TopBarProps): ReactElement => { - return ( - + ); }; export default TopBar; diff --git a/frontend/src/unknown-page.tsx b/frontend/src/unknown-page.tsx index 325975f..b00db7c 100644 --- a/frontend/src/unknown-page.tsx +++ b/frontend/src/unknown-page.tsx @@ -1,9 +1,7 @@ import React from 'react'; export const UnknownPage = () => { - return ( -

    Unknown page

    - ) + return

    Unknown page

    ; }; -export default UnknownPage; \ No newline at end of file +export default UnknownPage; diff --git a/frontend/src/utils/service-actions-buttons.tsx b/frontend/src/utils/service-actions-buttons.tsx index 16c1706..6f8cadd 100644 --- a/frontend/src/utils/service-actions-buttons.tsx +++ b/frontend/src/utils/service-actions-buttons.tsx @@ -1,14 +1,15 @@ import React from 'react'; -import { onGetIssuesQueueSizeClick, onIssuesRefreshClick } from './service-actions'; +import { + onGetIssuesQueueSizeClick, + onIssuesRefreshClick, +} from './service-actions'; export const IssuesForceRefreshButton = (): JSX.Element => { - return ( - - ); + return ; }; export const GetIssuesQueueSizeButton = (): JSX.Element => { - return ( - - ); + return ( + + ); }; diff --git a/frontend/src/utils/service-actions.ts b/frontend/src/utils/service-actions.ts index 0c2f950..b0298ac 100644 --- a/frontend/src/utils/service-actions.ts +++ b/frontend/src/utils/service-actions.ts @@ -1,21 +1,31 @@ -import axios from "axios"; +import axios from 'axios'; import React from 'react'; export const onIssuesRefreshClick = (e: React.MouseEvent) => { - if (e.target !== e.currentTarget) return; - e.stopPropagation(); - const rawInput = prompt("Force issues refresh (delimiters - space, comma, semicolon or tab)", ""); - if (!rawInput) return; - const list = rawInput.split(/[ ,;\t\n\r]/).map(item => Number(item)).filter(item => (Number.isFinite(item) && item > 0)); - if (!list) return; - axios.post(`/redmine-event-emitter/append-issues`, list); + if (e.target !== e.currentTarget) return; + e.stopPropagation(); + const rawInput = prompt( + 'Force issues refresh (delimiters - space, comma, semicolon or tab)', + '', + ); + if (!rawInput) return; + const list = rawInput + .split(/[ ,;\t\n\r]/) + .map((item) => Number(item)) + .filter((item) => Number.isFinite(item) && item > 0); + if (!list) return; + axios.post(`/redmine-event-emitter/append-issues`, list); }; -export const onGetIssuesQueueSizeClick = async (e: React.MouseEvent): Promise => { - if (e.target !== e.currentTarget) return; - e.stopPropagation(); - const resp = await axios.get(`${process.env.REACT_APP_BACKEND}redmine-event-emitter/get-issues-queue-size`); - console.debug(`resp -`, resp); // DEBUG - if (!resp || typeof resp.data !== 'number') return; - alert(`Issues queue size - ${resp.data}`); +export const onGetIssuesQueueSizeClick = async ( + e: React.MouseEvent, +): Promise => { + if (e.target !== e.currentTarget) return; + e.stopPropagation(); + const resp = await axios.get( + `${process.env.REACT_APP_BACKEND}redmine-event-emitter/get-issues-queue-size`, + ); + console.debug(`resp -`, resp); // DEBUG + if (!resp || typeof resp.data !== 'number') return; + alert(`Issues queue size - ${resp.data}`); }; diff --git a/frontend/src/utils/spent-hours-to-fixed.ts b/frontend/src/utils/spent-hours-to-fixed.ts index a7e8045..0607f52 100644 --- a/frontend/src/utils/spent-hours-to-fixed.ts +++ b/frontend/src/utils/spent-hours-to-fixed.ts @@ -1,11 +1,13 @@ /** * Форматирование чисел для вывода трудозатрат - * @param a - * @returns + * @param a + * @returns */ -export const SpentHoursToFixed = (a: number|string|null|undefined): string => { - if (a === null || typeof a === 'undefined') return '-'; - const res = (typeof a === 'number') ? a : Number(a); - if (!Number.isFinite(res)) return '-'; - return `${parseFloat(res.toFixed(1))}`; -}; \ No newline at end of file +export const SpentHoursToFixed = ( + a: number | string | null | undefined, +): string => { + if (a === null || typeof a === 'undefined') return '-'; + const res = typeof a === 'number' ? a : Number(a); + if (!Number.isFinite(res)) return '-'; + return `${parseFloat(res.toFixed(1))}`; +}; diff --git a/frontend/src/utils/style.ts b/frontend/src/utils/style.ts index 0b675d2..268f4fc 100644 --- a/frontend/src/utils/style.ts +++ b/frontend/src/utils/style.ts @@ -1,24 +1,26 @@ 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("") - ); + 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 => { - const style = {} as Record; - str.split(";").forEach(el => { - const [property, value] = el.split(":"); - if (!property) return; +export const getStyleObjectFromString = ( + str: string, +): Record => { + const style = {} as Record; + str.split(';').forEach((el) => { + const [property, value] = el.split(':'); + if (!property) return; - const formattedProperty = formatStringToCamelCase(property.trim()); - style[formattedProperty] = value.trim(); - }); + const formattedProperty = formatStringToCamelCase(property.trim()); + style[formattedProperty] = value.trim(); + }); - return style; -}; \ No newline at end of file + return style; +}; diff --git a/frontend/src/utils/unreaded-provider.ts b/frontend/src/utils/unreaded-provider.ts index 2bf4299..9adc8b7 100644 --- a/frontend/src/utils/unreaded-provider.ts +++ b/frontend/src/utils/unreaded-provider.ts @@ -1,23 +1,23 @@ export function GetIssueReadingTimestamp(issueId: number): number { - const value = window.localStorage.getItem(getKey(issueId)); - return value ? Number(value) : 0; + const value = window.localStorage.getItem(getKey(issueId)); + return value ? Number(value) : 0; } export function SetIssueReadingTimestamp(issueId: number): number { - const now = (new Date()).getTime(); - window.localStorage.setItem(getKey(issueId), String(now)); - return now; + const now = new Date().getTime(); + window.localStorage.setItem(getKey(issueId), String(now)); + return now; } export function SetIssuesReadingTimestamp(issueIds: number[]): number { - const now = (new Date()).getTime(); - for (let i = 0; i < issueIds.length; i++) { - const issueId = issueIds[i]; - window.localStorage.setItem(getKey(issueId), String(now)); - } - return now; + const now = new Date().getTime(); + for (let i = 0; i < issueIds.length; i++) { + const issueId = issueIds[i]; + window.localStorage.setItem(getKey(issueId), String(now)); + } + return now; } function getKey(issueId: number): string { - return `issue_read_${issueId}`; -} \ No newline at end of file + return `issue_read_${issueId}`; +}