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

Allow users to create deliveries #21

Merged
merged 37 commits into from
Aug 14, 2018
Merged

Allow users to create deliveries #21

merged 37 commits into from
Aug 14, 2018

Conversation

johnbradley
Copy link
Contributor

@johnbradley johnbradley commented Jul 26, 2018

Adds changes to allow a user to select a project, recipient and enter a message to be included in the email.
High Level Changes

  • New Delivery button to the Outgoing deliveries
  • deliveries/new/select-project route where user selects a project to deliver
  • new/select-recipient?project_id=<id> route where user selects recipient and share users
  • deliveries/new/enter-user-message?project_id=<id>&to_user_id=<id>... route to enter a message and send the delivery

Future changes

  • make use of DukeDS affiliates for selecting recipient and share users

Needs Duke-GCB/D4S2#169

Fixes #6

Adds
deliveries/new/select-project route
deliveries/new/select-recipient route
deliveries/new/enter-user-message route

Passses IDs as queryParams between routes

TODO:
replace underscore query params with camel case
filter projects to those we can actually deliver
filter recipient to not include current user
cleanup select recipient screen - still confusing
Adds checkboxes for selecting projects and users.
Filters users to exclude current user.
also fixes failing new tests for controllers
application now contains logic for currentDukeDsUser
so this is no longer needed.
adds tests for new routes
Th computed field shareUsers returns a javascript array of promises
so we can't just call `then` on the returned array.
@johnbradley johnbradley requested a review from dleehr July 30, 2018 15:16
Copy link
Member

@dleehr dleehr left a comment

Choose a reason for hiding this comment

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

Code looks good, only had a couple in-line observations. I wanted to see how this worked, so most of the feedback below is about the select-recipients page:

  • A label/heading for the primary recipient should be more prominent (maybe an h4?), and indicate that this is required.
  • "Select additional users to share project." - The meaning of this text is not clear. This text should describe that this area is for selecting additional users that will get access to the data but not ownership, and that this is optional.
  • While the table of users is sortable, it doesn't come up in any discernable order. Perhaps sort by last name initially? (That could be done in the JS)
  • There are some empty rows in my listing - We should probably filter these out in the UI. I can't imagine any good things can come from delivering to these users :)
  • The name and email filters are case-sensitive, which I didn't expect or discover right away. At first I thought it was broken. I used a filterFunction for doing case-insensitive filtering on project statuses, and that may help here.
  • It's possible to select the primary user as one of the additional "share with" users, which doesn't make sense.
  • The more I think about this, the more I think the "picking additional users" part should be a separate step. It makes it easy to filter out the primary user and lets the user focus or skip that step more easily.

import Ember from 'ember';

export default Ember.Controller.extend({
queryParams: ['project_id', 'to_user_id', 'share_user_ids'],
Copy link
Member

Choose a reason for hiding this comment

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

Is there a reason for using snake_case for some variables and parameters?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I was trying to match the underlying API but it does look weird. Will fix.

}),
shareUsers: Ember.computed('share_user_ids', function () {
const store = this.get('store');
const shareUserIds = this.get('share_user_ids');
Copy link
Member

Choose a reason for hiding this comment

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

I notice that the shareUserIds are passed around as a comma-separated string. Do arrays not work as queryParam variables?

Copy link
Contributor Author

@johnbradley johnbradley Aug 8, 2018

Choose a reason for hiding this comment

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

In my experience it automatically comma separates (%2C) array elements, but this seems rather unexpected and not deserialized on the other end.
It sounds like query parameters serialization is an ongoing issue in ember: emberjs/ember.js#14174

}),
isResendSelected: Ember.computed('selectedRouteName', function () {
return this.get('selectedRouteName') === 'deliveries.show.resend';
})
}),
isNewDeliverySelected: Ember.computed('selectedRouteName', function () {
Copy link
Member

Choose a reason for hiding this comment

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

You can use Ember.computed.equal here

@@ -6,9 +6,13 @@ export default Ember.Component.extend({
transfer: null, /* specific transfer to be passed to child breadcrumbs */
selectedRouteName: null, /* route we should select ('deliveries.index', 'deliveries.show', etc) */
isDeliverySelected: Ember.computed('selectedRouteName', function () {
Copy link
Member

Choose a reason for hiding this comment

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

I don't know that a regex is easier to read than indexOf == 0, but FYI there's Ember.computed.match

@johnbradley
Copy link
Contributor Author

I simplified this PR by removing the optional step of picking share users. I will create an issue to implement this feature in a future PR.

@johnbradley johnbradley requested a review from dleehr August 10, 2018 13:29
Copy link
Member

@dleehr dleehr left a comment

Choose a reason for hiding this comment

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

Nice improvements. Some suggestions for Ember practices that I've found to make things easier and didn't catch on the first review.

@@ -24,7 +26,7 @@ export default Ember.Controller.extend({
}
}),
currentUserCanDeliver: Ember.computed('currentUserProjectAuthRole', function () {
Copy link
Member

Choose a reason for hiding this comment

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

Can't this be Ember.computed.equal()?

@@ -6,6 +6,7 @@
showGlobalFilter=false
showPageSize=false
pageSize=12
filteringIgnoreCase=true
Copy link
Member

Choose a reason for hiding this comment

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

Wish I had known about this before!

import ApplicationAdapter from './application';

export default ApplicationAdapter.extend({
urlForQuery(query) {
Copy link
Member

Choose a reason for hiding this comment

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

I think I follow this, but I wanted to clarify.

In the discussion of Duke-GCB/D4S2#170, I argued that it didn't make sense for the API to have a required query parameter to fetch project permissions. I recognize that, without a proper hasMany relationship to get the related records, Ember Data offers store.query() to do fetch a filtered list for a resource, and this code overrides the default query implementation to work with the project detail route in Duke-GCB/D4S2#170. I think it's probably worth a comment about why this is here.

I'll follow up with an issue, but I wanted to have a discussion on whether or not it makes sense to just make ajax calls for the project permissions instead of shoehorning them into ember data models.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

My understanding is the hasMany relationship requires a top level endpoint to get the related keys (ie. /duke-ds-project-permission/<id>). I'm all for just making an ajax call, but not sure how easy it would be to re-use the emberjs auth.

Copy link
Member

Choose a reason for hiding this comment

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

Yeah I think we're on the same page - I was mostly trying to think out loud as to why this is done this way. Through the course of that I think I came to the conclusion that this doesn't fit 100% in a vanilla Ember Data store, but there are ways to make an authenticated AJAX call here and customize how the response is handled. We do it in bespin-ui.

Copy link
Member

Choose a reason for hiding this comment

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

See #22

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I added a comment to the adapter.

}
return this.get('store').query('duke-ds-project-permission', {
'project': projectId,
'user': currentUserId,
Copy link
Member

Choose a reason for hiding this comment

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

The user parameter is not used, and we don't need quotes around the key names.

Copy link
Contributor Author

@johnbradley johnbradley Aug 10, 2018

Choose a reason for hiding this comment

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

The user parameter is added to the end of the url and filters the permissions to just those for the current user. Otherwise the firstObject might be another user's permissions.

Copy link
Member

Choose a reason for hiding this comment

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

I see, so my understanding of the urlForQuery method wasn’t complete. This query parameter still gets passed through

Copy link
Contributor Author

@johnbradley johnbradley Aug 10, 2018

Choose a reason for hiding this comment

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

Yep. That's why I had to delete query.project; in urlForQuery or it ended up being used twice.

project: null,
application: Ember.inject.controller(),
currentDukeDsUser: Ember.computed.alias('application.currentDukeDsUser'),
currentUserProjectPermissions: Ember.computed('project', 'currentDukeDsUser.id', function () {
Copy link
Member

Choose a reason for hiding this comment

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

I see several instances where computed properties are returning promises (e.g. the result of a store.query()), which can make you want to tear your hair out. I know it's given me a lot of grief during development and testing, and I'm guessing it's why these functions have so many early bail outs.

I'd encourage you to have a look at that article to see if any of those patterns apply, and I'll mention that I've had good results with setting the property after the promise resolves:

  1. For controllers, you can add an observer on init (or some other property). This is how the application controller sets the current user:

authenticatedDidChange: Ember.on('init', Ember.observer('session.isAuthenticated', function () {
if (this.get('session.isAuthenticated')) {
this.get('dukeDsUser').currentDukeDsUser().then(currentDukeDsUser => {
this.set('currentDukeDsUser', currentDukeDsUser);
});
} else {
this.set('currentDukeDsUser', null);
}
})
)

  1. For components, kick off the promises in didInsertElement(), since they don't have the same object lifecycle as other objects (via https://emberigniter.com/render-promise-before-it-resolves/):

https://github.com/Duke-GCB/bespin-ui/blob/9d25758fb6d82c7e46f309bee248877b239685c8/app/components/questionnaire/dds-project-field.js#L12-L21

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That guide says that there is a danger with this approach (#1 in the guide):

waiting for promises and this.setting – on a potentially destroyed component

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm going to use the #3 Special promise-backed objects option.

export default DS.Model.extend({
project: DS.belongsTo('DukeDsProject'),
user: DS.belongsTo('DukeDsUser'),
auth_role: DS.attr('string')
Copy link
Member

Choose a reason for hiding this comment

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

Should be camelCase

currentDukeDsUser: Ember.computed.alias('application.currentDukeDsUser'),
validUsersList: Ember.computed('model.[]', function () {
// remove users with invalid fullName values
return this.get('model')
Copy link
Member

Choose a reason for hiding this comment

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

I see users in my listing with empty email addresses too. Won't this be problematic too?

Copy link
Member

Choose a reason for hiding this comment

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

empty-emails

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I added code to filter out users without emails.

actions: {
toUserSelectionChanged(actionData) {
var selectedItem = null;
if (actionData.selectedItems) {
Copy link
Member

Choose a reason for hiding this comment

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

One benefit of Ember is that arrays (and other enumerables have additional, observable properties like firstObject and lastObject that don't raise errors for empty arrays.

So, rather than having to check the length of the array first and then index into it, you could reduce the entire body to

this.set('toUser', selectedItems.get('firstObject'));

});
}),
currentUserProjectAuthRole: Ember.computed('currentUserProjectPermissions.[]', function () {
const authRoles = this.get('currentUserProjectPermissions').mapBy('auth_role');
Copy link
Member

Choose a reason for hiding this comment

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

Can also use .get('firstObject') here

}),
actions: {
projectSelectionChanged(actionData) {
var selectedItem = null;
Copy link
Member

Choose a reason for hiding this comment

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

See other comments about firstObject

@johnbradley johnbradley requested a review from dleehr August 13, 2018 17:09
@johnbradley johnbradley merged commit 430d3fa into master Aug 14, 2018
@johnbradley johnbradley deleted the 6-new-deliveries branch August 14, 2018 13:53
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.

2 participants