Добавлено модальное окно для просмотра комментариев к задаче
This commit is contained in:
parent
d60a082327
commit
44934a590c
5 changed files with 141 additions and 9 deletions
|
|
@ -5,14 +5,20 @@ import Css from './issues-list-card.module.css';
|
||||||
import * as TimePassedNs from '../misc-components/time-passed';
|
import * as TimePassedNs from '../misc-components/time-passed';
|
||||||
import * as TagsNs from '../misc-components/tags';
|
import * as TagsNs from '../misc-components/tags';
|
||||||
import * as IssueHrefNs from '../misc-components/issue-href';
|
import * as IssueHrefNs from '../misc-components/issue-href';
|
||||||
|
import * as IssueDetailsDialogNs from '../misc-components/issue-details-dialog';
|
||||||
|
|
||||||
export type Props = {
|
export type Props = {
|
||||||
store: IIssueStore
|
store: IIssueStore
|
||||||
};
|
};
|
||||||
|
|
||||||
export const IssuesListCard = observer((props: Props): JSX.Element => {
|
export const IssuesListCard = observer((props: Props): JSX.Element => {
|
||||||
|
const detailsStore = IssueDetailsDialogNs.Store.create({
|
||||||
|
issue: props.store,
|
||||||
|
visible: false
|
||||||
|
});
|
||||||
return (
|
return (
|
||||||
<div className={Css.listItem}>
|
<div className={Css.listItem} onClick={(e) => {e.stopPropagation(); e.preventDefault(); detailsStore.show();}}>
|
||||||
|
<IssueDetailsDialogNs.IssueDetailsDialog store={detailsStore}/>
|
||||||
<div>
|
<div>
|
||||||
<TimePassedNs.TimePassed params={{ fromIssue: { issue: props.store, keyName: 'timePassedClass' } }}/>
|
<TimePassedNs.TimePassed params={{ fromIssue: { issue: props.store, keyName: 'timePassedClass' } }}/>
|
||||||
<span className={Css.issueSubject}>
|
<span className={Css.issueSubject}>
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@
|
||||||
/*display: flex;*/
|
/*display: flex;*/
|
||||||
box-shadow: 0 0 3px rgba(0, 0, 0, 0.5);
|
box-shadow: 0 0 3px rgba(0, 0, 0, 0.5);
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
z-index: 100;
|
/* z-index: 100; */
|
||||||
}
|
}
|
||||||
|
|
||||||
.kanbanCard div {
|
.kanbanCard div {
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import { ICardStore } from './store';
|
||||||
import { getStyleObjectFromString } from '../utils/style';
|
import { getStyleObjectFromString } from '../utils/style';
|
||||||
import * as TimePassedNs from '../misc-components/time-passed';
|
import * as TimePassedNs from '../misc-components/time-passed';
|
||||||
import * as TagsNs from '../misc-components/tags';
|
import * as TagsNs from '../misc-components/tags';
|
||||||
|
import * as IssueDetailsDialogNs from '../misc-components/issue-details-dialog';
|
||||||
|
|
||||||
export type Props = {
|
export type Props = {
|
||||||
store: ICardStore
|
store: ICardStore
|
||||||
|
|
@ -49,11 +50,17 @@ export const KanbanCard = observer((props: Props) => {
|
||||||
keyName: 'timePassedClass'
|
keyName: 'timePassedClass'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
const detailsStore = IssueDetailsDialogNs.Store.create({
|
||||||
|
issue: props.store.issue,
|
||||||
|
visible: false,
|
||||||
|
})
|
||||||
return (
|
return (
|
||||||
<div className={KanbanCardCss.kanbanCard}>
|
<div className={KanbanCardCss.kanbanCard} onClick={(e) => {e.preventDefault(); e.stopPropagation(); detailsStore.show();}}>
|
||||||
|
<IssueDetailsDialogNs.IssueDetailsDialog store={detailsStore} />
|
||||||
<div className={KanbanCardCss.kanbanCardTitle}>
|
<div className={KanbanCardCss.kanbanCardTitle}>
|
||||||
<TimePassedNs.TimePassed params={timePassedParams}/>
|
<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>
|
<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.current_user.name}</div>
|
||||||
<div>Прио.: {props.store.issue.priority.name}</div>
|
<div>Прио.: {props.store.issue.priority.name}</div>
|
||||||
<div>Версия: {props.store.issue.fixed_version?.name || ''}</div>
|
<div>Версия: {props.store.issue.fixed_version?.name || ''}</div>
|
||||||
|
|
@ -61,7 +68,6 @@ export const KanbanCard = observer((props: Props) => {
|
||||||
<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}
|
{tagsSection}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
21
frontend/src/misc-components/issue-details-dialog.module.css
Normal file
21
frontend/src/misc-components/issue-details-dialog.module.css
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
.modal {
|
||||||
|
z-index: 1000;
|
||||||
|
position: fixed;
|
||||||
|
display: none;
|
||||||
|
padding-top: 100px;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
overflow: auto;
|
||||||
|
background-color: rgb(0, 0, 0);
|
||||||
|
background-color: rgba(0, 0, 0, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modalContent {
|
||||||
|
background-color: #fefefe;
|
||||||
|
margin: auto;
|
||||||
|
padding: 20px;
|
||||||
|
border: 1px solid #888;
|
||||||
|
width: 80%;
|
||||||
|
}
|
||||||
99
frontend/src/misc-components/issue-details-dialog.tsx
Normal file
99
frontend/src/misc-components/issue-details-dialog.tsx
Normal file
|
|
@ -0,0 +1,99 @@
|
||||||
|
import React, { useEffect } from 'react';
|
||||||
|
import { RedmineTypes } from '../redmine-types';
|
||||||
|
import { observer } from 'mobx-react-lite';
|
||||||
|
import { Instance, types } from 'mobx-state-tree';
|
||||||
|
import * as IssueHrefNs from '../misc-components/issue-href';
|
||||||
|
import Css from './issue-details-dialog.module.css';
|
||||||
|
|
||||||
|
export const Store = types.model({
|
||||||
|
visible: types.boolean,
|
||||||
|
issue: types.frozen<RedmineTypes.ExtendedIssue>()
|
||||||
|
}).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;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}).views((self) => {
|
||||||
|
return {
|
||||||
|
get displayStyle(): React.CSSProperties {
|
||||||
|
return {display: self.visible ? 'block' : 'none'};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
export type Props = {
|
||||||
|
store: Instance<typeof Store>
|
||||||
|
};
|
||||||
|
|
||||||
|
export const IssueDetailsDialog = observer((props: Props): JSX.Element => {
|
||||||
|
// DEBUG: begin
|
||||||
|
useEffect(() => {
|
||||||
|
console.debug(`Issue detailts dialog: issue_id=${props.store.issue.id}; subject=${props.store.issue?.subject || '-'}; description=${props.store.issue.description}; visible=${props.store.visible}`);
|
||||||
|
});
|
||||||
|
// DEBUG: end
|
||||||
|
return (
|
||||||
|
<div className={Css.modal} style={props.store.displayStyle}>
|
||||||
|
<div className={Css.modalContent}>
|
||||||
|
<h1>
|
||||||
|
<button onClick={(e) => {e.preventDefault(); e.stopPropagation(); props.store.hide();}}>close</button>
|
||||||
|
<IssueHrefNs.IssueHref
|
||||||
|
id={props.store.issue?.id || -1}
|
||||||
|
subject={props.store.issue?.subject || ''}
|
||||||
|
tracker={props.store.issue?.tracker?.name || ''}
|
||||||
|
url={props.store.issue?.url?.url || ''}
|
||||||
|
/>
|
||||||
|
</h1>
|
||||||
|
<hr/>
|
||||||
|
<div>
|
||||||
|
<h2>Описание:</h2>
|
||||||
|
<pre>
|
||||||
|
{props.store.issue.description}
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
<hr/>
|
||||||
|
<div>
|
||||||
|
<h2>Комментарии:</h2>
|
||||||
|
<Comments details={props.store.issue.journals || []} issue={props.store.issue}/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
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</>
|
||||||
|
}
|
||||||
|
console.debug(`Comments: details=${JSON.stringify(props.details)}`); // DEBUG
|
||||||
|
const list = comments.map((detail) => {
|
||||||
|
const key = `issueid_${props.issue.id}_commentid_${detail.id}`;
|
||||||
|
return <Comment data={detail} key={key}/>
|
||||||
|
});
|
||||||
|
return (
|
||||||
|
<>{list}</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Comment = (props: {data: RedmineTypes.Journal}): JSX.Element => {
|
||||||
|
console.debug(`Comment: data=${JSON.stringify(props.data)}`); // DEBUG
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<h3>{props.data.user.name}:</h3>
|
||||||
|
<div>
|
||||||
|
<pre>
|
||||||
|
{props.data.notes || '-'}
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
<hr/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue