Skip to content

Commit

Permalink
Add "Transfer ownership" function to docs (#645)
Browse files Browse the repository at this point in the history
* Set up Transfer modal

* Set up TypeToConfirm

* Improve design

* Improve error handling

* Tweak TypeToConfirm value

* Write and stub tests

* Update base.d.ts

* Update sidebar.ts

* Write type-to-confirm tests

* Add PeopleSelect tests

* Enable patching a document's owner

* Tweak grammar

* Fix transferOwnership payload

* Update sidebar.ts

* Start of modal success

* Add `document` assertions

* Fix failing/stubbed tests

* Switch `owners` syntax

* Cleanup and documentation

* Update sidebar.ts

* Update document-test.ts

* Allow drafts to be transferred

---------

Co-authored-by: Josh Freda <[email protected]>
  • Loading branch information
jeffdaley and jfreda authored Mar 12, 2024
1 parent 040ee15 commit 37de695
Show file tree
Hide file tree
Showing 17 changed files with 924 additions and 97 deletions.
16 changes: 9 additions & 7 deletions web/app/components/document/modal.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,15 @@
disabled={{or @taskButtonIsDisabled this.taskIsRunning}}
{{on "click" (perform this.task)}}
/>
<Hds::Button
data-test-document-modal-secondary-button
@text="Cancel"
@color="secondary"
disabled={{this.taskIsRunning}}
{{on "click" F.close}}
/>
{{#unless @secondaryButtonIsHidden}}
<Hds::Button
data-test-document-modal-secondary-button
@text="Cancel"
@color="secondary"
disabled={{this.taskIsRunning}}
{{on "click" F.close}}
/>
{{/unless}}
</Hds::ButtonSet>
</M.Footer>
{{/if}}
Expand Down
12 changes: 11 additions & 1 deletion web/app/components/document/modal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,9 @@ interface DocumentModalComponentSignature {
taskButtonIsDisabled?: boolean;
hideFooterWhileSaving?: boolean;
color?: HdsModalColor;
secondaryButtonIsHidden?: boolean;
close: () => void;
task?: () => Promise<void> | void;
task?: (newOwner?: string) => Promise<void> | void;
};
Blocks: {
default: [{ taskIsRunning: boolean }];
Expand Down Expand Up @@ -69,6 +70,15 @@ export default class DocumentModalComponent extends Component<DocumentModalCompo

try {
this.taskIsRunning = true;

/**
* Clear errors before entering a full-modal state,
* such as when showing the "Transferring ownership" message.
*/
if (!this.footerIsShown) {
this.resetErrors();
}

await this.args.task();
this.args.close();
} catch (error: unknown) {
Expand Down
121 changes: 121 additions & 0 deletions web/app/components/document/sidebar.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -406,7 +406,25 @@
</div>
{{/if}}
{{/each-in}}

{{! Transfer ownership }}
{{#if this.isOwner}}
<div class="pt-5">
<div class="border-t border-color-border-faint pt-7">
<Action
data-test-transfer-document-ownership-button
{{on "click" this.showTransferOwnershipModal}}
disabled={{this.editingIsDisabled}}
class="flex gap-2 text-body-100 text-color-foreground-disabled hover:text-color-foreground-faint focus:text-color-foreground-faint"
>
<FlightIcon @name="swap-horizontal" />
Transfer ownership...
</Action>
</div>
</div>
{{/if}}
</div>

</div>

{{#if this.footerIsShown}}
Expand Down Expand Up @@ -605,6 +623,109 @@
/>
{{/if}}

{{! Transfer ownership }}
{{#if this.transferOwnershipModalIsShown}}
<TypeToConfirm
@value="transfer"
@onEnter={{this.clickTransferButton}}
as |T|
>
<Document::Modal
data-test-transfer-ownership-modal
@headerText="Transfer ownership"
@errorTitle="Couldn't transfer ownership"
@close={{this.hideTransferOwnershipModal}}
@secondaryButtonIsHidden={{true}}
@task={{perform this.transferOwnership}}
@taskButtonText="Transfer doc"
@hideFooterWhileSaving={{true}}
@taskButtonIsDisabled={{or
(not this.newOwners.length)
(not T.hasConfirmed)
}}
>
<:default as |M|>
{{#if M.taskIsRunning}}
<div
data-test-transferring-doc
class="grid place-items-center pt-1 pb-8 text-center"
>
<FlightIcon @name="loading" @size="24" class="mb-5" />
<h2>Transferring doc...</h2>
</div>
{{else}}

<p class="mb-5 text-body-300">
Give this document to someone in your workspace.
<br />
We'll notify them when the transfer completes.
</p>

<div class="grid gap-4 pb-2.5">
<div>
<label
data-test-select-new-owner-label
{{on "click" this.focusPeopleSelect}}
for={{this.transferOwnershipPeopleSelectID}}
class="hermes-form-label"
>
Select new owner
</label>
<Inputs::PeopleSelect
{{autofocus waitUntilNextRunloop=true}}
@triggerId={{this.transferOwnershipPeopleSelectID}}
@selected={{this.newOwners}}
@onChange={{this.setNewOwner}}
@isSingleSelect={{true}}
@renderInPlace={{true}}
@excludeSelf={{true}}
/>
</div>
<div>
<T.Input {{did-insert this.registerTypeToConfirmInput}} />
</div>
</div>
{{/if}}
</:default>
</Document::Modal>
</TypeToConfirm>
{{/if}}

{{! Ownership transferred }}
{{#if this.ownershipTransferredModalIsShown}}
<Hds::Modal
data-test-ownership-transferred-modal
@onClose={{this.hideOwnershipTransferredModal}}
as |M|
>
<M.Header>
<div class="flex items-center">
<FlightIcon
@name="check-circle-fill"
class="mr-2 text-color-palette-green-200"
/>
Done
</div>
</M.Header>
<M.Body>
<div class="grid place-items-center pt-8 pb-8 text-center">
<h2>Ownership transferred.</h2>
<p class="mb-2.5 flex text-body-300">
{{get-model-attr "person.name" (get @document.owners 0)}}
has been notified of the change.
</p>
</div>
</M.Body>
<M.Footer>
<Hds::Button
data-test-document-modal-primary-button
@text="Close"
{{on "click" this.hideOwnershipTransferredModal}}
/>
</M.Footer>
</Hds::Modal>
{{/if}}

{{#if this.requestReviewModalIsShown}}
<Document::Modal
data-test-publish-for-review-modal
Expand Down
140 changes: 139 additions & 1 deletion web/app/components/document/sidebar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,9 +72,16 @@ export default class DocumentSidebarComponent extends Component<DocumentSidebarC
@service declare session: SessionService;
@service declare flashMessages: HermesFlashMessagesService;

/**
* The ID shared between the "Select a new owner" PeopleSelect and its label.
*/
protected transferOwnershipPeopleSelectID =
"transfer-ownership-people-select";

@tracked deleteModalIsShown = false;
@tracked requestReviewModalIsShown = false;
@tracked docPublishedModalIsShown = false;
@tracked protected transferOwnershipModalIsShown = false;
@tracked protected projectsModalIsShown = false;
@tracked docTypeCheckboxValue = false;
@tracked emailFields = ["approvers", "contributors"];
Expand Down Expand Up @@ -167,6 +174,24 @@ export default class DocumentSidebarComponent extends Component<DocumentSidebarC
*/
@tracked protected projectsErrorIsShown = false;

/**
* The new owner of the document.
* Set when the user selects a new owner from the "Transfer ownership" modal.
*/
@tracked private newOwners: string[] = [];

/**
* The `TypeToConfirm` input of the "Transfer ownership" modal.
* Registered on insert and focused when the user selects a new owner.
*/
@tracked protected typeToConfirmInput: HTMLInputElement | null = null;

/**
* Whether the "Ownership transferred" modal is shown.
* True when the `transferOwnership` task completes successfully.
*/
@tracked protected ownershipTransferredModalIsShown = false;

@tracked userHasScrolled = false;
@tracked _body: HTMLElement | null = null;

Expand Down Expand Up @@ -491,6 +516,24 @@ export default class DocumentSidebarComponent extends Component<DocumentSidebarC
this.projectsModalIsShown = false;
}

/**
* The action to show the "Transfer ownership" modal.
* Triggered by clicking the "Transfer ownership" button in the footer.
*/
@action protected showTransferOwnershipModal() {
this.transferOwnershipModalIsShown = true;
}

/**
* The action to hide the "Transfer ownership" modal.
* Passed to the "Transfer ownership" modal component and
* triggered on modal close.
*/
@action protected hideTransferOwnershipModal() {
this.transferOwnershipModalIsShown = false;
this.newOwners = [];
}

@action refreshRoute() {
// We force refresh due to a bug with `refreshModel: true`
// See: https://github.com/emberjs/ember.js/issues/19260
Expand Down Expand Up @@ -527,6 +570,65 @@ export default class DocumentSidebarComponent extends Component<DocumentSidebarC
});
}

/**
* The action to set the new intended owner of the doc.
* Called as the `onChange` action in the "Transfer ownership" modal's
* PeopleSelect component. Sets the newOwner property and focuses the
* TypeToConfirm input.
*/
@action protected setNewOwner(newOwners: string[]) {
this.newOwners = newOwners;
this.focusTypeToConfirmInput();
}

/**
* The action to register the "TypeToConfirm" input of the "Transfer ownership" modal.
* Runs on insert and captures the typeToConfirmInput for focus targeting.
*/
@action protected registerTypeToConfirmInput(input: HTMLInputElement) {
this.typeToConfirmInput = input;
}

/**
* The action to focus the "TypeToConfirm" input of the "Transfer ownership" modal.
* Called for conveniences when the user selects a new owner from the PeopleSelect.
*/
@action private focusTypeToConfirmInput() {
assert("typeToConfirmInput must exist", this.typeToConfirmInput);
this.typeToConfirmInput.focus();
}

/**
* The action to focus the PeopleSelect input. Runs when the `label` is clicked.
* This is a workaround until `ember-power-select` `8.0.0` is released, enabling
* the `labelText` argument in the PowerSelectMultiple component.
*/
@action protected focusPeopleSelect() {
const peopleSelect = htmlElement(
"dialog .multiselect input",
) as HTMLInputElement;
peopleSelect.focus();
}

/**
* The to click the "Transfer doc" button. Runs on Enter when the TypeToConfirm
* input is valid and focused. Runs the `transferOwnership` task along with
* the modal's internal tasks for consistency with the real click action.
*/
@action protected clickTransferButton() {
const button = htmlElement("dialog .hds-button") as HTMLButtonElement;
button.click();
}

/**
* The action to show the "Ownership transferred" modal.
* Passed as the `onClose` action of the "Transfer ownership" modal
* and triggered when clicking the "Close" button in the modal.
*/
@action protected hideOwnershipTransferredModal() {
this.ownershipTransferredModalIsShown = false;
}

/**
* A task that waits for a short time and then resolves.
* Used to trigger the "link created" state of the share button.
Expand Down Expand Up @@ -698,7 +800,7 @@ export default class DocumentSidebarComponent extends Component<DocumentSidebarC
},
);

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

try {
Expand All @@ -711,6 +813,14 @@ export default class DocumentSidebarComponent extends Component<DocumentSidebarC
},
);
} catch (error) {
/**
* Errors are normally handled in a flash message, but if the
* consuming method needs special treatment, such as to trigger
* a modal error, we throw the error up the chain.
*/
if (throwOnError) {
throw error;
}
const e = error as Error;
this.maybeLockDoc(e);
this.showFlashError(e, "Unable to save document");
Expand Down Expand Up @@ -799,6 +909,34 @@ export default class DocumentSidebarComponent extends Component<DocumentSidebarC
}
});

/**
* The task to transfer ownership of a document.
* Called when the user selects a new owner from the "Transfer ownership" modal.
* Updates the document's `owners` array and saves it to the back end.
*/
protected transferOwnership = dropTask(async () => {
assert("owner must exist", this.newOwners.length > 0);

try {
await this.patchDocument.perform(
{
owners: this.newOwners,
},
true,
);

this.transferOwnershipModalIsShown = false;
this.ownershipTransferredModalIsShown = true;
this.newOwners = [];
} catch (error) {
const e = error as Error;
this.maybeLockDoc(e);

// trigger the modal error
throw e;
}
});

@action updateApprovers(approvers: string[]) {
this.approvers = approvers;
}
Expand Down
Loading

0 comments on commit 37de695

Please sign in to comment.