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

Add "Transfer ownership" function to docs #645

Merged
merged 25 commits into from
Mar 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading