Skip to content

Commit

Permalink
Change and improve implementation for notifications
Browse files Browse the repository at this point in the history
Pagnination
The implementation for fetching notifications is changed. Instead of
keeping offset and limit in the state, the page is fetched by page
number instead (similar to the history).
The issue of overfetching on the last page was solved by calculating the
number of remaining items to be fetched for the last page.

Closes #288

Prevent assignee selection from overflowing
When the name of the assignee is too long it gets shortened by ellipses
in the workflow item table.

Closes #299

Display parent project and subproject name for notifications
The displaynames of the project and subproject are now displayed on the
notification page for each entry. If the user does not have permissions
to see the project/subproject it gets replaced by the word "Redacted".

Closes #298

Display correct names in notifications
If the user does not have the permission to see a
project/subproject/workflowitem, the displayname is not displayed in the
notification and is instead left blank.

Closes #292

Refactor Helpers
- Functions that are not used were removed
- Other functions were re-written for code clarity

Move 'Read All' button
To increase usability, the 'Read All' button was moved to the left side
of the page. It is now above the icon that indicates whether the
notification has been read which makes its purpose clearer. The color
has also been changed to black instead of blue.

Closes #301

Background color for unread notifications
To increase usability, the background color for unread notifications has
been changed to a light gray color.

Closes #300

Remove view button if disabled
- The 'view' button has been changed to an icon button instead of
floating action button to make the look a little leaner
- The button has been surrounded by a tooltip which tells the user what
the button does
- If the user does not have the permissions to see the
project/subproject/workflowitem, the button is not displayed instead of
disabled

Closes #302
  • Loading branch information
mathiashoeld committed May 23, 2019
1 parent 3f1647f commit 783f81a
Show file tree
Hide file tree
Showing 15 changed files with 227 additions and 331 deletions.
11 changes: 9 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,13 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).

## [Unreleased]
<!-- ### Added -->
### Added
- Different background color for unread notifications [#300](https://github.com/openkfw/TruBudget/issues/300)

<!-- ### Changed -->
### Changed
- Notification displays name of parent project and subproject [#298](https://github.com/openkfw/TruBudget/issues/298)
- Move 'Read All' button to the left side [#301](https://github.com/openkfw/TruBudget/issues/301)
- Don't display view button if user is not allowed to see project/subproject [#302](https://github.com/openkfw/TruBudget/issues/302)

<!-- ### Deprecated -->

Expand All @@ -17,6 +21,9 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
### Fixed

- Empty history displayed after API call is finished [#294](https://github.com/openkfw/TruBudget/issues/294)
- Last page of notifications displays correct number of items [#288](https://github.com/openkfw/TruBudget/issues/288)
- Prevent assignee selection from overflowing [#299](https://github.com/openkfw/TruBudget/issues/299)
- Display correct name in notifications [#292](https://github.com/openkfw/TruBudget/issues/292)

<!-- ### Security -->
## [1.0.1] - 2019-05-21
Expand Down
1 change: 1 addition & 0 deletions e2e-test/cypress/integration/notification_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ describe("open notifications", function() {
.then(() => cy.closeProject(projectId, "project.close", assignee))
.then(() => cy.login("jxavier"))
.then(() => cy.visit("/notifications"))
.then(() => cy.get("[data-test=notification-unread-0]").should("be.visible"))
);
});

Expand Down
2 changes: 1 addition & 1 deletion e2e-test/cypress/integration/workflowitem_history_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ describe("Workflowitem's history", function() {

it("The history is sorted from new to old", function() {
// Change assignee to create new history event
cy.get(".workflowitem-assignee").click();
cy.get("[data-test=workflowitem-assignee]").click();
cy.get("[role=listbox]")
.find("[value=jdoe]")
.click()
Expand Down
135 changes: 9 additions & 126 deletions frontend/src/helper.js
Original file line number Diff line number Diff line change
@@ -1,29 +1,24 @@
import React from "react";
import { Iterable } from "immutable";
import dayjs from "dayjs";

import OpenIcon from "@material-ui/icons/Remove";
import DoneIcon from "@material-ui/icons/Check";

import indigo from "@material-ui/core/colors/indigo";

import OpenIcon from "@material-ui/icons/Remove";
import accounting from "accounting";
import dayjs from "dayjs";
import { Iterable } from "immutable";
import _cloneDeep from "lodash/cloneDeep";
import _isEmpty from "lodash/isEmpty";
import _isEqual from "lodash/isEqual";
import _cloneDeep from "lodash/cloneDeep";
import _isUndefined from "lodash/isUndefined";
import _isString from "lodash/isString";
import _isUndefined from "lodash/isUndefined";
import React from "react";

import accounting from "accounting";
import strings from "./localizeStrings";
import currencies from "./currency";
import strings from "./localizeStrings";

const numberFormat = {
decimal: ".",
thousand: ",",
precision: 2
};

const statusColors = [indigo[100], indigo[300]];

export const toJS = WrappedComponent => wrappedComponentProps => {
const KEY = 0;
const VALUE = 1;
Expand Down Expand Up @@ -71,12 +66,6 @@ export const fromAmountString = (amount, currency) => {
return accounting.unformat(amount, getCurrencyFormat(currency).decimal);
};

export const formatAmountString = (amount, currency) => {
if (_isString(amount) && amount.trim().length <= 0) {
return "";
}
return amount;
};
export const getCurrencies = () => {
return Object.keys(currencies).map(currency => {
return {
Expand All @@ -97,11 +86,6 @@ export const toAmountString = (amount, currency) => {
return accounting.formatMoney(amount, getCurrencyFormat(currency));
};

export const tsToString = ts => {
let dateString = dayjs.unix(ts).format("MMM D, YYYY");
return dateString;
};

export const unixTsToString = ts => {
let dateString = dayjs.unix(ts).format("MMM D, YYYY");
return dateString;
Expand Down Expand Up @@ -136,112 +120,11 @@ export const statusIconMapping = {
open: <OpenIcon />
};

export const roleMapper = {
approver: strings.common.approver,
bank: strings.common.bank,
assignee: strings.common.assignee
};

export const createDoughnutData = (labels, data, colors = statusColors) => ({
labels,
datasets: [
{
data: data,
backgroundColor: colors,
hoverBackgroundColor: colors
}
]
});

export const calculateUnspentAmount = items => {
const amount = items.reduce((acc, item) => {
return acc + parseFloat(item.data.amount, 10);
}, 0);
return amount;
};

export const getCompletionRatio = subprojects => {
const completedSubprojects = getCompletedSubprojects(subprojects);
const percentageCompleted = completedSubprojects.length / subprojects.length * 100;
return percentageCompleted > 0 ? percentageCompleted : 0;
};

const getCompletedSubprojects = subprojects => {
const completedSubprojects = subprojects.filter(subproject => {
return subproject.data.status === "closed";
});
return completedSubprojects;
};

export const getCompletionString = subprojects => {
const completedSubprojects = getCompletedSubprojects(subprojects);
return strings.formatString(
strings.subproject.subproject_completion_string,
completedSubprojects.length,
subprojects.length
);
};

export const formatString = (text, ...args) => {
return strings.formatString(text, ...args);
};
export const formatUpdateString = (identifier, createdBy, data) => {
let string = strings.formatString(strings.history.changed_by, identifier, createdBy);
const changes = Object.keys(data)
.map(key => formatString(strings.history.to, key, data[key]))
.join(", ");
return string.concat(changes);
};

export const calculateWorkflowBudget = workflows => {
return workflows.reduce(
(acc, workflow) => {
const { amount, amountType, status } = workflow.data;
const parsedAmount = parseFloat(amount, 10);
const next = {
assigned: amountType === "allocated" ? acc.assigned + parsedAmount : acc.assigned,
disbursed: amountType === "disbursed" ? acc.disbursed + parsedAmount : acc.disbursed,
currentDisbursement:
amountType === "disbursed" && status === "closed"
? acc.currentDisbursement + parsedAmount
: acc.currentDisbursement
};
return next;
},
{
assigned: 0,
disbursed: 0,
currentDisbursement: 0
}
);
};

export const getNotAssignedBudget = (amount, assignedBudget, disbursedBudget) => {
const notAssigned = amount - assignedBudget - disbursedBudget;
return notAssigned >= 0 ? notAssigned : 0;
};

export const getProgressInformation = items => {
let startValue = {
open: 0,
closed: 0
};
const projectStatus = items.reduce((acc, item) => {
const status = item.data.status;
return {
open: status === "open" ? acc.open + 1 : acc.open,
closed: status === "closed" ? acc.closed + 1 : acc.closed
};
}, startValue);
return projectStatus;
};

export const preselectCurrency = (parentCurrency, setCurrency) => {
const preSelectedCurrency = _isUndefined(parentCurrency) ? "EUR" : parentCurrency;
setCurrency(preSelectedCurrency);
};

export const createTaskData = (items, type) => {
const projectStatus = getProgressInformation(items);
return createDoughnutData([strings.common.open, strings.common.closed], [projectStatus.open, projectStatus.closed]);
};
6 changes: 5 additions & 1 deletion frontend/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import relativeTime from "dayjs/plugin/relativeTime";

import red from "@material-ui/core/colors/deepOrange";
import blue from "@material-ui/core/colors/indigo";
import grey from "@material-ui/core/colors/grey";

import Main from "./pages/Main/Main";
import LoginPageContainer from "./pages/Login/LoginPageContainer";
Expand All @@ -31,7 +32,10 @@ const store = configureStore(initialState, history);
const muiTheme = createMuiTheme({
palette: {
primary: blue,
secondary: red
secondary: red,
grey: {
main: grey[100]
}
},
typography: {
useNextVariants: true
Expand Down
21 changes: 12 additions & 9 deletions frontend/src/pages/Notifications/FlyInNotifications.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import IconButton from "@material-ui/core/IconButton";
import LaunchIcon from "@material-ui/icons/ZoomIn";
import Typography from "@material-ui/core/Typography";

import { intentMapping, getDisplayName, parseURI, isAllowedToSee } from "./helper";
import { intentMapping, parseURI, isAllowedToSee, getParentData } from "./helper";

const styles = {
notification: {
Expand All @@ -32,6 +32,7 @@ export default class FlyInNotification extends Component {
const subprojectId = metadata.subproject ? metadata.subproject.id : undefined;
const { publisher } = businessEvent;
const message = intentMapping(notification);
const { projectDisplayName, subprojectDisplayName } = getParentData(notification);
return (
<Card
key={id + "flyin"}
Expand All @@ -43,15 +44,17 @@ export default class FlyInNotification extends Component {
<CardHeader
avatar={<Avatar>{publisher ? publisher[0].toString().toUpperCase() : "?"}</Avatar>}
action={
<IconButton
disabled={!isAllowedToSee(notification)}
color="primary"
onClick={() => history.push(parseURI({ projectId, subprojectId }))}
>
<LaunchIcon />
</IconButton>
isAllowedToSee(notification) ? (
<IconButton
disabled={!isAllowedToSee(notification)}
color="primary"
onClick={() => history.push(parseURI({ projectId, subprojectId }))}
>
<LaunchIcon />
</IconButton>
) : null
}
title={getDisplayName(notification)}
title={projectDisplayName + " " + subprojectDisplayName}
/>
<CardContent>
<Typography component="p">{message}</Typography>
Expand Down
Loading

0 comments on commit 783f81a

Please sign in to comment.