Добавлен признак новых сообщений
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 IssueHrefNs from '../misc-components/issue-href';
|
||||
import * as IssueDetailsDialogNs from '../misc-components/issue-details-dialog';
|
||||
import * as UnreadedFlagNs from '../misc-components/unreaded-flag';
|
||||
|
||||
export type Props = {
|
||||
store: IIssueStore
|
||||
};
|
||||
|
||||
export const IssuesListCard = observer((props: Props): JSX.Element => {
|
||||
const unreadedStore = UnreadedFlagNs.CreateStoreFromLocalStorage(props.store);
|
||||
const detailsStore = IssueDetailsDialogNs.Store.create({
|
||||
issue: props.store,
|
||||
visible: false
|
||||
visible: false,
|
||||
unreadedFlagStore: unreadedStore
|
||||
});
|
||||
return (
|
||||
<div className={Css.listItem} onClick={(e) => {e.stopPropagation(); detailsStore.show();}}>
|
||||
<IssueDetailsDialogNs.IssueDetailsDialog store={detailsStore}/>
|
||||
<div>
|
||||
<UnreadedFlagNs.UnreadedFlag store={unreadedStore}/>
|
||||
<TimePassedNs.TimePassed params={{ fromIssue: { issue: props.store, keyName: 'timePassedClass' } }}/>
|
||||
<span className={Css.issueSubject}>
|
||||
<IssueHrefNs.IssueHref
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import { getStyleObjectFromString } from '../utils/style';
|
|||
import * as TimePassedNs from '../misc-components/time-passed';
|
||||
import * as TagsNs from '../misc-components/tags';
|
||||
import * as IssueDetailsDialogNs from '../misc-components/issue-details-dialog';
|
||||
import * as UnreadedFlagNs from '../misc-components/unreaded-flag';
|
||||
|
||||
export type Props = {
|
||||
store: ICardStore
|
||||
|
|
@ -50,14 +51,17 @@ export const KanbanCard = observer((props: Props) => {
|
|||
keyName: 'timePassedClass'
|
||||
}
|
||||
}
|
||||
const unreadedStore = UnreadedFlagNs.CreateStoreFromLocalStorage(props.store.issue);
|
||||
const detailsStore = IssueDetailsDialogNs.Store.create({
|
||||
issue: props.store.issue,
|
||||
visible: false,
|
||||
})
|
||||
unreadedFlagStore: unreadedStore
|
||||
});
|
||||
return (
|
||||
<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>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -4,10 +4,13 @@ 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';
|
||||
import * as UnreadedFlagNs from '../misc-components/unreaded-flag';
|
||||
import { SetIssueReadingTimestamp } from '../utils/unreaded-provider';
|
||||
|
||||
export const Store = types.model({
|
||||
visible: types.boolean,
|
||||
issue: types.frozen<RedmineTypes.ExtendedIssue>()
|
||||
issue: types.frozen<RedmineTypes.ExtendedIssue>(),
|
||||
unreadedFlagStore: types.maybe(UnreadedFlagNs.Store)
|
||||
}).actions((self) => {
|
||||
return {
|
||||
hide: () => {
|
||||
|
|
@ -17,6 +20,11 @@ export const Store = types.model({
|
|||
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) => {
|
||||
|
|
|
|||
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