Добавлен признак новых сообщений
This commit is contained in:
parent
e79a7cd492
commit
0dc7e0c4ad
6 changed files with 113 additions and 3 deletions
|
|
@ -6,20 +6,24 @@ 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';
|
import * as IssueDetailsDialogNs from '../misc-components/issue-details-dialog';
|
||||||
|
import * as UnreadedFlagNs from '../misc-components/unreaded-flag';
|
||||||
|
|
||||||
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 unreadedStore = UnreadedFlagNs.CreateStoreFromLocalStorage(props.store);
|
||||||
const detailsStore = IssueDetailsDialogNs.Store.create({
|
const detailsStore = IssueDetailsDialogNs.Store.create({
|
||||||
issue: props.store,
|
issue: props.store,
|
||||||
visible: false
|
visible: false,
|
||||||
|
unreadedFlagStore: unreadedStore
|
||||||
});
|
});
|
||||||
return (
|
return (
|
||||||
<div className={Css.listItem} onClick={(e) => {e.stopPropagation(); detailsStore.show();}}>
|
<div className={Css.listItem} onClick={(e) => {e.stopPropagation(); detailsStore.show();}}>
|
||||||
<IssueDetailsDialogNs.IssueDetailsDialog store={detailsStore}/>
|
<IssueDetailsDialogNs.IssueDetailsDialog store={detailsStore}/>
|
||||||
<div>
|
<div>
|
||||||
|
<UnreadedFlagNs.UnreadedFlag store={unreadedStore}/>
|
||||||
<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}>
|
||||||
<IssueHrefNs.IssueHref
|
<IssueHrefNs.IssueHref
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ 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';
|
import * as IssueDetailsDialogNs from '../misc-components/issue-details-dialog';
|
||||||
|
import * as UnreadedFlagNs from '../misc-components/unreaded-flag';
|
||||||
|
|
||||||
export type Props = {
|
export type Props = {
|
||||||
store: ICardStore
|
store: ICardStore
|
||||||
|
|
@ -50,14 +51,17 @@ export const KanbanCard = observer((props: Props) => {
|
||||||
keyName: 'timePassedClass'
|
keyName: 'timePassedClass'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
const unreadedStore = UnreadedFlagNs.CreateStoreFromLocalStorage(props.store.issue);
|
||||||
const detailsStore = IssueDetailsDialogNs.Store.create({
|
const detailsStore = IssueDetailsDialogNs.Store.create({
|
||||||
issue: props.store.issue,
|
issue: props.store.issue,
|
||||||
visible: false,
|
visible: false,
|
||||||
})
|
unreadedFlagStore: unreadedStore
|
||||||
|
});
|
||||||
return (
|
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} />
|
<IssueDetailsDialogNs.IssueDetailsDialog store={detailsStore} />
|
||||||
<div className={KanbanCardCss.kanbanCardTitle}>
|
<div className={KanbanCardCss.kanbanCardTitle}>
|
||||||
|
<UnreadedFlagNs.UnreadedFlag store={unreadedStore}/>
|
||||||
<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>
|
||||||
|
|
|
||||||
|
|
@ -4,10 +4,13 @@ import { observer } from 'mobx-react-lite';
|
||||||
import { Instance, types } from 'mobx-state-tree';
|
import { Instance, types } from 'mobx-state-tree';
|
||||||
import * as IssueHrefNs from '../misc-components/issue-href';
|
import * as IssueHrefNs from '../misc-components/issue-href';
|
||||||
import Css from './issue-details-dialog.module.css';
|
import Css from './issue-details-dialog.module.css';
|
||||||
|
import * as UnreadedFlagNs from '../misc-components/unreaded-flag';
|
||||||
|
import { SetIssueReadingTimestamp } from '../utils/unreaded-provider';
|
||||||
|
|
||||||
export const Store = types.model({
|
export const Store = types.model({
|
||||||
visible: types.boolean,
|
visible: types.boolean,
|
||||||
issue: types.frozen<RedmineTypes.ExtendedIssue>()
|
issue: types.frozen<RedmineTypes.ExtendedIssue>(),
|
||||||
|
unreadedFlagStore: types.maybe(UnreadedFlagNs.Store)
|
||||||
}).actions((self) => {
|
}).actions((self) => {
|
||||||
return {
|
return {
|
||||||
hide: () => {
|
hide: () => {
|
||||||
|
|
@ -17,6 +20,11 @@ export const Store = types.model({
|
||||||
show: () => {
|
show: () => {
|
||||||
console.debug(`Issue details dialog show: issue_id=${self.issue.id}`); // DEBUG
|
console.debug(`Issue details dialog show: issue_id=${self.issue.id}`); // DEBUG
|
||||||
self.visible = true;
|
self.visible = true;
|
||||||
|
if (self.unreadedFlagStore) {
|
||||||
|
self.unreadedFlagStore.read();
|
||||||
|
} else {
|
||||||
|
SetIssueReadingTimestamp(self.issue.id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}).views((self) => {
|
}).views((self) => {
|
||||||
|
|
|
||||||
16
frontend/src/misc-components/unreaded-flag.module.css
Normal file
16
frontend/src/misc-components/unreaded-flag.module.css
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
.circle {
|
||||||
|
height: 6px;
|
||||||
|
width: 6px;
|
||||||
|
background-color: #707070;
|
||||||
|
border: 2px solid #707070;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.circle.unreaded {
|
||||||
|
border: 2px solid #0000FF;
|
||||||
|
}
|
||||||
|
|
||||||
|
.circle.forMe {
|
||||||
|
background-color: #FF0000;
|
||||||
|
}
|
||||||
64
frontend/src/misc-components/unreaded-flag.tsx
Normal file
64
frontend/src/misc-components/unreaded-flag.tsx
Normal file
|
|
@ -0,0 +1,64 @@
|
||||||
|
import React from 'react';
|
||||||
|
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';
|
||||||
|
|
||||||
|
export const Store = types.model({
|
||||||
|
issue: types.frozen<RedmineTypes.ExtendedIssue>(),
|
||||||
|
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
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Props = {
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
})
|
||||||
14
frontend/src/utils/unreaded-provider.ts
Normal file
14
frontend/src/utils/unreaded-provider.ts
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
export function GetIssueReadingTimestamp(issueId: number): number {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getKey(issueId: number): string {
|
||||||
|
return `issue_read_${issueId}`;
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue