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 moving integration tasks between teams #5513
Allow moving integration tasks between teams #5513
Conversation
If a task with the same hash exists in the destination, it is deleted first. This currently only touches `changeTaskTeam`, but moving teams is also possible via `updateTask`.
This removes the ability of moving tasks between teams via `updateTask` and requires `changeTaskTeam` to be called. Having only one simpler mutation to do this is less error prone and reduces maintenance.
d4d6fb9
to
0847523
Compare
@nickoferrall could you take a look please? |
Hey @Dschoordsch, would you mind clarifying a few things in the testing steps please:
If you wouldn't mind adding a bit more detail to the testing steps, I'll review the PR asap! |
@nickoferrall I updated the testing steps to be more specific, let me know if that works for you. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@nickoferrall Awesome find! In my test Bob had the integration set up for both Team A and Team B, then it works. Apparently we're using the integration of the assignee and team combination. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Creating a new issue makes sense to me. Great work finding a fix for such a tricky edge case! LGTM
If the task has an integration, then the target team user must have the right integration set up to ensure the task can be displayed in the target team.
Before trying to move an integrated task to another team, the client side checks if the user has set up the correct integration for the target team. If not a dialog is presented to the user to add it on the spot.
@nickoferrall I made additional changes to catch the use case when a user tries to move a task to a team without having the integration set up for it yet. I also added separate testing flow for it. Could you please review again? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's all looking good to me! Feel free to review my comments and then proceed to Maintainer Review
...dules/outcomeCard/components/OutcomeCardAssignMenu/TaskFooterTeamAssigneeAddGitHubDialog.tsx
Outdated
Show resolved
Hide resolved
...modules/outcomeCard/components/OutcomeCardAssignMenu/TaskFooterTeamAssigneeAddJiraDialog.tsx
Outdated
Show resolved
Hide resolved
}) | ||
|
||
const StyledTip = styled('p')({ | ||
fontSize: 13, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I find the text here a little bit hard to read. Perhaps fontSize 14 with lineHeight 20?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, looks much better. It came from AddMissingJiraFieldModal
so we might want to increase size there as well
@enriquesanchez I added a dialog which appears when a user tries to move a task with integration to a team where they didn't set an integration up yet, is this dialog ok: |
@mattkrick could you please review? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hi @Dschoordsch
What happens if the user clicks cancel? Would the task still be moved and disconnected from the integration? Or would it abort the action altogether?
Design wise looks good! My only suggestion is to use the main Sky blue color for the Add it now
button instead of the red/orange gradient. We only use that color gradient in buttons in very specific actions (like creating a new meeting), so I think it's best to stick to the main blue for this specific case.
agreed the blue does feel better, although our prior art is to use the primary CTA for these modals. we'll probably need to revisit all the other modals like this & change the color there, too (removing a team member, removing an org user, asking for super permission from jira to fix their integration, promoting to team lead, etc....) |
Thanks @mattkrick 🙌 @Dschoordsch I think then it's best to keep it as is right now, and further down the line I can look at updating these colors altogether. |
The action is aborted. |
fontWeight: 600 | ||
}) | ||
|
||
const TaskFooterTeamAssigneeAddGitHubDialog = (props: Props) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i think the execution here is good, my only question is would anyone in real life click the cancel button? if not, maybe we don't even prompt them & we just do it behind the scenes for them.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I can imagine that people might reconsider adding the integration to the target team, but for me it's more important that they are informed that the new integration is added to the team. To me moving over the integration to the new team is not obvious, if I think about it, then it is logical, but when I'm moving a task I'm not thinking about what the tool might do in the background.
...modules/outcomeCard/components/OutcomeCardAssignMenu/TaskFooterTeamAssigneeAddJiraDialog.tsx
Outdated
Show resolved
Hide resolved
...s/client/modules/outcomeCard/components/OutcomeCardAssignMenu/TaskFooterTeamAssigneeMenu.tsx
Outdated
Show resolved
Hide resolved
...s/client/modules/outcomeCard/components/OutcomeCardAssignMenu/TaskFooterTeamAssigneeMenu.tsx
Outdated
Show resolved
Hide resolved
...s/client/modules/outcomeCard/components/OutcomeCardAssignMenu/TaskFooterTeamAssigneeMenu.tsx
Outdated
Show resolved
Hide resolved
...s/client/modules/outcomeCard/components/OutcomeCardAssignMenu/TaskFooterTeamAssigneeMenu.tsx
Show resolved
Hide resolved
...s/client/modules/outcomeCard/components/OutcomeCardAssignMenu/TaskFooterTeamAssigneeMenu.tsx
Show resolved
Hide resolved
@@ -55,6 +56,7 @@ export default { | |||
} | |||
|
|||
// RESOLUTION | |||
// filter mentions of old team members from task content |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it's possible that someone calls this mutation outside of our UI.
If that happens, we need this mutation to guarantee that an integration exists on the new teamId
I propose the following:
- remove the OAuth2 flow from the client, if they click cancel, we abort, just like you have written. if they click OK, then we call this mutation
- this mutation looks up the AtlassianAuth (or GitHubAuth) for the viewerId/teamId combination
- if it exists, the teamId param is valid
- if it doesn't exist, then it looks up the Auth for the viewerId/oldTeamId
- if that doesn't exist, return error: No valid integration auth
- If that does exist, then copy that row & insert it with the updated teamId
eliminating the extra oauth2 flow means the process goes faster & more importantly we can verify that the new teamId supports that task integration
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I got a bit lost in edge cases. When the viewer has no integration in the target team, there are these options:
- if user has integration in source team, then we will use that, but still ask
- if accessUser is someone else and they have integration for target team, then we will use that without asking
- else we need to ask the user to integrate with complete oauth flow
For now I ignore 2) and 3) and just assume it's 1) as that's the most common case in the UI.
Better would be of course to detect 2) and 3) as well and handle it appropriately in the UI, but we do not get all information necessary at the moment via GraphQL and I also don't want to repeat the same logic if there is a nicer solution.
Let's just roll with 1) and fix 2) and 3) another day, user value is probably also minimal. Use case 2) I can imagine is a bit annoying, i.e. team lead has 2 parabol teams and adds integration. They're also the only ones with access to Jira. Every time some other team member would move a task, they would get shown the dialog. It would still move the task, but it might get annoying. But if that hypothetical user exists, they can open a ticket.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fantastic work exploring all the edge cases.
i think your assumption are reasonable & i feel very comfortable with the solution you've built!
If a task is integrated and the user does not have an integration set up for the target team, then copy their source teams integration over. If they have neither source nor target team integration and the task was pushed by someone else, then check if their integration is present in target team and use that.
Assume for now in the UI that a user has set up the integration for the source team and that we can just transfer it over to the target team.
docs/integrations.md
Outdated
- fallback to any other member | ||
- [x] **pushing a task** requires **viewer auth or assignee's auth**, but in both cases a comment will be added if viewer !== assignee | ||
- [x] adding tasks in **scoping** requires **viewer's auth** | ||
- [ ] adding task **estimates** uses **viewer's auth or assignee's auth** (Most likely just added in scoping by the assignee, but even when added to the board before it's little risk as estimating is a team activity) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we should probably try to use task.integration?.accessUserId
too
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
true
@@ -72,9 +133,25 @@ export default { | |||
const updates = { | |||
content: rawContent === nextRawContent ? undefined : JSON.stringify(nextRawContent), | |||
updatedAt: now, | |||
teamId | |||
teamId, | |||
integration: task.integration && { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the &&
will turn into a false
.
currently in the DB, the integration
field doesn't exist on a Task if it is not integrated.
to keep this same structure, we would want this to be undefined
if there is no integration or if the integration is unchanged (rethinkdb driver filters out keys with undefined values). Gotta love NoSQL 😞
For your consideration, I wrapped all the integration stuff up in a single conditional. This might be useful as we move forward with jira server & gitlab integrations. Then you can just write integration: updatedIntegration
:
const {integration} = task
let updatedIntegration: typeof integration
if (integration) {
const {accessUserId, service} = integration
const authKeys = [
{teamId: task.teamId, userId: viewerId},
{teamId, userId: viewerId},
{teamId, userId: accessUserId}
]
const errToNullMap = <T>(val: T | Error | null) => (val instanceof Error ? null : val)
const [sourceTeamAuth, targetTeamAuth, accessUsersTargetTeamAuth] =
service === 'jira'
? (await dataLoader.get('freshAtlassianAuth').loadMany(authKeys)).map(errToNullMap)
: service === 'github'
? (await dataLoader.get('githubAuth').loadMany(authKeys)).map(errToNullMap)
: []
if (!targetTeamAuth && !sourceTeamAuth && !accessUsersTargetTeamAuth) {
return standardError(new Error('No valid integration found'), {
userId: viewerId
})
}
const data = {teamId, userId: viewerId}
// use viewer's auth or fallback to the existing accessUserId
const nextAccessUserId = sourceTeamAuth || targetTeamAuth ? viewerId : accessUserId
if (nextAccessUserId !== accessUserId) {
updatedIntegration = {...integration, accessUserId: nextAccessUserId}
}
if (sourceTeamAuth && !targetTeamAuth) {
if (service === 'jira') {
await upsertAtlassianAuth({
...sourceTeamAuth,
teamId
})
publish(SubscriptionChannel.TEAM, teamId, 'AddAtlassianAuthPayload', data, subOptions)
} else if (service === 'github') {
await upsertGitHubAuth({
...sourceTeamAuth,
teamId
})
publish(SubscriptionChannel.TEAM, teamId, 'AddGitHubAuthPayload', data, subOptions)
}
}
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
While I like the updatedIntegration
cleanup, the current &&
would still work, since it would return undefined
when task.integration === undefined
. &&
doesn't convert the result to boolean.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ah you're right! that's what i get for thinking at night
const [sourceTeamAuth, targetTeamAuth, accessUsersTargetTeamAuth] = | ||
task.integration?.service === 'jira' | ||
? await Promise.all([ | ||
dataLoader.get('freshAtlassianAuth').load({teamId: task.teamId, userId: viewerId}), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
these might be good candidates for loadMany
, although those have the possibility of returning an Error instance.
I wrote an example of what that might look like, let me know what you think
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't really like the loadMany
signature and it's just a wrapper for Promise.all(load(...))
. Question is whether we want to silently ignore errors and convert them to null or let them bubble up. I tend to default to just let these bubble up since we don't expect any errors here. But I can reduce repetition.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
agreed, the signature used to be so much nicer! graphql/dataloader#216
we're already silently ignoring any errors inside the batchFn, i wish we could tell the dataloader that we don't need any errors that come out of it.
mapping & wrapping in Promise.all is probably the cleanest way as long as we can guarantee that none of them will throw.
@@ -55,6 +56,7 @@ export default { | |||
} | |||
|
|||
// RESOLUTION | |||
// filter mentions of old team members from task content |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fantastic work exploring all the edge cases.
i think your assumption are reasonable & i feel very comfortable with the solution you've built!
When a task changes team and the integration is copied to the new team, then we already checked the integration for the target team, thus the dataLoader has `null` cached for it. We need to clear this value before publishing/returning any results which might read that value again.
If a task with the same hash exists in the destination, it is deleted first.
A conflicting integration task in the target is deleted first and the delete notifications are sent out, then the original task is moved.
In order to avoid having duplicated functionality the
teamId
was removed fromupdateTask
mutation, sochangeTaskTeam
is the only way left to move a task.Testing
Moving task between teams replacing existing task
Setup
Verification
Moving task to new team without integration
Setup
Verification
Fixes: #5200