Skip to content

Commit

Permalink
Add support for Google Groups in the approver field (#661)
Browse files Browse the repository at this point in the history
* Prep for groups in the PeopleSelect

* Explore XDropdownList

* More basicDropdown discovery

* Improve types and styles

* Update nav.hbs

* Add offset, improve styles

* Improve dropdown design

* Style tweak

* WIP "Loading"

* Improve loading states

* Cleanup

* Update people-select-test.ts

* Tweak styles and offset

* Prevent dropdown from opening on ArrowUp/Down

* Set up `includeGroups` arg

* Revert unnecessary change

* Flatten array instead of groups

* Reduce diff

* Start of EmberData files

* Clean Mirage People route

* More prep for EmberData requests

* Add notes from meeting with Josh

* Rename/reorganize

* Post-merge resolutions

* Add conditional approve button and text

* Improve group handling

* Add group logic to maybeFetchPeople

* Track cachedValue in `editableField`

* Fix test; WIP update-editable-field function

* Put TODO catch block on OPTIONS call

* Get list to update on groupApproverClick

* Remove approvers from drafts POST request - this isn't used in the frontend

* Add groups API

* Support group approvals

* Forgot document group review model

* WIP OPTIONS call

* Add members of approver groups as individual approvers in the database

* Load group with `maybeFetchPeople`

* Filter departed users

* Fix errors

* Add/update tests; cleanup and documentation

* Enable setting a Google Groups prefix

* Only apply prefix if it looks like the user isn't beginning to type it

* Add "remove me" assertion

* Return results with and without a configured groups prefix

---------

Co-authored-by: Josh Freda <[email protected]>
  • Loading branch information
jeffdaley and jfreda authored Apr 5, 2024
1 parent 7459890 commit 4660ccf
Show file tree
Hide file tree
Showing 33 changed files with 632 additions and 123 deletions.
24 changes: 24 additions & 0 deletions web/app/adapters/group.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import DS from "ember-data";
import ApplicationAdapter from "./application";
import RSVP from "rsvp";

export default class GroupAdapter extends ApplicationAdapter {
/**
* The Query method for the group model.
* Returns an array of groups that match the query.
* Also used by the `queryRecord` method.
*/
query(_store: DS.Store, _type: DS.Model, query: { query: string }) {
const results = this.fetchSvc
.fetch(`/api/${this.configSvc.config.api_version}/groups`, {
method: "POST",
body: JSON.stringify({
// Spaces throw an error, so we replace them with dashes
query: query.query.replace(" ", "-"),
}),
})
.then((r) => r?.json());

return RSVP.hash({ results });
}
}
1 change: 1 addition & 0 deletions web/app/components/document/index.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
@docType={{@docType}}
@isCollapsed={{this.sidebarIsCollapsed}}
@toggleCollapsed={{this.toggleSidebarCollapsedState}}
@viewerIsGroupApprover={{@viewerIsGroupApprover}}
/>
{{/unless}}
</div>
Expand Down
1 change: 1 addition & 0 deletions web/app/components/document/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ interface DocumentIndexComponentSignature {
document: HermesDocument;
modelIsChanging: boolean;
docType: Promise<HermesDocumentType>;
viewerIsGroupApprover: boolean;
};
}

Expand Down
89 changes: 47 additions & 42 deletions web/app/components/document/sidebar.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -284,11 +284,12 @@
<EditableField
data-test-document-approvers
data-test-editable={{this.isOwner}}
@value={{this.approvers}}
@value={{this.allApprovers}}
@onChange={{this.updateApprovers}}
@onSave={{perform this.save "approvers"}}
@onSave={{perform this.saveApprovers}}
@isSaving={{this.saveIsRunning}}
@isReadOnly={{this.editingIsDisabled}}
@includeGroupsInPeopleSelect={{true}}
{{! Provide the document to the `has-approved-doc` helper }}
@document={{@document}}
/>
Expand Down Expand Up @@ -333,7 +334,7 @@
@model={{project.id}}
class="related-resource-link quarternary-button peer flex h-[32px] items-center gap-2 px-[5px]"
>
<div class="flex w-[18px] shrink-0 justify-center py-[2px]">
<div class="flex w-[18px] shrink-0 justify-center">
<Project::StatusIcon @status={{project.status}} />
</div>
<div class="overflow-hidden">
Expand Down Expand Up @@ -475,9 +476,8 @@
</div>
{{else}}
{{#let (and this.isDraft this.isOwner) as |canPublish|}}
{{#if (or canPublish this.isApprover)}}
{{#if (or canPublish this.isApprover this.isGroupApproverOnly)}}
<div class="flex gap-2 px-3">

{{#if canPublish}}
{{! Publish for review... }}
<Hds::Button
Expand All @@ -498,7 +498,7 @@
{{on "click" (fn (set this "deleteModalIsShown" true))}}
/>
{{else}}
{{! isApprover }}
{{! isApprover or isGroupApproverOnly}}

{{! Read-only / isRunning state }}
{{#if
Expand Down Expand Up @@ -540,6 +540,7 @@
class="w-full"
{{on "click" (perform this.approve)}}
/>

{{#if (eq @document.docType "FRD")}}
{{! Reject FRD }}
<Hds::Button
Expand All @@ -552,42 +553,45 @@
{{on "click" (perform this.rejectFRD)}}
/>
{{/if}}
{{! Overflow menu (Leave approver role) }}
<X::DropdownList
data-test-sidebar-footer-overflow-menu
@placement="top-start"
@items={{array
(hash
icon="user-minus"
label="Leave approver role"
action=(perform this.leaveApproverRole)
)
}}
>
<:anchor as |dd|>
<dd.ToggleAction>
<Hds::Button
data-test-sidebar-footer-secondary-dropdown-button
@text="More actions"
@icon="more-horizontal"
@color="secondary"
@isIconOnly={{true}}
/>
</dd.ToggleAction>
</:anchor>
<:item as |dd|>
<dd.Action
class="flex gap-2.5"
{{on "click" dd.attrs.action}}
>
<FlightIcon
@name={{dd.attrs.icon}}
class="opacity-60"
/>
{{dd.attrs.label}}
</dd.Action>
</:item>
</X::DropdownList>

{{#unless this.isGroupApproverOnly}}
{{! Overflow menu (Leave approver role) }}
<X::DropdownList
data-test-sidebar-footer-overflow-menu
@placement="top-start"
@items={{array
(hash
icon="user-minus"
label="Leave approver role"
action=(perform this.leaveApproverRole)
)
}}
>
<:anchor as |dd|>
<dd.ToggleAction>
<Hds::Button
data-test-sidebar-footer-secondary-dropdown-button
@text="More actions"
@icon="more-horizontal"
@color="secondary"
@isIconOnly={{true}}
/>
</dd.ToggleAction>
</:anchor>
<:item as |dd|>
<dd.Action
class="flex gap-2.5"
{{on "click" dd.attrs.action}}
>
<FlightIcon
@name={{dd.attrs.icon}}
class="opacity-60"
/>
{{dd.attrs.label}}
</dd.Action>
</:item>
</X::DropdownList>
{{/unless}}
{{/if}}
{{/if}}
</div>
Expand Down Expand Up @@ -763,6 +767,7 @@
<Hds::Form::Field @layout="vertical" as |F|>
<F.Control>
<Inputs::PeopleSelect
@includeGroups={{true}}
@renderInPlace={{true}}
@selected={{this.approvers}}
@onChange={{this.updateApprovers}}
Expand Down
91 changes: 71 additions & 20 deletions web/app/components/document/sidebar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
task,
timeout,
} from "ember-concurrency";
import { capitalize, dasherize } from "@ember/string";
import { capitalize } from "@ember/string";
import cleanString from "hermes/utils/clean-string";
import { debounce, schedule } from "@ember/runloop";
import FetchService from "hermes/services/fetch";
Expand All @@ -35,6 +35,7 @@ import { ProjectStatus } from "hermes/types/project-status";
import { RelatedHermesDocument } from "../related-resources";
import PersonModel from "hermes/models/person";
import RecentlyViewedService from "hermes/services/recently-viewed";
import StoreService from "hermes/services/store";
import ModalAlertsService, { ModalType } from "hermes/services/modal-alerts";

interface DocumentSidebarComponentSignature {
Expand All @@ -43,6 +44,7 @@ interface DocumentSidebarComponentSignature {
document: HermesDocument;
docType: Promise<HermesDocumentType>;
isCollapsed: boolean;
viewerIsGroupApprover: boolean;
toggleCollapsed: () => void;
};
}
Expand Down Expand Up @@ -71,6 +73,7 @@ export default class DocumentSidebarComponent extends Component<DocumentSidebarC
@service declare recentlyViewed: RecentlyViewedService;
@service declare router: RouterService;
@service declare session: SessionService;
@service declare store: StoreService;
@service declare flashMessages: HermesFlashMessagesService;
@service declare modalAlerts: ModalAlertsService;

Expand Down Expand Up @@ -115,8 +118,28 @@ export default class DocumentSidebarComponent extends Component<DocumentSidebarC

@tracked contributors: string[] = this.args.document.contributors || [];

/**
* A locally tracked array of approvers. Used to update the UI immediately
* instead of waiting for the back end to confirm the change.
*/
@tracked approvers: string[] = this.args.document.approvers || [];

/**
* A locally tracked array of approverGroups. Used to update the UI immediately
* instead of waiting for the back end to confirm the change.
*/

@tracked approverGroups: string[] = this.args.document.approverGroups || [];

/**
* A computed property that returns all approvers and approverGroups.
* Passed to the EditableField component to render the list of approvers and groups.
* Recomputes when the approvers or approverGroups arrays change.
*/
protected get allApprovers() {
return this.approverGroups.concat(this.approvers);
}

@tracked product = this.args.document.product || "";

@tracked status = this.args.document.status;
Expand Down Expand Up @@ -165,7 +188,7 @@ export default class DocumentSidebarComponent extends Component<DocumentSidebarC
/**
* Whether the Approvers list is shown.
* True except immediately after the user leaves the approver role.
* See note in `leaveApproverRole` for more information.
* See note in `toggleApproverVisibility` for more information.
*/
@tracked protected approversAreShown = true;

Expand Down Expand Up @@ -398,6 +421,14 @@ export default class DocumentSidebarComponent extends Component<DocumentSidebarC
);
}

/**
* Whether the viewer is a group approver, but not an individual approver.
* If true, hides the "Remove me" overflow menu next to the "Approve" button.
*/
protected get isGroupApproverOnly() {
return this.args.viewerIsGroupApprover && !this.isApprover;
}

get isContributor() {
return this.args.document.contributors?.some(
(e) => e === this.args.profile.email,
Expand All @@ -421,22 +452,6 @@ export default class DocumentSidebarComponent extends Component<DocumentSidebarC
@tracked protected hasRejectedFRD =
this.args.document.changesRequestedBy?.includes(this.args.profile.email);

/**
* Whether the doc status is approved. Used to determine editing privileges.
* If the doc is approved, editing is exclusive to the doc owner.
*/
private get docIsApproved() {
return this.args.document.status.toLowerCase() === "approved";
}

/**
* Whether the doc status is in review. Used to determine editing privileges.
* If the doc is in review, editing is exclusive to the doc owner.
*/
private get docIsInReview() {
return dasherize(this.args.document.status) === "in-review";
}

/**
* Whether the document viewer is its owner.
* True if the logged in user's email matches the documents owner.
Expand Down Expand Up @@ -490,7 +505,10 @@ export default class DocumentSidebarComponent extends Component<DocumentSidebarC
* immediately after the user leaves the approver role.
*/
protected get footerIsShown() {
return !this.hasJustLeftApproverRole && (this.isApprover || this.isOwner);
return (
!this.hasJustLeftApproverRole &&
(this.isApprover || this.isOwner || this.isGroupApproverOnly)
);
}

/**
Expand Down Expand Up @@ -745,6 +763,7 @@ export default class DocumentSidebarComponent extends Component<DocumentSidebarC
return (
this.save.isRunning ||
this.saveCustomField.isRunning ||
this.patchDocument.isRunning ||
this.saveProduct.isRunning
);
}
Expand Down Expand Up @@ -802,6 +821,17 @@ export default class DocumentSidebarComponent extends Component<DocumentSidebarC
},
);

/**
* The action to save approvers. Called by the EditableField component `onSave`.
* Sends a patch request to update the `approvers` and `approverGroups` fields.
*/
protected saveApprovers = dropTask(async () => {
await this.patchDocument.perform({
approvers: this.approvers,
approverGroups: this.approverGroups,
});
});

patchDocument = enqueueTask(async (fields: any, throwOnError?: boolean) => {
const endpoint = this.isDraft ? "drafts" : "documents";

Expand Down Expand Up @@ -948,8 +978,20 @@ export default class DocumentSidebarComponent extends Component<DocumentSidebarC
}
});

/**
* The action passed to the approvers EditableField as `onChange`.
* Updates the local approver arrays when people are added or removed.
*/
@action updateApprovers(approvers: string[]) {
this.approvers = approvers;
this.approverGroups = approvers.filter((approver) => {
return this.store.peekRecord("group", approver);
});

this.approvers = approvers.filter((approver) => {
if (!this.approverGroups.includes(approver)) {
return this.store.peekRecord("person", approver);
}
});
}

@action updateContributors(contributors: string[]) {
Expand Down Expand Up @@ -1099,6 +1141,15 @@ export default class DocumentSidebarComponent extends Component<DocumentSidebarC

this.hasApproved = true;

/**
* This will be the case with a group approver.
*/
if (!this.approvers.includes(this.args.profile.email)) {
this.approvers.push(this.args.profile.email);
this.approvers = this.approvers;
this.toggleApproverVisibility();
}

if (options instanceof MouseEvent || !options?.skipSuccessMessage) {
this.showFlashSuccess("Done!", "Document approved");
}
Expand Down
1 change: 1 addition & 0 deletions web/app/components/editable-field.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
{{! @glint-ignore - TODO: type this as an array }}
@selected={{this.value}}
@onChange={{this.onChange}}
@includeGroups={{@includeGroupsInPeopleSelect}}
@onKeydown={{fn this.onPeopleSelectKeydown this.maybeUpdateValue}}
{{dismissible
dismiss=(fn this.maybeUpdateValue this.value)
Expand Down
1 change: 1 addition & 0 deletions web/app/components/editable-field.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ interface EditableFieldComponentSignature {
buttonSize?: "medium"; // Default is `small`
tag?: "h1"; // Default is `p`
document?: HermesDocument; // Used to check an approver's approval status
includeGroupsInPeopleSelect?: boolean;
};
Blocks: {};
}
Expand Down
Loading

0 comments on commit 4660ccf

Please sign in to comment.