Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Issue/2009 Notifications Added #2064

Conversation

RachelDau
Copy link
Contributor

@RachelDau RachelDau commented Mar 2, 2023

For issue 2009 adds notification to obojobo. Right now these display in the Nav section of a module. You must physically type the information into the database notifications. It then uses the last login date and compares it to the notification created_at time to determine if the notification should be displayed and which one should be displayed.

Notification will be taken directly from the database, written to the cookie, pulled out of the cookie on the front-end side, and rendered from the dashboard repository nav file. If notifications are present (typed into Database) upon page refresh a red circle indicator will appear by the username when on a dashboard page. When you click the indicator or the username area it will toggle a notifications drop down. From here you can see and exit notifications which removes them from the cookie. If you input new notification into the database, the previous ones will disappear so they must be done together if you want them to appear together. You cannot use certain symbols such as ; in the cookie title or text and it is processed as a string and this symbol will mess up how the code processes the notifications from the cookie.

@RachelDau RachelDau changed the title Issue/2009 notification added WIP: Issue/2009 notification added Mar 2, 2023
Copy link
Contributor

@FrenjaminBanklin FrenjaminBanklin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Initial thoughts:

You might need to describe the new notification_status table a bit. It's not clear what the user_id, draft_id or status fields are for. It doesn't look like draft_id is made anywhere.

Ideally all available notifications should be objects in an array and handled programmatically instead of explicitly defining notificationText and notificationTitle to handle only one notification at a time.

If the idea for notifications is that we aren't tracking which ones have been seen by which users, then the three fields mentioned above shouldn't be necessary. In order to preserve notifications between page loads, look into adding them to a cookie (see here for an example of using cookies) and changing the utilities to adjust what's saved in the cookies.

More specific comments per file below.

Comment on lines 102 to 105
.then(() => viewerUsersState.getLastLogin(userId))
.then(result => {
if (result) lastLogin = result.last_login
})
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ideally req.currentUser would just have a lastLogin property that you could check, rather than creating a whole new function to get that information separately.

Comment on lines 106 to 121
.then(() => viewerNotificationState.getId(lastLogin))
.then(result => {
if (result) id = result.id
})
.then(() => viewerNotificationState.getStatus(id))
.then(result => {
if (result) isNotificationEnabled = result.status
})
.then(() => viewerNotificationState.getTitle(id))
.then(result => {
if (result) notificationTitle = result.title
})
.then(() => viewerNotificationState.getText(id))
.then(result => {
if (result) notificationText = result.text
})
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Try to provide all valid notifications to the frontend in a single array rather than going through multiple steps to grab individual pieces of a single notification like this.

Comment on lines 143 to 145
isNotificationEnabled,
notificationTitle,
notificationText
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See above comment: rather than informing the frontend of a single notification with three new properties, try to provide all valid notifications as objects in a single array.

Comment on lines 3 to 38
function getStatus(id) {
return db.oneOrNone(
`
SELECT status FROM notification_status
WHERE
id = $[id]
`,
{
id
}
)
}
function getTitle(id) {
return db.oneOrNone(
`
SELECT title FROM notification_status
WHERE
id = $[id]
`,
{
id
}
)
}
function getText(id) {
return db.oneOrNone(
`
SELECT text FROM notification_status
WHERE
id = $[id]
`,
{
id
}
)
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't create multiple functions for each individual field, just have a single getNotifications(ids) function to grab multiple notifications - pass it an array (could even be an array containing a single number) and handle results accordingly.

Comment on lines 39 to 50
function getId(lastLoginDate) {
return db.oneOrNone(
`
SELECT id FROM notification_status
WHERE created_at >= $[lastLoginDate]
LIMIT 1
`,
{
lastLoginDate
}
)
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Several things here:

  • Change the name from getId to something more descriptive like getRecentNotifications.
  • Generalize lastLoginDate to targetDate or maybe even just date.
  • Use manyOrNone instead of oneOrNone and remove the LIMIT so this function grabs all notifications after a given date.
  • Add an ORDER BY created_at ASC so notifications always appear in a predictable order.

Comment on lines 1 to 41
const db = oboRequire('server/db')
//const logger = oboRequire('server/logger')
/*
function setLastLogin(userId){
const date = new Date();
const today = date.getDate();
return db
.none(
`
INSERT INTO users
(id, last_login)
VALUES($[userId], $[today])
ON CONFLICT (id) DO UPDATE
SET last_login = $[today]
WHERE id = $[userId]
`,
{
userId,
today
}
)
.catch(error => {
logger.error('DB UNEXPECTED on users.set', error, error.toString())
})
} */
function getLastLogin(userId) {
return db.oneOrNone(
`
SELECT last_login FROM users
WHERE id = $[userId]
`,
{
userId
}
)
}

module.exports = {
//setLastLogin,
getLastLogin
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See if it's possible to attach the last login date to req.currentUser instead of creating a separate function like this to look it up separately.

}

exports.down = function(db) {
return null
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The down of a migration is the opposite of the up. In this case, the down of this migration should remove the last_login column.

@RachelDau RachelDau changed the base branch from dev/30-howlite to dev/31-taconite March 21, 2023 15:47
@RachelDau
Copy link
Contributor Author

While previously mentioned issues have been addressed current known issues now include:

  • last_login is not updating in the user table
  • notifications are no re-rendering upon exit button press
  • removing the notifications from the cookie has not successfully been implemented
  • unit tests for visits.js changes are unwritten... and there are jest failures from tests in that file
  • a CSS cleanup and style improvement is needed for the notifications

@RachelDau RachelDau requested a review from Viggie1 May 16, 2023 20:19
@RachelDau RachelDau changed the base branch from dev/31-taconite to dev/32-pigeonite September 18, 2023 14:45
@RachelDau
Copy link
Contributor Author

merged with latest dev branch to solidify new node version , no notifications changes were included in the merge.

@RachelDau RachelDau changed the base branch from dev/32-pigeonite to dev/33-serpierite September 21, 2023 13:23
@RachelDau
Copy link
Contributor Author

Taking into account all previous feedback, notifications are now in a pop-up with some style changes. Some CSS styles were reorganized and jest tested added for the new notification indicator. It now tracks the number of notifications present in a callback function from the notification component to repository-nav. When exiting the indicator updates immediately.

As usually, after migrating the db up to create the notification table type directly into it to add notifications with a id, title, and text.

Any and all feedback is appreciated :)

Copy link
Contributor

@FrenjaminBanklin FrenjaminBanklin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is looking pretty good.

The main things I can see that still need addressing are tab reachability and click handlers. Currently the notifications indicator is not tab-reachable, so it can only be activated by clicking it directly. When the notifications are visible, only the 'close' buttons are tab-reachable so there's currently no way of screen readers reaching the text content.

It looks like clicking anywhere except one of the notification's 'X' buttons dismisses the notifications popup entirely. Even clicking inside one of the notifications.

@RachelDau
Copy link
Contributor Author

Updated to include notification indicator in tab order and make notifications tab keyboard accessible. Clicking inside the notification no longer exits the pop-up.

@RachelDau
Copy link
Contributor Author

Updated the screen for when all notifications are exited (style update)

@RachelDau
Copy link
Contributor Author

RachelDau commented Mar 28, 2024

These are ready for review

In order to test locally, you must migrate the db up to create the new notification table and last login in the users table. yarn db:migrateup You then must type directly in the database notification table the content for the notification (id, title, and text). Let me know if you need help.

Notification will be taken directly from the database, written to the cookie, pulled out of the cookie on the front-end side, and rendered from the dashboard repository nav file. If notifications are present (typed into Database) upon page refresh a number of notifications text indicator will appear under the username when on a dashboard page.

When you click the indicator or the username area it will open a notifications pop-up. From here you can see and exit notifications which removes them from the cookie. If you input new notification into the database, the previous ones will disappear so they must be done together if you want them to appear together. (they will also disappear on re-login)

You cannot use certain symbols such as ; in the cookie title or text and it is processed as a string and this symbol will mess up how the code processes the notifications from the cookie. Also if the title is one word thats more than 37 characters in length that may be an issue.

@RachelDau RachelDau linked an issue Apr 10, 2024 that may be closed by this pull request
Copy link
Contributor

@FrenjaminBanklin FrenjaminBanklin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry this has taken me so long to review.

This has shaped up really well, the only big thing I can see that needs doing now is controlling tab flow. You'll notice that other modal popups (the Module Options window, the user search sub-window in the Share dialog, etc.) only allow the tab key to reach elements that are within the currently open modal dialog window.

I'm not sure exactly how this is being accomplished, but I have a feeling it's a result of using the ReactModal component (see how it's used in the Dashboard component).

If you can figure out how to get the notifications working within ReactModal, you may not have to do as much tab index management yourself.

Also, I feel like we could potentially sanitize cookie string characters such as ;, but avoiding them should be easy enough. Addressing reserved/unsafe characters can be a future issue.

@RachelDau
Copy link
Contributor Author

Alright I added React modal so that the notifications window is tab accessible and you can't tab back to the main page when accessing. The text and title seem to still need the tabIndex on them to be tab accessible. One thing to note is now clicking outside of the notification window will also close it as this is a sort of default when using modal. Adding modal caused a lot of issues with other tests and so a variety of files that use the repository-nav component and test files for those files needed appropriate mocks of react modal added in order to continue functioning. A few Dashboard tests needed a slight rewrite because they hinged on their being one modal and now there are two with notifications so the tests were modified to ensure that is reflected.

@FrenjaminBanklin FrenjaminBanklin changed the base branch from dev/33-serpierite to dev/34-bismuth May 2, 2024 14:46
Copy link
Contributor

@FrenjaminBanklin FrenjaminBanklin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is nearly ready to go - just a few very minor adjustments to unit tests and one correction to one of the new migrations.

I think the preferred tab order per notification would go title -> message -> close button, but that would require adjusting the order in which the elements are rendered and probably corresponding changes to the styles to make things show up properly. It can wait to be a follow-up issue, I think.

}

exports.down = function(db) {
return db.removeColumn('last_login')
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

db.removeColumn takes two arguments, the first being the table name.

This should read return db.removeColumn('users', 'last_login').

tree = component.toJSON()
expect(tree).toMatchSnapshot()
expect(document.cookie).toBe(
'notifications=%5B%7B%22key%22%3A2%2C%22text%22%3A%22Notification2%22%2C%22title%22%3A%22Title2%22%7D%5D;'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See above comment re: variables and encodeURIComponent.

expect(tree).toMatchSnapshot()

if (document && document.cookie) {
//don't get here
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could probably have some expects above this to check that document has a value but document.cookie does not, rather than this if/else.

cookiePropsRaw.forEach(c => {
const parts = c.trim().split('=')

if (parts[0] === 'notifications') {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See previous comment re: using expect to fail early instead of relying on an if/else to check for success.

@RachelDau
Copy link
Contributor Author

I tested migrations up and down and it all appears to be good now. I also regenerated migration files so they are new. Unit test changes were made to reflect using expect and not if else statements.

Copy link
Contributor

@FrenjaminBanklin FrenjaminBanklin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I might not have noticed a few things in my last pass, but with a few more minor revisions to the unit tests I think this will be all set.

const key = 0
const exitButtons = component.root.findAllByProps({ className: 'notification-exit-button' })

if (exitButtons[key]) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry if I didn't comment on or notice this before. Feels like I keep sending you back for different things every time.

Is there any scenario where this test should run successfully and exitButtons[key] would evaluate falsy, or if exitButtons[key] is falsy does that mean something went wrong?

Based on the findAllByProps line above I would expect a case where exitButtons[key] not resolving to something truthy to indicate that there was a problem. In a case like this you could use https://jestjs.io/docs/expect#tobetruthy, or forego the key variable altogether and expect(exitButtons.length).toBe(0) or even expect(exitButtons.length > 0).toBe(true).

tree = component.toJSON()
expect(tree).toMatchSnapshot()

if (elementToExit) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is another case where you should use expect rather than if. Ideally you should populate elementToExit and then have an immediate expect to make sure it's what you expect it to be, at which point this if would be unnecessary because you've already made sure it exists.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, removing the if worked perfectly. Seems it was redundant to check if it existed and then expect it to exist and be undefined in value

const tree = component.toJSON()
expect(tree).toMatchSnapshot()

if (document && document.cookie) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this test is only being run one time and should only be testing one thing, there should probably not be an if...else here to determine which expectations are run. In this case you could replace this if...else with two expectations:
expect(document).toBeTruthy() and expect(document.cookie).toBeTruthy(). At that point you can reasonably assume that only the if block would ever run, so you wouldn't need the else block or the if...else at all.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oops i totally missed this but rewrote the very similar tests yesterday. Fixing now

@RachelDau
Copy link
Contributor Author

Alright, I removed the if statements from testing as they weren't needed and added more expects to ensure things are tested. When testing make sure to test how it handles no notifications and an empty table. It was working when I tested it I just have a weird fear about that breaking everything.

@RachelDau
Copy link
Contributor Author

image
Something I noticed is because I check that the date is changed to today using a function every so once in awhile the test is off by a second because of the time it takes to set last login and then compare to todays date that a second has passed by the time it is called. Repro rate seems to be every 1/10 tests or so.

@FrenjaminBanklin
Copy link
Contributor

Hmm. The values of today being off due to execution time is probably something that needs to be resolved now.

Quick Googling brings up this SO article discussing how to mock the constructor for Date, which is probably what we'd have to do here to make sure results are consistent.

@RachelDau
Copy link
Contributor Author

My latest commit sets a mock timer to a specific date and using that set date to reference and test, eliminating any dependency on the execution time. Thanks for the link it helped!

Copy link
Contributor

@FrenjaminBanklin FrenjaminBanklin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tests are all passing, and I think the code is looking pretty good. Anything I can think to change later will be a separate issue.

@FrenjaminBanklin FrenjaminBanklin merged commit 281573a into ucfopen:dev/34-bismuth May 9, 2024
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Notifications.
2 participants