From 6d1914f43db8af28f97d75ad40a0af6e15c2b530 Mon Sep 17 00:00:00 2001 From: Vicki League Date: Tue, 25 Feb 2025 09:56:01 -0500 Subject: [PATCH] [CL-485] Add small delay for async action loading state (#12835) --- .../src/async-actions/bit-action.directive.ts | 29 +++-- .../async-actions/form-button.directive.ts | 6 +- .../src/async-actions/standalone.mdx | 29 ++++- .../src/async-actions/standalone.stories.ts | 35 +++++- .../src/button/button.component.html | 4 +- .../components/src/button/button.component.ts | 72 ++++++++--- .../icon-button/icon-button.component.html | 4 +- .../src/icon-button/icon-button.component.ts | 116 +++++++++++++----- .../src/shared/button-like.abstraction.ts | 6 +- .../components/send-form.component.ts | 4 +- .../cipher-attachments.component.spec.ts | 24 ++-- .../cipher-attachments.component.ts | 8 +- .../components/cipher-form.component.ts | 4 +- .../item-details-section.component.spec.ts | 3 + .../add-edit-folder-dialog.component.ts | 2 +- .../assign-collections.component.ts | 4 +- 16 files changed, 253 insertions(+), 97 deletions(-) diff --git a/libs/components/src/async-actions/bit-action.directive.ts b/libs/components/src/async-actions/bit-action.directive.ts index 3e793ae2ec..ac50082852 100644 --- a/libs/components/src/async-actions/bit-action.directive.ts +++ b/libs/components/src/async-actions/bit-action.directive.ts @@ -21,30 +21,35 @@ export class BitActionDirective implements OnDestroy { private destroy$ = new Subject(); private _loading$ = new BehaviorSubject(false); - disabled = false; - - @Input("bitAction") handler: FunctionReturningAwaitable; - + /** + * Observable of loading behavior subject + * + * Used in `form-button.directive.ts` + */ readonly loading$ = this._loading$.asObservable(); - constructor( - private buttonComponent: ButtonLikeAbstraction, - @Optional() private validationService?: ValidationService, - @Optional() private logService?: LogService, - ) {} - get loading() { return this._loading$.value; } set loading(value: boolean) { this._loading$.next(value); - this.buttonComponent.loading = value; + this.buttonComponent.loading.set(value); } + disabled = false; + + @Input("bitAction") handler: FunctionReturningAwaitable; + + constructor( + private buttonComponent: ButtonLikeAbstraction, + @Optional() private validationService?: ValidationService, + @Optional() private logService?: LogService, + ) {} + @HostListener("click") protected async onClick() { - if (!this.handler || this.loading || this.disabled || this.buttonComponent.disabled) { + if (!this.handler || this.loading || this.disabled || this.buttonComponent.disabled()) { return; } diff --git a/libs/components/src/async-actions/form-button.directive.ts b/libs/components/src/async-actions/form-button.directive.ts index 7c92865b98..1c2855f32e 100644 --- a/libs/components/src/async-actions/form-button.directive.ts +++ b/libs/components/src/async-actions/form-button.directive.ts @@ -41,15 +41,15 @@ export class BitFormButtonDirective implements OnDestroy { if (submitDirective && buttonComponent) { submitDirective.loading$.pipe(takeUntil(this.destroy$)).subscribe((loading) => { if (this.type === "submit") { - buttonComponent.loading = loading; + buttonComponent.loading.set(loading); } else { - buttonComponent.disabled = this.disabled || loading; + buttonComponent.disabled.set(this.disabled || loading); } }); submitDirective.disabled$.pipe(takeUntil(this.destroy$)).subscribe((disabled) => { if (this.disabled !== false) { - buttonComponent.disabled = this.disabled || disabled; + buttonComponent.disabled.set(this.disabled || disabled); } }); } diff --git a/libs/components/src/async-actions/standalone.mdx b/libs/components/src/async-actions/standalone.mdx index efde494f2d..f484ea01c5 100644 --- a/libs/components/src/async-actions/standalone.mdx +++ b/libs/components/src/async-actions/standalone.mdx @@ -1,6 +1,7 @@ -import { Meta } from "@storybook/addon-docs"; +import { Meta, Story } from "@storybook/addon-docs"; +import * as stories from "./standalone.stories.ts"; - + # Standalone Async Actions @@ -8,9 +9,13 @@ These directives should be used when building a standalone button that triggers in the background, eg. Refresh buttons. For non-submit buttons that are associated with forms see [Async Actions In Forms](?path=/story/component-library-async-actions-in-forms-documentation--page). +If the long running background task resolves quickly (e.g. less than 75 ms), the loading spinner +will not display on the button. This prevents an undesirable "flicker" of the loading spinner when +it is not necessary for the user to see it. + ## Usage -Adding async actions to standalone buttons requires the following 2 steps +Adding async actions to standalone buttons requires the following 2 steps: ### 1. Add a handler to your `Component` @@ -60,3 +65,21 @@ from how click handlers are usually defined with the output syntax `(click)="han `; ``` + +## Stories + +### Promise resolves -- loading spinner is displayed + + + +### Promise resolves -- quickly without loading spinner + + + +### Promise rejects + + + +### Observable + + diff --git a/libs/components/src/async-actions/standalone.stories.ts b/libs/components/src/async-actions/standalone.stories.ts index f658dfb0f0..52b85b8856 100644 --- a/libs/components/src/async-actions/standalone.stories.ts +++ b/libs/components/src/async-actions/standalone.stories.ts @@ -11,9 +11,9 @@ import { IconButtonModule } from "../icon-button"; import { BitActionDirective } from "./bit-action.directive"; -const template = ` +const template = /*html*/ ` `; @@ -22,9 +22,30 @@ const template = ` selector: "app-promise-example", }) class PromiseExampleComponent { + statusEmoji = "🟡"; action = async () => { await new Promise((resolve, reject) => { - setTimeout(resolve, 2000); + setTimeout(() => { + resolve(); + this.statusEmoji = "🟢"; + }, 5000); + }); + }; +} + +@Component({ + template, + selector: "app-action-resolves-quickly", +}) +class ActionResolvesQuicklyComponent { + statusEmoji = "🟡"; + + action = async () => { + await new Promise((resolve, reject) => { + setTimeout(() => { + resolve(); + this.statusEmoji = "🟢"; + }, 50); }); }; } @@ -59,6 +80,7 @@ export default { PromiseExampleComponent, ObservableExampleComponent, RejectedPromiseExampleComponent, + ActionResolvesQuicklyComponent, ], imports: [ButtonModule, IconButtonModule, BitActionDirective], providers: [ @@ -100,3 +122,10 @@ export const RejectedPromise: ObservableStory = { template: ``, }), }; + +export const ActionResolvesQuickly: PromiseStory = { + render: (args) => ({ + props: args, + template: ``, + }), +}; diff --git a/libs/components/src/button/button.component.html b/libs/components/src/button/button.component.html index ee4d150dfc..a07ab9fb99 100644 --- a/libs/components/src/button/button.component.html +++ b/libs/components/src/button/button.component.html @@ -1,10 +1,10 @@ - + diff --git a/libs/components/src/button/button.component.ts b/libs/components/src/button/button.component.ts index 96311f9152..0b4ce3073c 100644 --- a/libs/components/src/button/button.component.ts +++ b/libs/components/src/button/button.component.ts @@ -2,7 +2,9 @@ // @ts-strict-ignore import { coerceBooleanProperty } from "@angular/cdk/coercion"; import { NgClass } from "@angular/common"; -import { Input, HostBinding, Component } from "@angular/core"; +import { Input, HostBinding, Component, model, computed } from "@angular/core"; +import { toObservable, toSignal } from "@angular/core/rxjs-interop"; +import { debounce, interval } from "rxjs"; import { ButtonLikeAbstraction, ButtonType } from "../shared/button-like.abstraction"; @@ -49,6 +51,9 @@ const buttonStyles: Record = { providers: [{ provide: ButtonLikeAbstraction, useExisting: ButtonComponent }], standalone: true, imports: [NgClass], + host: { + "[attr.disabled]": "disabledAttr()", + }, }) export class ButtonComponent implements ButtonLikeAbstraction { @HostBinding("class") get classList() { @@ -64,24 +69,41 @@ export class ButtonComponent implements ButtonLikeAbstraction { "tw-no-underline", "hover:tw-no-underline", "focus:tw-outline-none", - "disabled:tw-bg-secondary-300", - "disabled:hover:tw-bg-secondary-300", - "disabled:tw-border-secondary-300", - "disabled:hover:tw-border-secondary-300", - "disabled:!tw-text-muted", - "disabled:hover:!tw-text-muted", - "disabled:tw-cursor-not-allowed", - "disabled:hover:tw-no-underline", ] .concat(this.block ? ["tw-w-full", "tw-block"] : ["tw-inline-block"]) - .concat(buttonStyles[this.buttonType ?? "secondary"]); + .concat(buttonStyles[this.buttonType ?? "secondary"]) + .concat( + this.showDisabledStyles() || this.disabled() + ? [ + "disabled:tw-bg-secondary-300", + "disabled:hover:tw-bg-secondary-300", + "disabled:tw-border-secondary-300", + "disabled:hover:tw-border-secondary-300", + "disabled:!tw-text-muted", + "disabled:hover:!tw-text-muted", + "disabled:tw-cursor-not-allowed", + "disabled:hover:tw-no-underline", + ] + : [], + ); } - @HostBinding("attr.disabled") - get disabledAttr() { - const disabled = this.disabled != null && this.disabled !== false; - return disabled || this.loading ? true : null; - } + protected disabledAttr = computed(() => { + const disabled = this.disabled() != null && this.disabled() !== false; + return disabled || this.loading() ? true : null; + }); + + /** + * Determine whether it is appropriate to display the disabled styles. We only want to show + * the disabled styles if the button is truly disabled, or if the loading styles are also + * visible. + * + * We can't use `disabledAttr` for this, because it returns `true` when `loading` is `true`. + * We only want to show disabled styles during loading if `showLoadingStyles` is `true`. + */ + protected showDisabledStyles = computed(() => { + return this.showLoadingStyle() || (this.disabledAttr() && this.loading() === false); + }); @Input() buttonType: ButtonType; @@ -96,7 +118,23 @@ export class ButtonComponent implements ButtonLikeAbstraction { this._block = coerceBooleanProperty(value); } - @Input() loading = false; + loading = model(false); - @Input() disabled = false; + /** + * Determine whether it is appropriate to display a loading spinner. We only want to show + * a spinner if it's been more than 75 ms since the `loading` state began. This prevents + * a spinner "flash" for actions that are synchronous/nearly synchronous. + * + * We can't use `loading` for this, because we still need to disable the button during + * the full `loading` state. I.e. we only want the spinner to be debounced, not the + * loading state. + * + * This pattern of converting a signal to an observable and back to a signal is not + * recommended. TODO -- find better way to use debounce with signals (CL-596) + */ + protected showLoadingStyle = toSignal( + toObservable(this.loading).pipe(debounce((isLoading) => interval(isLoading ? 75 : 0))), + ); + + disabled = model(false); } diff --git a/libs/components/src/icon-button/icon-button.component.html b/libs/components/src/icon-button/icon-button.component.html index 6eeaaaffaf..0145f0b0ba 100644 --- a/libs/components/src/icon-button/icon-button.component.html +++ b/libs/components/src/icon-button/icon-button.component.html @@ -1,10 +1,10 @@ - + = { "hover:tw-bg-transparent-hover", "hover:tw-border-text-contrast", "focus-visible:before:tw-ring-text-contrast", - "disabled:tw-opacity-60", - "disabled:hover:tw-border-transparent", - "disabled:hover:tw-bg-transparent", ...focusRing, ], main: [ @@ -46,9 +45,6 @@ const styles: Record = { "hover:tw-bg-transparent-hover", "hover:tw-border-primary-600", "focus-visible:before:tw-ring-primary-600", - "disabled:!tw-text-secondary-300", - "disabled:hover:tw-border-transparent", - "disabled:hover:tw-bg-transparent", ...focusRing, ], muted: [ @@ -60,11 +56,8 @@ const styles: Record = { "hover:tw-bg-transparent-hover", "hover:tw-border-primary-600", "focus-visible:before:tw-ring-primary-600", - "disabled:!tw-text-secondary-300", "aria-expanded:hover:tw-bg-secondary-700", "aria-expanded:hover:tw-border-secondary-700", - "disabled:hover:tw-border-transparent", - "disabled:hover:tw-bg-transparent", ...focusRing, ], primary: [ @@ -74,9 +67,6 @@ const styles: Record = { "hover:tw-bg-primary-600", "hover:tw-border-primary-600", "focus-visible:before:tw-ring-primary-600", - "disabled:tw-opacity-60", - "disabled:hover:tw-border-primary-600", - "disabled:hover:tw-bg-primary-600", ...focusRing, ], secondary: [ @@ -86,10 +76,6 @@ const styles: Record = { "hover:!tw-text-contrast", "hover:tw-bg-text-muted", "focus-visible:before:tw-ring-primary-600", - "disabled:tw-opacity-60", - "disabled:hover:tw-border-text-muted", - "disabled:hover:tw-bg-transparent", - "disabled:hover:!tw-text-muted", ...focusRing, ], danger: [ @@ -100,10 +86,6 @@ const styles: Record = { "hover:tw-bg-transparent", "hover:tw-border-primary-600", "focus-visible:before:tw-ring-primary-600", - "disabled:!tw-text-secondary-300", - "disabled:hover:tw-border-transparent", - "disabled:hover:tw-bg-transparent", - "disabled:hover:!tw-text-secondary-300", ...focusRing, ], light: [ @@ -113,10 +95,48 @@ const styles: Record = { "hover:tw-bg-transparent-hover", "hover:tw-border-text-alt2", "focus-visible:before:tw-ring-text-alt2", + ...focusRing, + ], + unstyled: [], +}; + +const disabledStyles: Record = { + contrast: [ + "disabled:tw-opacity-60", + "disabled:hover:tw-border-transparent", + "disabled:hover:tw-bg-transparent", + ], + main: [ + "disabled:!tw-text-secondary-300", + "disabled:hover:tw-border-transparent", + "disabled:hover:tw-bg-transparent", + ], + muted: [ + "disabled:!tw-text-secondary-300", + "disabled:hover:tw-border-transparent", + "disabled:hover:tw-bg-transparent", + ], + primary: [ + "disabled:tw-opacity-60", + "disabled:hover:tw-border-primary-600", + "disabled:hover:tw-bg-primary-600", + ], + secondary: [ + "disabled:tw-opacity-60", + "disabled:hover:tw-border-text-muted", + "disabled:hover:tw-bg-transparent", + "disabled:hover:!tw-text-muted", + ], + danger: [ + "disabled:!tw-text-secondary-300", + "disabled:hover:tw-border-transparent", + "disabled:hover:tw-bg-transparent", + "disabled:hover:!tw-text-secondary-300", + ], + light: [ "disabled:tw-opacity-60", "disabled:hover:tw-border-transparent", "disabled:hover:tw-bg-transparent", - ...focusRing, ], unstyled: [], }; @@ -137,11 +157,14 @@ const sizes: Record = { ], standalone: true, imports: [NgClass], + host: { + "[attr.disabled]": "disabledAttr()", + }, }) export class BitIconButtonComponent implements ButtonLikeAbstraction, FocusableElement { @Input("bitIconButton") icon: string; - @Input() buttonType: IconButtonType; + @Input() buttonType: IconButtonType = "main"; @Input() size: IconButtonSize = "default"; @@ -155,22 +178,51 @@ export class BitIconButtonComponent implements ButtonLikeAbstraction, FocusableE "hover:tw-no-underline", "focus:tw-outline-none", ] - .concat(styles[this.buttonType ?? "main"]) - .concat(sizes[this.size]); + .concat(styles[this.buttonType]) + .concat(sizes[this.size]) + .concat(this.showDisabledStyles() || this.disabled() ? disabledStyles[this.buttonType] : []); } get iconClass() { return [this.icon, "!tw-m-0"]; } - @HostBinding("attr.disabled") - get disabledAttr() { - const disabled = this.disabled != null && this.disabled !== false; - return disabled || this.loading ? true : null; - } + protected disabledAttr = computed(() => { + const disabled = this.disabled() != null && this.disabled() !== false; + return disabled || this.loading() ? true : null; + }); - @Input() loading = false; - @Input() disabled = false; + /** + * Determine whether it is appropriate to display the disabled styles. We only want to show + * the disabled styles if the button is truly disabled, or if the loading styles are also + * visible. + * + * We can't use `disabledAttr` for this, because it returns `true` when `loading` is `true`. + * We only want to show disabled styles during loading if `showLoadingStyles` is `true`. + */ + protected showDisabledStyles = computed(() => { + return this.showLoadingStyle() || (this.disabledAttr() && this.loading() === false); + }); + + loading = model(false); + + /** + * Determine whether it is appropriate to display a loading spinner. We only want to show + * a spinner if it's been more than 75 ms since the `loading` state began. This prevents + * a spinner "flash" for actions that are synchronous/nearly synchronous. + * + * We can't use `loading` for this, because we still need to disable the button during + * the full `loading` state. I.e. we only want the spinner to be debounced, not the + * loading state. + * + * This pattern of converting a signal to an observable and back to a signal is not + * recommended. TODO -- find better way to use debounce with signals (CL-596) + */ + protected showLoadingStyle = toSignal( + toObservable(this.loading).pipe(debounce((isLoading) => interval(isLoading ? 75 : 0))), + ); + + disabled = model(false); getFocusTarget() { return this.elementRef.nativeElement; diff --git a/libs/components/src/shared/button-like.abstraction.ts b/libs/components/src/shared/button-like.abstraction.ts index 026a4b8302..5ee9d27259 100644 --- a/libs/components/src/shared/button-like.abstraction.ts +++ b/libs/components/src/shared/button-like.abstraction.ts @@ -1,8 +1,10 @@ // FIXME: Update this file to be type safe and remove this and next line +import { ModelSignal } from "@angular/core"; + // @ts-strict-ignore export type ButtonType = "primary" | "secondary" | "danger" | "unstyled"; export abstract class ButtonLikeAbstraction { - loading: boolean; - disabled: boolean; + loading: ModelSignal; + disabled: ModelSignal; } diff --git a/libs/tools/send/send-ui/src/send-form/components/send-form.component.ts b/libs/tools/send/send-ui/src/send-form/components/send-form.component.ts index 8abdaa69bb..3149307bdd 100644 --- a/libs/tools/send/send-ui/src/send-form/components/send-form.component.ts +++ b/libs/tools/send/send-ui/src/send-form/components/send-form.component.ts @@ -120,11 +120,11 @@ export class SendFormComponent implements AfterViewInit, OnInit, OnChanges, Send ngAfterViewInit(): void { if (this.submitBtn) { this.bitSubmit.loading$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((loading) => { - this.submitBtn.loading = loading; + this.submitBtn.loading.set(loading); }); this.bitSubmit.disabled$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((disabled) => { - this.submitBtn.disabled = disabled; + this.submitBtn.disabled.set(disabled); }); } } diff --git a/libs/vault/src/cipher-form/components/attachments/cipher-attachments.component.spec.ts b/libs/vault/src/cipher-form/components/attachments/cipher-attachments.component.spec.ts index ce12ca95e1..f8aeb695e4 100644 --- a/libs/vault/src/cipher-form/components/attachments/cipher-attachments.component.spec.ts +++ b/libs/vault/src/cipher-form/components/attachments/cipher-attachments.component.spec.ts @@ -103,7 +103,7 @@ describe("CipherAttachmentsComponent", () => { fixture = TestBed.createComponent(CipherAttachmentsComponent); component = fixture.componentInstance; component.cipherId = "5555-444-3333" as CipherId; - component.submitBtn = {} as ButtonComponent; + component.submitBtn = TestBed.createComponent(ButtonComponent).componentInstance; fixture.detectChanges(); }); @@ -134,34 +134,38 @@ describe("CipherAttachmentsComponent", () => { describe("bitSubmit", () => { beforeEach(() => { - component.submitBtn.disabled = undefined; - component.submitBtn.loading = undefined; + component.submitBtn.disabled.set(undefined); + component.submitBtn.loading.set(undefined); }); it("updates sets initial state of the submit button", async () => { await component.ngOnInit(); - expect(component.submitBtn.disabled).toBe(true); + expect(component.submitBtn.disabled()).toBe(true); }); it("sets submitBtn loading state", () => { + jest.useFakeTimers(); + component.bitSubmit.loading = true; - expect(component.submitBtn.loading).toBe(true); + jest.runAllTimers(); + + expect(component.submitBtn.loading()).toBe(true); component.bitSubmit.loading = false; - expect(component.submitBtn.loading).toBe(false); + expect(component.submitBtn.loading()).toBe(false); }); it("sets submitBtn disabled state", () => { component.bitSubmit.disabled = true; - expect(component.submitBtn.disabled).toBe(true); + expect(component.submitBtn.disabled()).toBe(true); component.bitSubmit.disabled = false; - expect(component.submitBtn.disabled).toBe(false); + expect(component.submitBtn.disabled()).toBe(false); }); }); @@ -169,7 +173,7 @@ describe("CipherAttachmentsComponent", () => { let file: File; beforeEach(() => { - component.submitBtn.disabled = undefined; + component.submitBtn.disabled.set(undefined); file = new File([""], "attachment.txt", { type: "text/plain" }); const inputElement = fixture.debugElement.query(By.css("input[type=file]")); @@ -189,7 +193,7 @@ describe("CipherAttachmentsComponent", () => { }); it("updates disabled state of submit button", () => { - expect(component.submitBtn.disabled).toBe(false); + expect(component.submitBtn.disabled()).toBe(false); }); }); diff --git a/libs/vault/src/cipher-form/components/attachments/cipher-attachments.component.ts b/libs/vault/src/cipher-form/components/attachments/cipher-attachments.component.ts index 7e26e8afae..5380f9e434 100644 --- a/libs/vault/src/cipher-form/components/attachments/cipher-attachments.component.ts +++ b/libs/vault/src/cipher-form/components/attachments/cipher-attachments.component.ts @@ -114,7 +114,7 @@ export class CipherAttachmentsComponent implements OnInit, AfterViewInit { return; } - this.submitBtn.disabled = status !== "VALID"; + this.submitBtn.disabled.set(status !== "VALID"); }); } @@ -127,7 +127,7 @@ export class CipherAttachmentsComponent implements OnInit, AfterViewInit { // Update the initial state of the submit button if (this.submitBtn) { - this.submitBtn.disabled = !this.attachmentForm.valid; + this.submitBtn.disabled.set(!this.attachmentForm.valid); } } @@ -137,7 +137,7 @@ export class CipherAttachmentsComponent implements OnInit, AfterViewInit { return; } - this.submitBtn.loading = loading; + this.submitBtn.loading.set(loading); }); this.bitSubmit.disabled$.pipe(takeUntilDestroyed(this.destroy$)).subscribe((disabled) => { @@ -145,7 +145,7 @@ export class CipherAttachmentsComponent implements OnInit, AfterViewInit { return; } - this.submitBtn.disabled = disabled; + this.submitBtn.disabled.set(disabled); }); } diff --git a/libs/vault/src/cipher-form/components/cipher-form.component.ts b/libs/vault/src/cipher-form/components/cipher-form.component.ts index 7335471799..080af48925 100644 --- a/libs/vault/src/cipher-form/components/cipher-form.component.ts +++ b/libs/vault/src/cipher-form/components/cipher-form.component.ts @@ -144,11 +144,11 @@ export class CipherFormComponent implements AfterViewInit, OnInit, OnChanges, Ci ngAfterViewInit(): void { if (this.submitBtn) { this.bitSubmit.loading$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((loading) => { - this.submitBtn.loading = loading; + this.submitBtn.loading.set(loading); }); this.bitSubmit.disabled$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((disabled) => { - this.submitBtn.disabled = disabled; + this.submitBtn.disabled.set(disabled); }); } } diff --git a/libs/vault/src/cipher-form/components/item-details/item-details-section.component.spec.ts b/libs/vault/src/cipher-form/components/item-details/item-details-section.component.spec.ts index aa68770774..074b4b9e4b 100644 --- a/libs/vault/src/cipher-form/components/item-details/item-details-section.component.spec.ts +++ b/libs/vault/src/cipher-form/components/item-details/item-details-section.component.spec.ts @@ -250,6 +250,7 @@ describe("ItemDetailsSectionComponent", () => { describe("showOwnership", () => { it("should return true if ownership change is allowed or in edit mode with at least one organization", () => { + component.config.allowPersonalOwnership = true; jest.spyOn(component, "allowOwnershipChange", "get").mockReturnValue(true); expect(component.showOwnership).toBe(true); @@ -261,6 +262,7 @@ describe("ItemDetailsSectionComponent", () => { }); it("should hide the ownership control if showOwnership is false", async () => { + component.config.allowPersonalOwnership = true; jest.spyOn(component, "showOwnership", "get").mockReturnValue(false); fixture.detectChanges(); await fixture.whenStable(); @@ -271,6 +273,7 @@ describe("ItemDetailsSectionComponent", () => { }); it("should show the ownership control if showOwnership is true", async () => { + component.config.allowPersonalOwnership = true; jest.spyOn(component, "allowOwnershipChange", "get").mockReturnValue(true); fixture.detectChanges(); await fixture.whenStable(); diff --git a/libs/vault/src/components/add-edit-folder-dialog/add-edit-folder-dialog.component.ts b/libs/vault/src/components/add-edit-folder-dialog/add-edit-folder-dialog.component.ts index 362063ff34..0979fc7356 100644 --- a/libs/vault/src/components/add-edit-folder-dialog/add-edit-folder-dialog.component.ts +++ b/libs/vault/src/components/add-edit-folder-dialog/add-edit-folder-dialog.component.ts @@ -104,7 +104,7 @@ export class AddEditFolderDialogComponent implements AfterViewInit, OnInit { return; } - this.submitBtn.loading = loading; + this.submitBtn.loading.set(loading); }); } diff --git a/libs/vault/src/components/assign-collections.component.ts b/libs/vault/src/components/assign-collections.component.ts index 76a6a1b10a..6fbad5ac1c 100644 --- a/libs/vault/src/components/assign-collections.component.ts +++ b/libs/vault/src/components/assign-collections.component.ts @@ -213,7 +213,7 @@ export class AssignCollectionsComponent implements OnInit, OnDestroy, AfterViewI return; } - this.submitBtn.loading = loading; + this.submitBtn.loading.set(loading); }); this.bitSubmit.disabled$.pipe(takeUntil(this.destroy$)).subscribe((disabled) => { @@ -221,7 +221,7 @@ export class AssignCollectionsComponent implements OnInit, OnDestroy, AfterViewI return; } - this.submitBtn.disabled = disabled; + this.submitBtn.disabled.set(disabled); }); }