Форматирование frontend-а приведено к общим правилам с backend-ом с помощью eslint

This commit is contained in:
Pavel Gnedov 2023-10-03 07:48:57 +07:00
parent 0e28eba615
commit 0b82ca564a
31 changed files with 1079 additions and 892 deletions

View file

@ -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(
<React.StrictMode>
<App />
</React.StrictMode>
</React.StrictMode>,
);
// If you want to start measuring performance in your app, pass a function

View file

@ -5,14 +5,12 @@ 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 (
<IssuesListCardNs.IssuesListCard store={issue} key={issue.id}/>
);
return <IssuesListCardNs.IssuesListCard store={issue} key={issue.id} />;
});
let title: JSX.Element;
if (props.store.metainfo.url) {
@ -23,14 +21,18 @@ export const IssuesListBoard = observer((props: Props): JSX.Element => {
return (
<div className={Css.board}>
<div className={Css.boardName}>
<h2 className={Css.boardHeader} id={props.store.metainfo.title}>{title}</h2>
<h2 className={Css.boardHeader} id={props.store.metainfo.title}>
{title}
</h2>
<a href={`#${props.store.metainfo.title}`}>
<img src="/images/anchor BLUE.svg" alt="anchor" className={Css.anchorIcon} />
<img
src="/images/anchor BLUE.svg"
alt="anchor"
className={Css.anchorIcon}
/>
</a>
</div>
<div className={Css.listContainer}>
{list}
</div>
<div className={Css.listContainer}>{list}</div>
</div>
);
});

View file

@ -15,10 +15,12 @@ export const IssuesListBoardPage = (): JSX.Element => {
});
// DEBUG: end
const store = IssuesListStoreNs.PageStore.create({loaded: false, type: type, name: name});
const store = IssuesListStoreNs.PageStore.create({
loaded: false,
type: type,
name: name,
});
IssuesListStoreNs.PageStoreLoadData(store);
return (
<IssuesListBoardsNs.IssuesListBoards store={store}/>
);
return <IssuesListBoardsNs.IssuesListBoards store={store} />;
};

View file

@ -7,22 +7,24 @@ 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 <div>Loading...</div>
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}/>
const board = (
<IssuesListBoardNs.IssuesListBoard store={boardData} key={key} />
);
list.push(board);
}
const topRightMenuStore = TopRightMenuNs.Store.create({visible: false});
const topRightMenuStore = TopRightMenuNs.Store.create({ visible: false });
const onAllReadItemClick = (e: React.MouseEvent) => {
e.stopPropagation();
SetIssuesReadingTimestamp(props.store.issueIds);

View file

@ -11,7 +11,7 @@ import { SpentHoursToFixed } from '../utils/spent-hours-to-fixed';
import { getStyleObjectFromString } from '../utils/style';
export type Props = {
store: IIssueStore
store: IIssueStore;
};
export const defaultPriorityStyleKey = 'priorityStyle';
@ -21,15 +21,28 @@ export const IssuesListCard = observer((props: Props): JSX.Element => {
const detailsStore = IssueDetailsDialogNs.Store.create({
issue: props.store,
visible: false,
unreadedFlagStore: unreadedStore
unreadedFlagStore: unreadedStore,
});
const priorityStyle = getStyleObjectFromString(props.store[defaultPriorityStyleKey]);
const tagsNewLine = (props.store.styledTags && props.store.styledTags.length > 0) ? <br/> : null;
const priorityStyle = getStyleObjectFromString(
props.store[defaultPriorityStyleKey],
);
const tagsNewLine =
props.store.styledTags && props.store.styledTags.length > 0 ? <br /> : null;
return (
<div className={Css.todoBlock} onClick={(e) => { e.stopPropagation(); detailsStore.show(); }}>
<div
className={Css.todoBlock}
onClick={(e) => {
e.stopPropagation();
detailsStore.show();
}}
>
<IssueDetailsDialogNs.IssueDetailsDialog store={detailsStore} />
<div className={Css.relevanceColor}>
<TimePassedNs.TimePassed params={{ fromIssue: { issue: props.store, keyName: 'timePassedClass' } }} />
<TimePassedNs.TimePassed
params={{
fromIssue: { issue: props.store, keyName: 'timePassedClass' },
}}
/>
</div>
<div className={Css.importantInformation}>
<span className={Css.issueSubject}>
@ -41,12 +54,18 @@ export const IssuesListCard = observer((props: Props): JSX.Element => {
/>
</span>
<span> </span>
<span className={Css.timeBox}>{SpentHoursToFixed(props.store.total_spent_hours)} / {SpentHoursToFixed(props.store.total_estimated_hours)}</span>
<span className={Css.timeBox}>
{SpentHoursToFixed(props.store.total_spent_hours)} /{' '}
{SpentHoursToFixed(props.store.total_estimated_hours)}
</span>
{tagsNewLine}
<TagsNs.Tags params={{ tags: props.store.styledTags }} />
<div className={Css.positionInfo}>
<span className={Css.timeBox}>{props.store.status.name}</span><span> </span>
<span className={Css.priorityBox} style={priorityStyle}>{props.store.priority.name}</span>
<span className={Css.timeBox}>{props.store.status.name}</span>
<span> </span>
<span className={Css.priorityBox} style={priorityStyle}>
{props.store.priority.name}
</span>
</div>
</div>
</div>

View file

@ -1,46 +1,49 @@
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<RedmineTypes.ExtendedIssue>();
export interface IIssueStore extends Instance<typeof IssueStore> {}
export type IIssueStore = Instance<typeof IssueStore>;
export const MetaInfoStore = types.model({
title: types.string,
url: types.maybe(types.string),
rootIssue: types.maybe(types.model({
rootIssue: types.maybe(
types.model({
id: 0,
tracker: types.model({
id: 0,
name: ''
name: '',
}),
subject: ''
}))
subject: '',
}),
),
});
export const BoardStore = types.model({
data: types.array(IssueStore),
metainfo: MetaInfoStore
metainfo: MetaInfoStore,
});
export interface IBoardStore extends Instance<typeof BoardStore> {}
export type IBoardStore = Instance<typeof BoardStore>;
export const PageStore = types.model({
export const PageStore = types
.model({
loaded: types.boolean,
type: types.string,
name: types.string,
data: types.maybeNull(
types.array(BoardStore)
)
}).actions((self) => {
data: types.maybeNull(types.array(BoardStore)),
})
.actions((self) => {
return {
setData: (data: any) => {
self.data = data;
self.loaded = true;
}
},
};
}).views((self) => {
})
.views((self) => {
return {
get issueIds(): number[] {
if (!self.data) return [];
@ -56,23 +59,27 @@ export const PageStore = types.model({
}
}
return res;
}
},
};
});
});
export async function PageStoreLoadData(store: IPageStore): Promise<void> {
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;
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>};
const item = resp.data[i] as { data: any[]; metainfo: Record<string, any> };
data.push({
metainfo: item.metainfo,
data: item.data ? item.data.map((group: { status: string, count: number, issues: any[] }) => {
return group.issues
}).flat() : []
data: item.data
? item.data
.map((group: { status: string; count: number; issues: any[] }) => {
return group.issues;
})
.flat()
: [],
});
}
@ -83,4 +90,4 @@ export async function PageStoreLoadData(store: IPageStore): Promise<void> {
store.setData(data);
}
export interface IPageStore extends Instance<typeof PageStore> {}
export type IPageStore = Instance<typeof PageStore>;

View file

@ -5,13 +5,16 @@ 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 (
<KanbanCard.KanbanCard store={card} key={card.issue.id}></KanbanCard.KanbanCard>
<KanbanCard.KanbanCard
store={card}
key={card.issue.id}
></KanbanCard.KanbanCard>
);
});
return (

View file

@ -5,7 +5,7 @@ import { observer } from 'mobx-react-lite';
import Column from './column';
export type Props = {
store: IBoardStore
store: IBoardStore;
};
export const KanbanBoard = observer((props: Props) => {
@ -18,14 +18,14 @@ export const KanbanBoard = observer((props: Props) => {
const columns = [];
for (let i = 0; i < props.store.data.length; i++) {
const column = props.store.data[i];
columns.push(<Column store={column}/>)
columns.push(<Column store={column} />);
}
return (
<>
<h1 id={props.store.metainfo.title}>{title} <a href={`#${props.store.metainfo.title}`}>#</a></h1>
<div className={KanbanBoardCss.kanbanContainer}>
{columns}
</div>
<h1 id={props.store.metainfo.title}>
{title} <a href={`#${props.store.metainfo.title}`}>#</a>
</h1>
<div className={KanbanBoardCss.kanbanContainer}>{columns}</div>
</>
);
});

View file

@ -8,13 +8,13 @@ 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 <div>Loading...</div>
return <div>Loading...</div>;
}
const list: any[] = [];
for (let i = 0; i < data.length; i++) {
@ -23,7 +23,7 @@ export const KanbanBoards = observer((props: Props) => {
const board = <KB.KanbanBoard store={boardData} key={key} />;
list.push(board);
}
const topRightMenuStore = TopRightMenuNs.Store.create({visible: false});
const topRightMenuStore = TopRightMenuNs.Store.create({ visible: false });
const onAllReadClick = (e: React.MouseEvent) => {
e.stopPropagation();
SetIssuesReadingTimestamp(props.store.issueIds);
@ -34,17 +34,21 @@ export const KanbanBoards = observer((props: Props) => {
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 = <button onClick={onTreeRefreshClick}>Force tree refresh</button>;
axios.get(
`${process.env.REACT_APP_BACKEND}simple-kanban-board/tree/${props.store.name}/refresh`,
);
};
treeRefreshMenuItem = (
<button onClick={onTreeRefreshClick}>Force tree refresh</button>
);
}
return (
<>
<TopRightMenuNs.TopRightMenu store={topRightMenuStore}>
<button onClick={onAllReadClick}>Всё прочитано</button>
{treeRefreshMenuItem}
<ServiceActionsButtons.IssuesForceRefreshButton/>
<ServiceActionsButtons.GetIssuesQueueSizeButton/>
<ServiceActionsButtons.IssuesForceRefreshButton />
<ServiceActionsButtons.GetIssuesQueueSizeButton />
</TopRightMenuNs.TopRightMenu>
{list}
</>

View file

@ -9,7 +9,7 @@ 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 = {
@ -24,7 +24,7 @@ export const KanbanCardTag = (props: TagProps): JSX.Element => {
{props.tag}
</span>
);
}
};
/**
* Какие дальше требования к карточкам?
@ -43,33 +43,47 @@ 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 = <TagsNs.Tags params={{tags: tags}}/>
tagsSection = <TagsNs.Tags params={{ tags: tags }} />;
}
const timePassedParams: TimePassedNs.Params = {
fromIssue: {
issue: props.store.issue,
keyName: 'timePassedClass'
}
}
const unreadedStore = UnreadedFlagNs.CreateStoreFromLocalStorage(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
unreadedFlagStore: unreadedStore,
});
return (
<div className={KanbanCardCss.kanbanCard} onClick={(e) => {e.stopPropagation(); detailsStore.show();}}>
<div
className={KanbanCardCss.kanbanCard}
onClick={(e) => {
e.stopPropagation();
detailsStore.show();
}}
>
<IssueDetailsDialogNs.IssueDetailsDialog store={detailsStore} />
<div className={KanbanCardCss.kanbanCardTitle}>
<UnreadedFlagNs.UnreadedFlag store={unreadedStore}/>
<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>
<UnreadedFlagNs.UnreadedFlag store={unreadedStore} />
<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>
<div>Исп.: {props.store.issue.current_user.name}</div>
<div>Прио.: {props.store.issue.priority.name}</div>
<div>Версия: {props.store.issue.fixed_version?.name || ''}</div>
<div>Прогресс: {props.store.issue.done_ratio}</div>
<div>Трудозатраты: {props.store.issue.total_spent_hours} / {props.store.issue.total_estimated_hours}</div>
<div>
Трудозатраты: {props.store.issue.total_spent_hours} /{' '}
{props.store.issue.total_estimated_hours}
</div>
{tagsSection}
</div>
);

View file

@ -2,65 +2,70 @@ import { Instance, types } from 'mobx-state-tree';
import { RedmineTypes } from '../redmine-types';
import axios from 'axios';
export const IssueStore = types.frozen<RedmineTypes.ExtendedIssue>()
export const IssueStore = types.frozen<RedmineTypes.ExtendedIssue>();
export interface IIssueStore extends Instance<typeof IssueStore> {}
export type IIssueStore = Instance<typeof IssueStore>;
export const ColumnStore = types.model({
export const ColumnStore = types
.model({
status: '',
count: 0,
issues: types.array(IssueStore)
}).views((self) => {
issues: types.array(IssueStore),
})
.views((self) => {
return {
get cards(): ICardStore[] {
return self.issues.map(issue => {
return self.issues.map((issue) => {
return CardStore.create({
issue: issue
})
issue: issue,
});
});
},
};
});
}
}
});
export interface IColumnStore extends Instance<typeof ColumnStore> {}
export type IColumnStore = Instance<typeof ColumnStore>;
export const MetaInfoStore = types.model({
title: '',
url: types.maybe(types.string),
rootIssue: types.maybe(types.model({
rootIssue: types.maybe(
types.model({
id: 0,
tracker: types.model({
id: 0,
name: ''
name: '',
}),
subject: ''
}))
subject: '',
}),
),
});
export interface IMetaInfoStore extends Instance<typeof MetaInfoStore> {}
export type IMetaInfoStore = Instance<typeof MetaInfoStore>;
export const BoardStore = types.model({
data: types.array(ColumnStore),
metainfo: MetaInfoStore
metainfo: MetaInfoStore,
});
export interface IBoardStore extends Instance<typeof BoardStore> {}
export type IBoardStore = Instance<typeof BoardStore>;
export const PageStore = types.model({
export const PageStore = types
.model({
loaded: false,
type: '',
name: '',
data: types.maybeNull(
types.array(BoardStore)
)
}).actions(self => {
data: types.maybeNull(types.array(BoardStore)),
})
.actions((self) => {
return {
setData: (data: any) => {
self.data = data;
self.loaded = true;
}
},
};
}).views((self) => {
})
.views((self) => {
return {
get issueIds(): number[] {
if (!self.data) return [];
@ -80,19 +85,19 @@ export const PageStore = types.model({
return res;
},
get canTreeRefresh(): boolean {
return (self.type === 'tree');
}
return self.type === 'tree';
},
};
});
});
export async function PageStoreLoadData(store: IPageStore): Promise<void> {
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;
if (!resp?.data) return;
store.setData(resp.data);
}
export interface IPageStore extends Instance<typeof PageStore> { }
export type IPageStore = Instance<typeof PageStore>;
export type CardField = {
component: string;
@ -100,10 +105,8 @@ export type CardField = {
export const CardParamsStore = types.optional(
types.model({
fields: types.array(
types.frozen<CardField>()
),
autoCollapse: types.boolean
fields: types.array(types.frozen<CardField>()),
autoCollapse: types.boolean,
}),
{
fields: [
@ -112,16 +115,15 @@ export const CardParamsStore = types.optional(
{ component: 'text', label: 'Версия', path: 'fixed_version.name' },
{ component: 'text', label: 'Прогресс', path: 'done_ratio' },
{ component: 'labor_costs' },
{ component: 'tags', label: 'Tags', path: 'styledTags' }
{ component: 'tags', label: 'Tags', path: 'styledTags' },
],
autoCollapse: false,
}
},
);
export const CardStore = types.model({
issue: IssueStore,
params: CardParamsStore
params: CardParamsStore,
});
export interface ICardStore extends Instance<typeof CardStore> {}
export type ICardStore = Instance<typeof CardStore>;

View file

@ -9,11 +9,13 @@ import { SetIssueReadingTimestamp } from '../utils/unreaded-provider';
import axios from 'axios';
import * as Luxon from 'luxon';
export const Store = types.model({
export const Store = types
.model({
visible: types.boolean,
issue: types.frozen<RedmineTypes.ExtendedIssue>(),
unreadedFlagStore: types.maybe(UnreadedFlagNs.Store)
}).actions((self) => {
unreadedFlagStore: types.maybe(UnreadedFlagNs.Store),
})
.actions((self) => {
return {
hide: () => {
console.debug(`Issue details dialog hide: issue_id=${self.issue.id}`); // DEBUG
@ -27,18 +29,19 @@ export const Store = types.model({
} else {
SetIssueReadingTimestamp(self.issue.id);
}
}
},
};
}).views((self) => {
})
.views((self) => {
return {
get displayStyle(): React.CSSProperties {
return {display: self.visible ? 'block' : 'none'};
}
return { display: self.visible ? 'block' : 'none' };
},
};
});
});
export type Props = {
store: Instance<typeof Store>
store: Instance<typeof Store>;
};
export const IssueDetailsDialog = observer((props: Props): JSX.Element => {
@ -53,7 +56,11 @@ export const IssueDetailsDialog = observer((props: Props): JSX.Element => {
};
return (
<div className={Css.reset}>
<div className={Css.modal} style={props.store.displayStyle} onClick={onCloseClick}>
<div
className={Css.modal}
style={props.store.displayStyle}
onClick={onCloseClick}
>
<div className={Css.modalContent}>
<h1>
<button onClick={onCloseClick}>close</button>
@ -65,17 +72,18 @@ export const IssueDetailsDialog = observer((props: Props): JSX.Element => {
url={props.store.issue?.url?.url || ''}
/>
</h1>
<hr/>
<hr />
<div>
<h2>Описание:</h2>
<pre>
{props.store.issue.description}
</pre>
<pre>{props.store.issue.description}</pre>
</div>
<hr/>
<hr />
<div>
<h2>Комментарии:</h2>
<Comments details={props.store.issue.journals || []} issue={props.store.issue}/>
<Comments
details={props.store.issue.journals || []}
issue={props.store.issue}
/>
</div>
</div>
</div>
@ -83,33 +91,36 @@ export const IssueDetailsDialog = observer((props: Props): JSX.Element => {
);
});
export const Comments = (props: {details?: RedmineTypes.Journal[], issue: RedmineTypes.ExtendedIssue}): JSX.Element => {
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</>
return <>No comments</>;
}
const list = comments.map((detail) => {
const key = `issueid_${props.issue.id}_commentid_${detail.id}`;
return <Comment data={detail} key={key}/>
return <Comment data={detail} key={key} />;
});
return (
<>{list}</>
);
}
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");
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 (
<>
<h3><span className={Css.dateField}>{date}</span> {props.data.user.name}:</h3>
<h3>
<span className={Css.dateField}>{date}</span> {props.data.user.name}:
</h3>
<div>
<pre>
{props.data.notes || '-'}
</pre>
<pre>{props.data.notes || '-'}</pre>
</div>
<hr/>
<hr />
</>
);
}
};

View file

@ -9,6 +9,8 @@ export type Props = {
export const IssueHref = (props: Props): JSX.Element => {
return (
<a href={props.url}>{props.tracker} #{props.id} - {props.subject}</a>
<a href={props.url}>
{props.tracker} #{props.id} - {props.subject}
</a>
);
};

View file

@ -17,4 +17,4 @@ export const Tag = (props: Props): JSX.Element => {
</span>
</>
);
}
};

View file

@ -7,21 +7,23 @@ export type Params = {
};
export type Props = {
params: Params
params: Params;
};
export const Tags = (props: Props): JSX.Element => {
if (!props.params.tags) {
return (<></>);
return <></>;
}
let label = props.params.label || '';
if (label) label = `${label}: `;
const tags = props.params.tags.map((tag) => {
return <TagNs.Tag tag={tag.tag} style={tag.style} key={tag.tag}/>;
const tags =
props.params.tags.map((tag) => {
return <TagNs.Tag tag={tag.tag} style={tag.style} key={tag.tag} />;
}) || [];
return (
<>
{label}{tags}
{label}
{tags}
</>
);
}
};

View file

@ -4,14 +4,14 @@ import { RedmineTypes } from '../redmine-types';
export type Params = {
fromIssue?: {
issue: RedmineTypes.ExtendedIssue,
keyName: string,
},
fromValue?: string
issue: RedmineTypes.ExtendedIssue;
keyName: string;
};
fromValue?: string;
};
export type Props = {
params: Params
params: Params;
};
export const TimePassed = (props: Props): JSX.Element => {
@ -21,13 +21,15 @@ export const TimePassed = (props: Props): JSX.Element => {
let timePassedClassName = ''; // TODO
if (props.params.fromIssue) {
const { issue, keyName } = props.params.fromIssue;
timePassedClassName = `${Css.timepassedDot} ${getClassName(issue[keyName])}`;
timePassedClassName = `${Css.timepassedDot} ${getClassName(
issue[keyName],
)}`;
} else if (props.params.fromValue) {
timePassedClassName = `${Css.timepassedDot} ${getClassName(props.params.fromValue)}`;
timePassedClassName = `${Css.timepassedDot} ${getClassName(
props.params.fromValue,
)}`;
}
return (
<span className={timePassedClassName}></span>
);
return <span className={timePassedClassName}></span>;
};
function getClassName(value: string): string {

View file

@ -3,17 +3,20 @@ 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) => {
export const Store = types
.model({
visible: types.boolean,
})
.views((self) => {
return {
get style(): React.CSSProperties {
return {
display: self.visible ? 'block' : 'none'
display: self.visible ? 'block' : 'none',
};
}
},
};
}).actions((self) => {
})
.actions((self) => {
return {
show: () => {
self.visible = true;
@ -23,9 +26,9 @@ export const Store = types.model({
},
toggle: () => {
self.visible = !self.visible;
}
},
};
});
});
export type Props = {
store: Instance<typeof Store>;
@ -40,16 +43,22 @@ export const TopRightMenu = observer((props: Props): JSX.Element => {
menuItems.push(<li key={key}>{item}</li>);
}
} else if (props.children) {
menuItems.push(<li key={0}>{props.children}</li>)
menuItems.push(<li key={0}>{props.children}</li>);
}
return (
<>
<button className={Css.menuButton} onClick={(e) => {e.stopPropagation(); props.store.toggle();}}>Menu</button>
<button
className={Css.menuButton}
onClick={(e) => {
e.stopPropagation();
props.store.toggle();
}}
>
Menu
</button>
<div className={Css.menu} style={props.store.style}>
<ul>
{menuItems}
</ul>
<ul>{menuItems}</ul>
</div>
</>
);
})
});

View file

@ -3,18 +3,24 @@ 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({
export const Store = types
.model({
issue: types.frozen<RedmineTypes.ExtendedIssue>(),
readingTimestamp: types.number
}).actions((self) => {
readingTimestamp: types.number,
})
.actions((self) => {
return {
read: () => {
self.readingTimestamp = SetIssueReadingTimestamp(self.issue.id);
}
},
};
}).views((self) => {
})
.views((self) => {
return {
getUpdatedTimestap(): number {
if (self.issue.journals) {
@ -27,7 +33,7 @@ export const Store = types.model({
}
}
if (lastComment) {
return (new Date(lastComment.created_on)).getTime();
return new Date(lastComment.created_on).getTime();
}
}
return 0;
@ -38,27 +44,35 @@ export const Store = types.model({
if (self.readingTimestamp < updatedTimestamp) {
className += ` ${Css.unreaded}`;
}
console.debug(`Unreaded flag getClassName: issueId=${self.issue.id}; readingTimestamp=${self.readingTimestamp}; updatedTimestamp=${updatedTimestamp}; className=${className}`); // DEBUG
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
readingTimestamp: timestamp,
});
}
export type Props = {
store: Instance<typeof Store>
}
store: Instance<typeof Store>;
};
export const UnreadedFlag = observer((props: Props): JSX.Element => {
const className = props.store.getClassName();
return (
<span className={className} onClick={(e) => {e.stopPropagation(); props.store.read();}}></span>
<span
className={className}
onClick={(e) => {
e.stopPropagation();
props.store.read();
}}
></span>
);
})
});

View file

@ -16,12 +16,23 @@ export const Basement = (props: Props): JSX.Element => {
<div className={BasementCss.basementGrid}>
<div className={BasementCss.bottomContacts}>
<a href="/">
<img src={props.iconUrl} alt="event_emitter_eltex_loc" className={BasementCss.eventEmitterEltexLoc} />
<img
src={props.iconUrl}
alt="event_emitter_eltex_loc"
className={BasementCss.eventEmitterEltexLoc}
/>
<span>redmine-issue-event-emitter</span>
</a>
<p><a href={props.contactUrl}> Проект
<span className={BasementCss.textBoxTextOrange}> Павел Гнедов</span>
</a></p>
<p>
<a href={props.contactUrl}>
{' '}
Проект
<span className={BasementCss.textBoxTextOrange}>
{' '}
Павел Гнедов
</span>
</a>
</p>
</div>
<div className={BasementCss.discuss}>
@ -30,8 +41,12 @@ export const Basement = (props: Props): JSX.Element => {
<p className={BasementCss.discussText}> ОБСУДИТЬ </p>
</a>
</div>
<img src={props.characterUrl} width="100" alt="Сharacter" className={BasementCss.character02} />
<img
src={props.characterUrl}
width="100"
alt="Сharacter"
className={BasementCss.character02}
/>
</div>
</div>
</div>

View file

@ -6,11 +6,7 @@ export type Props = {
};
export const Content = (props: Props) => {
return (
<div className={ContentCss.content}>
{props.children}
</div>
);
return <div className={ContentCss.content}>{props.children}</div>;
};
export default Content;

View file

@ -8,7 +8,11 @@ export type CoverProps = {
export const Cover = (props: CoverProps) => {
return (
<div className={CoverCss.cover}>
<img src="/images/Сharacter_01.png" alt="Сharacter" className={CoverCss.character} />
<img
src="/images/Сharacter_01.png"
alt="Сharacter"
className={CoverCss.character}
/>
<div className={CoverCss.info}>
<h3>Redmine Issue Event Emitter</h3>
<h1>ОБРАБОТКА И АНАЛИЗ ЗАДАЧ ИЗ "REDMINE"</h1>

View file

@ -8,12 +8,20 @@ export type Props = {
};
export const NotificationBlock = (props: Props) => {
const taskTitle = props?.taskTitle
? (<span className={NotificationBlockCss.text_box_text_blue}>{props.taskTitle} </span>)
: (<></>);
const taskTitle = props?.taskTitle ? (
<span className={NotificationBlockCss.text_box_text_blue}>
{props.taskTitle}{' '}
</span>
) : (
<></>
);
return (
<div className={NotificationBlockCss.message}>
<img src={props.avatarUrl} alt="event_emitter_eltex_loc" className={NotificationBlockCss.event_emitter_eltex_loc_icon} />
<img
src={props.avatarUrl}
alt="event_emitter_eltex_loc"
className={NotificationBlockCss.event_emitter_eltex_loc_icon}
/>
<div className={NotificationBlockCss.text_box}>
<p className={NotificationBlockCss.text_box_text}>
{taskTitle}

View file

@ -10,7 +10,7 @@ import TopBar from './top-bar';
export const StartPageData = {
contact: 'https://t.me/pavelgnedov',
bot: 'https://t.me/eltex_event_emitter_bot'
bot: 'https://t.me/eltex_event_emitter_bot',
};
export const StartPage = () => {
@ -19,83 +19,120 @@ export const StartPage = () => {
<TopBar contact={StartPageData.contact} />
<Cover telegramBotUrl={StartPageData.bot} />
<Content>
<ContentBlock title='Возможности'>
<ContentBlock title="Возможности">
<ul>
<li>Уведомления в реальном времени о событиях из задач - изменения статусов, упоминания комментариев</li>
<li>
Уведомления в реальном времени о событиях из задач - изменения
статусов, упоминания комментариев
</li>
<li>Генерация и управления отчётами о задачах</li>
<li>Под капотом приложение фреймворк</li>
</ul>
</ContentBlock>
<ContentBlock title='Функции telegram бота'>
<ContentBlock title="Функции telegram бота">
<ul>
<li>Последний отчёт для дейли проект ECCM</li>
<li>Дополнительные функции для разработчиков
eccm:/current_issues_eccm - список текущих задач по статусам - выбираютсятолько задачи из актуальных версий в статусах, где нужна какая-то реакцияили возможна работа прямо сейчас</li>
<li>Скриншоты уведомления от бота:
Примеры уведомлений о новых задачах и об изменениях статусов:</li>
<li>
Дополнительные функции для разработчиков eccm:/current_issues_eccm
- список текущих задач по статусам - выбираютсятолько задачи из
актуальных версий в статусах, где нужна какая-то реакцияили
возможна работа прямо сейчас
</li>
<li>
Скриншоты уведомления от бота: Примеры уведомлений о новых задачах
и об изменениях статусов:
</li>
</ul>
<NotificationBlock
taskTitle='Feature #245005'
avatarUrl='/images/event_emitter_eltex_loc-49px.png'
taskTitle="Feature #245005"
avatarUrl="/images/event_emitter_eltex_loc-49px.png"
>
Реализовать поддержку нового протокола: <br/><br/>
Реализовать поддержку нового протокола: <br />
<br />
Стив Джобс изменил статус задачи с Feedback на Closed
</NotificationBlock>
<NotificationBlock
taskTitle='Feature #241201'
avatarUrl='/images/event_emitter_eltex_loc-49px.png'
taskTitle="Feature #241201"
avatarUrl="/images/event_emitter_eltex_loc-49px.png"
>
Добавить поддержку новых моделей: <br/><br/>
Добавить поддержку новых моделей: <br />
<br />
Билл Гейтс создал новую задачу и назначил её на вас
</NotificationBlock>
<p>Простые уведомления о движении задач - и больше ничего лишнего.
<p>
Простые уведомления о движении задач - и больше ничего лишнего.
Пример уведомления по личному упоминанию в задаче:
</p>
<NotificationBlock
taskTitle='Question #230033'
avatarUrl='/images/event_emitter_eltex_loc-49px.png'
taskTitle="Question #230033"
avatarUrl="/images/event_emitter_eltex_loc-49px.png"
>
Сергей Брин:<br/><br/>
@Ларри Пейдж@, у меня есть хорошая идея. Посмотри, пожалуйста, по описанию к этой задаче.
Сергей Брин:
<br />
<br />
@Ларри Пейдж@, у меня есть хорошая идея. Посмотри, пожалуйста, по
описанию к этой задаче.
</NotificationBlock>
<NotificationBlock
taskTitle='Bug #191122'
avatarUrl='/images/event_emitter_eltex_loc-49px.png'
taskTitle="Bug #191122"
avatarUrl="/images/event_emitter_eltex_loc-49px.png"
>
Исправление уязвимости<br/><br/>
Линус Торвальдс завершил разработку по задаче и передал вам на ревью<br/><br/>
Исправление уязвимости
<br />
<br />
Линус Торвальдс завершил разработку по задаче и передал вам на ревью
<br />
<br />
Кажется получилось поправить проблемку. Глянь мой MR.
</NotificationBlock>
<p>Можно задавать коллегам вопросы прямо из комментария задачи, неотрываясь от её содержимого. Уведомление доставится в считанные минуты с ссылкой на задачу и информацией от кого это уведомление.</p>
<p>Пример запроса моих текущих задач с помощью команды
<span className={NotificationBlockCss.text_box_text_blue}>/current_issues_eccm</span>
<p>
Можно задавать коллегам вопросы прямо из комментария задачи,
неотрываясь от её содержимого. Уведомление доставится в считанные
минуты с ссылкой на задачу и информацией от кого это уведомление.
</p>
<p>
Пример запроса моих текущих задач с помощью команды
<span className={NotificationBlockCss.text_box_text_blue}>
/current_issues_eccm
</span>
</p>
<NotificationBlock
avatarUrl='/images/event_emitter_eltex_loc-49px.png'
>
Бьёрн Страуструп:<br/><br/>
Re-opened:<br/><br/>
<span className={NotificationBlockCss.text_box_text_blue}> - Feature #223301: </span>
Дополнить stdlib новыми функциями (прио - P4, версия - C++23)<br/><br/>
In Progress:<br/><br/>
<span className={NotificationBlockCss.text_box_text_blue}> - Question #223411:</span>
<NotificationBlock avatarUrl="/images/event_emitter_eltex_loc-49px.png">
Бьёрн Страуструп:
<br />
<br />
Re-opened:
<br />
<br />
<span className={NotificationBlockCss.text_box_text_blue}>
{' '}
- Feature #223301:{' '}
</span>
Дополнить stdlib новыми функциями (прио - P4, версия - C++23)
<br />
<br />
In Progress:
<br />
<br />
<span className={NotificationBlockCss.text_box_text_blue}>
{' '}
- Question #223411:
</span>
Выпуск релиза C++23 (прио - P4, версия - C++23)
</NotificationBlock>
</ContentBlock>
</Content>
<Basement contactUrl={StartPageData.contact} characterUrl='/images/Сharacter_02.png' iconUrl='/images/event_emitter_eltex_loc-32px.png'/>
<Basement
contactUrl={StartPageData.contact}
characterUrl="/images/Сharacter_02.png"
iconUrl="/images/event_emitter_eltex_loc-32px.png"
/>
</div>
);
};

View file

@ -13,16 +13,35 @@ const TopBar = (props: TopBarProps): ReactElement => {
<div className={TopBarCss.containerTitle}>
<div className={TopBarCss.logo}>
<a href="/" className={TopBarCss.logo}>
<img src={LogoImg} alt="event_emitter_eltex_loc" className={TopBarCss.eventEmitterEltexLoc} />
<img
src={LogoImg}
alt="event_emitter_eltex_loc"
className={TopBarCss.eventEmitterEltexLoc}
/>
<span>redmine-issue-event-emitter</span>
</a>
</div>
{props.children}
<p><a href="/" target="_blank"> #документация</a></p>
<p><a href={props.contact} target="_blank" rel="noreferrer"> #контакты</a></p>
<p><a href="https://gnedov.info/" target="_blank" rel="noreferrer"> #блог</a></p>
<p>
<a href="/" target="_blank">
{' '}
#документация
</a>
</p>
<p>
<a href={props.contact} target="_blank" rel="noreferrer">
{' '}
#контакты
</a>
</p>
<p>
<a href="https://gnedov.info/" target="_blank" rel="noreferrer">
{' '}
#блог
</a>
</p>
</div>
</div>
);

View file

@ -1,9 +1,7 @@
import React from 'react';
export const UnknownPage = () => {
return (
<p>Unknown page</p>
)
return <p>Unknown page</p>;
};
export default UnknownPage;

View file

@ -1,10 +1,11 @@
import React from 'react';
import { onGetIssuesQueueSizeClick, onIssuesRefreshClick } from './service-actions';
import {
onGetIssuesQueueSizeClick,
onIssuesRefreshClick,
} from './service-actions';
export const IssuesForceRefreshButton = (): JSX.Element => {
return (
<button onClick={onIssuesRefreshClick}>Force issues refresh</button>
);
return <button onClick={onIssuesRefreshClick}>Force issues refresh</button>;
};
export const GetIssuesQueueSizeButton = (): JSX.Element => {

View file

@ -1,20 +1,30 @@
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)", "");
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));
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<void> => {
export const onGetIssuesQueueSizeClick = async (
e: React.MouseEvent,
): Promise<void> => {
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`);
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}`);

View file

@ -3,9 +3,11 @@
* @param a
* @returns
*/
export const SpentHoursToFixed = (a: number|string|null|undefined): string => {
export const SpentHoursToFixed = (
a: number | string | null | undefined,
): string => {
if (a === null || typeof a === 'undefined') return '-';
const res = (typeof a === 'number') ? a : Number(a);
const res = typeof a === 'number' ? a : Number(a);
if (!Number.isFinite(res)) return '-';
return `${parseFloat(res.toFixed(1))}`;
};

View file

@ -1,19 +1,21 @@
const formatStringToCamelCase = (str: string): string => {
const splitted = str.split("-");
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("")
.map((word) => word[0].toUpperCase() + word.slice(1))
.join('')
);
};
export const getStyleObjectFromString = (str: string): Record<string, string> => {
export const getStyleObjectFromString = (
str: string,
): Record<string, string> => {
const style = {} as Record<string, string>;
str.split(";").forEach(el => {
const [property, value] = el.split(":");
str.split(';').forEach((el) => {
const [property, value] = el.split(':');
if (!property) return;
const formattedProperty = formatStringToCamelCase(property.trim());

View file

@ -4,13 +4,13 @@ export function GetIssueReadingTimestamp(issueId: number): number {
}
export function SetIssueReadingTimestamp(issueId: number): number {
const now = (new Date()).getTime();
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();
const now = new Date().getTime();
for (let i = 0; i < issueIds.length; i++) {
const issueId = issueIds[i];
window.localStorage.setItem(getKey(issueId), String(now));