Skip to content

Commit

Permalink
chore: Fix error loading the DotCategoryFieldSidebarComponent (#29279)
Browse files Browse the repository at this point in the history
### Parent Issue

#29275

### Proposed Changes
* Add proper typing to changeMode output
* Move the effect to ngOnInit with explicit inject to avoid form control
errors
* Change hasSelectedCategories to a calculated signal
* Add `type=button` in category button to avoid events with other input
fields

### Checklist
- [x] Tests
- [x] Translations
- [x] Security Implications Contemplated (add notes if applicable)

### Additional Info

As a user, when you create new content with a text field, focus on that
field, and then press Enter, the side menu related to the category field
appears, and this behavior is a bug.


https://github.com/user-attachments/assets/95efc9de-1aec-4686-9ded-640ee95f3b84
  • Loading branch information
nicobytes authored Jul 22, 2024
1 parent 2479a88 commit 9bee608
Show file tree
Hide file tree
Showing 10 changed files with 3,322 additions and 3,299 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import { debounceTime, distinctUntilChanged, filter, tap } from 'rxjs/operators'

import { DotMessagePipe } from '@dotcms/ui';

import { CategoryFieldViewMode } from '../../models/dot-category-field.models';

export const DEBOUNCE_TIME = 300;

const MINIMUM_CHARACTERS = 3;
Expand All @@ -33,7 +35,7 @@ export class DotCategoryFieldSearchComponent {
/**
* Represent a EventEmitter to notify we want change the mode to `list`.
*/
@Output() changeMode = new EventEmitter<string>();
@Output() changeMode = new EventEmitter<CategoryFieldViewMode>();

/**
* Represents the boolean variable isLoading.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
<ng-template pTemplate="header">
<div class="flex flex-row category-field__header">
<button
type="button"
(click)="sidebar.close($event)"
class="p-button-rounded p-button-text"
data-testId="back-btn"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ describe('DotEditContentCategoryFieldSidebarComponent', () => {

beforeEach(() => {
spectator = createComponent({
props: {
visible: true
},
providers: [
mockProvider(CategoriesService, {
getChildren: jest.fn().mockReturnValue(of(CATEGORY_LIST_MOCK))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@ import {
ChangeDetectionStrategy,
Component,
computed,
DestroyRef,
EventEmitter,
inject,
Input,
OnDestroy,
OnInit,
Output
} from '@angular/core';
Expand Down Expand Up @@ -60,29 +61,33 @@ import { DotCategoryFieldSelectedComponent } from '../dot-category-field-selecte
])
]
})
export class DotCategoryFieldSidebarComponent implements OnInit {
export class DotCategoryFieldSidebarComponent implements OnInit, OnDestroy {
/**
* Indicates whether the sidebar is visible or not.
* Indicates the visibility of the sidebar.
*
* @memberof DotCategoryFieldSidebarComponent
*/
visible = true;

@Input() visible = false;
/**
* Output that emit if the sidebar is closed
*/
@Output() closedSidebar = new EventEmitter<void>();

readonly store: InstanceType<typeof CategoryFieldStore> = inject(CategoryFieldStore);
/**
* Store based on the `CategoryFieldStore`.
*
* @memberof DotCategoryFieldSidebarComponent
*/
readonly store = inject(CategoryFieldStore);
/**
* Computed property for retrieving all category keys.
*/
$allCategoryKeys = computed(() => this.store.selected().map((category) => category.key));
readonly #destroyRef = inject(DestroyRef);

ngOnInit(): void {
this.store.getCategories();
}

this.#destroyRef.onDestroy(() => {
this.store.clean();
});
ngOnDestroy(): void {
this.store.clean();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,20 @@

<div class="dot-category-field__select">
<button
(click)="showCategoriesSidebar()"
[disabled]="disableSelectCategoriesButton()"
type="button"
(click)="openCategoriesSidebar()"
[disabled]="$showCategoriesSidebar()"
[label]="'edit.content.category-field.show-categories-dialog' | dm"
class="p-button-sm p-button-text p-button-secondary"
data-testId="show-sidebar-btn"
pButton></button>
</div>
<ng-container data-testId="sidebar-placeholder" dotDynamic></ng-container>

@if ($showCategoriesSidebar()) {
@defer (when $showCategoriesSidebar()) {
<dot-category-field-sidebar
[visible]="$showCategoriesSidebar()"
(closedSidebar)="closeCategoriesSidebar()"
data-testId="sidebar-placeholder" />
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import { DotCMSContentlet } from '@dotcms/dotcms-models';

import { DotCategoryFieldSidebarComponent } from './components/dot-category-field-sidebar/dot-category-field-sidebar.component';
import { DotEditContentCategoryFieldComponent } from './dot-edit-content-category-field.component';
import { CLOSE_SIDEBAR_CSS_DELAY_MS } from './dot-edit-content-category-field.const';
import {
CATEGORY_FIELD_CONTENTLET_MOCK,
CATEGORY_FIELD_MOCK,
Expand Down Expand Up @@ -75,6 +74,11 @@ describe('DotEditContentCategoryFieldComponent', () => {
expect(spectator.query(byTestId('show-sidebar-btn'))).not.toBeNull();
});

it('should the button be type=button', () => {
const selectBtn = spectator.query<HTMLButtonElement>(byTestId('show-sidebar-btn'));
expect(selectBtn.type).toBe('button');
});

it('should display the category list with chips when there are categories', () => {
expect(spectator.query(byTestId('category-chip-list'))).not.toBeNull();
});
Expand Down Expand Up @@ -125,11 +129,12 @@ describe('DotEditContentCategoryFieldComponent', () => {

spectator.detectChanges();
});

it('should invoke `showCategoriesSidebar` method when the select button is clicked', () => {
const selectBtn = spectator.query(byTestId('show-sidebar-btn'));
const showCategoriesSidebarSpy = jest.spyOn(
spectator.component,
'showCategoriesSidebar'
'openCategoriesSidebar'
);
expect(selectBtn).not.toBeNull();

Expand All @@ -138,7 +143,7 @@ describe('DotEditContentCategoryFieldComponent', () => {
expect(showCategoriesSidebarSpy).toHaveBeenCalled();
});

it('should disable the `Select` button after `showCategoriesSidebar` method is invoked', () => {
it('should disable the `Select` button after `openCategoriesSidebar` method is invoked', () => {
const selectBtn = spectator.query(byTestId('show-sidebar-btn')) as HTMLButtonElement;
expect(selectBtn).not.toBeNull();

Expand All @@ -149,21 +154,24 @@ describe('DotEditContentCategoryFieldComponent', () => {
expect(selectBtn.disabled).toBe(true);
});

it('should create a DotEditContentCategoryFieldSidebarComponent instance when the `Select` button is clicked', () => {
const selectBtn = spectator.query(byTestId('show-sidebar-btn')) as HTMLButtonElement;
it('should create a DotEditContentCategoryFieldSidebarComponent instance when the `Select` button is clicked', async () => {
const selectBtn = spectator.query<HTMLButtonElement>(byTestId('show-sidebar-btn'));
expect(selectBtn).not.toBeNull();

expect(spectator.query(DotCategoryFieldSidebarComponent)).toBeNull();

spectator.click(selectBtn);
await spectator.fixture.whenStable();

expect(spectator.query(DotCategoryFieldSidebarComponent)).not.toBeNull();
});

it('should remove DotEditContentCategoryFieldSidebarComponent when `closedSidebar` emit', fakeAsync(() => {
it('should remove DotEditContentCategoryFieldSidebarComponent when `closedSidebar` emit', fakeAsync(async () => {
const selectBtn = spectator.query(byTestId('show-sidebar-btn')) as HTMLButtonElement;

expect(selectBtn).not.toBeNull();
spectator.click(selectBtn);
await spectator.fixture.whenStable();

const sidebarComponentRef = spectator.query(DotCategoryFieldSidebarComponent);
expect(sidebarComponentRef).not.toBeNull();
Expand All @@ -172,9 +180,6 @@ describe('DotEditContentCategoryFieldComponent', () => {

spectator.detectComponentChanges();

// Due to a delay in the pipe of the subscription
spectator.tick(CLOSE_SIDEBAR_CSS_DELAY_MS + 100);

// Check if the sidebar component is removed
expect(spectator.query(DotCategoryFieldSidebarComponent)).toBeNull();

Expand All @@ -192,7 +197,6 @@ describe('DotEditContentCategoryFieldComponent', () => {
key: '1234',
value: 'test'
});

spectator.flushEffects();

const categoryValue = spectator.component.categoryFieldControl.value;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,28 +2,23 @@ import { NgClass } from '@angular/common';
import {
ChangeDetectionStrategy,
Component,
ComponentRef,
DestroyRef,
computed,
effect,
inject,
Injector,
input,
OnInit,
signal,
ViewChild
signal
} from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { ControlContainer, FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms';

import { ButtonModule } from 'primeng/button';

import { delay } from 'rxjs/operators';

import { DotCMSContentlet, DotCMSContentTypeField } from '@dotcms/dotcms-models';
import { DotDynamicDirective, DotMessagePipe } from '@dotcms/ui';
import { DotMessagePipe } from '@dotcms/ui';

import { DotCategoryFieldChipsComponent } from './components/dot-category-field-chips/dot-category-field-chips.component';
import { DotCategoryFieldSidebarComponent } from './components/dot-category-field-sidebar/dot-category-field-sidebar.component';
import { CLOSE_SIDEBAR_CSS_DELAY_MS } from './dot-edit-content-category-field.const';
import { CategoriesService } from './services/categories.service';
import { CategoryFieldStore } from './store/content-category-field.store';

Expand All @@ -43,15 +38,15 @@ import { CategoryFieldStore } from './store/content-category-field.store';
ButtonModule,
NgClass,
DotMessagePipe,
DotDynamicDirective,
DotCategoryFieldChipsComponent
DotCategoryFieldChipsComponent,
DotCategoryFieldSidebarComponent
],
templateUrl: './dot-edit-content-category-field.component.html',
styleUrl: './dot-edit-content-category-field.component.scss',
changeDetection: ChangeDetectionStrategy.OnPush,
host: {
'[class.dot-category-field__container--has-categories]': 'hasSelectedCategories()',
'[class.dot-category-field__container]': '!hasSelectedCategories()'
'[class.dot-category-field__container--has-categories]': '$hasSelectedCategories()',
'[class.dot-category-field__container]': '!$hasSelectedCategories()'
},
viewProviders: [
{
Expand All @@ -62,90 +57,71 @@ import { CategoryFieldStore } from './store/content-category-field.store';
providers: [CategoriesService, CategoryFieldStore]
})
export class DotEditContentCategoryFieldComponent implements OnInit {
readonly store = inject(CategoryFieldStore);
readonly #form = inject(ControlContainer).control as FormGroup;
readonly #injector = inject(Injector);
/**
* Disable the button to open the sidebar
*/
disableSelectCategoriesButton = signal(false);

@ViewChild(DotDynamicDirective, { static: true }) sidebarHost!: DotDynamicDirective;

$showCategoriesSidebar = signal(false);
/**
* The `field` variable is of type `DotCMSContentTypeField` and is a required input.
* @description The variable represents a field of a DotCMS content type and is a required input.
*/
field = input.required<DotCMSContentTypeField>();

/**
* Represents a DotCMS contentlet and is a required input
* @description DotCMSContentlet input representing a DotCMS contentlet.
*/
contentlet = input.required<DotCMSContentlet>();

readonly store: InstanceType<typeof CategoryFieldStore> = inject(CategoryFieldStore);
readonly #form = inject(ControlContainer).control as FormGroup;
readonly #destroyRef = inject(DestroyRef);
#componentRef: ComponentRef<DotCategoryFieldSidebarComponent>;

constructor() {
effect(() => {
const categoryValues = this.store.selectedCategoriesValues();

if (this.categoryFieldControl) {
this.categoryFieldControl.setValue(categoryValues);
}
});
}

/**
* Retrieve the category field control.
* The `$hasSelectedCategories` variable is a computed property that returns a boolean value.
*
* @return {FormControl} The category field control.
* @returns {Boolean} - True if there are selected categories, false otherwise.
*/
get categoryFieldControl(): FormControl {
return this.#form.get(this.store.fieldVariableName()) as FormControl;
}

$hasSelectedCategories = computed(() => !!this.store.hasSelectedCategories());
/**
* Determines if there are any selected categories.
* Getter to retrieve the category field control.
*
* @returns {Boolean} - True if there are selected categories, false otherwise.
* @return {FormControl} The category field control.
*/
hasSelectedCategories(): boolean {
return !!this.store.hasSelectedCategories();
get categoryFieldControl(): FormControl {
return this.#form.get(this.store.fieldVariableName()) as FormControl;
}

/**
* Open the "DotEditContentCategoryFieldDialogComponent" dialog to show the list of categories.
* Initialize the component.
*
* @returns {void}
* @memberof DotEditContentCategoryFieldComponent
*/
showCategoriesSidebar(): void {
this.disableSelectCategoriesButton.set(true);
this.#componentRef = this.sidebarHost.viewContainerRef.createComponent(
DotCategoryFieldSidebarComponent
);

this.setSidebarListener();
}

ngOnInit(): void {
this.store.load({
field: this.field(),
contentlet: this.contentlet()
});
effect(
() => {
const categoryValues = this.store.selectedCategoriesValues();
this.categoryFieldControl.setValue(categoryValues);
},
{
injector: this.#injector
}
);
}

private setSidebarListener() {
this.#componentRef.instance.closedSidebar
.pipe(takeUntilDestroyed(this.#destroyRef), delay(CLOSE_SIDEBAR_CSS_DELAY_MS))
.subscribe(() => {
// enable the show sidebar button
this.disableSelectCategoriesButton.set(false);
this.removeDotCategoryFieldSidebarComponent();
});
/**
* Open the categories sidebar.
*
* @memberof DotEditContentCategoryFieldComponent
*/
openCategoriesSidebar(): void {
this.$showCategoriesSidebar.set(true);
}

private removeDotCategoryFieldSidebarComponent() {
this.sidebarHost.viewContainerRef.clear();
/**
* Close the categories sidebar.
*
* @memberof DotEditContentCategoryFieldComponent
*/
closeCategoriesSidebar() {
this.$showCategoriesSidebar.set(false);
}
}
Original file line number Diff line number Diff line change
@@ -1,3 +1 @@
export const CLOSE_SIDEBAR_CSS_DELAY_MS = 300;

export const MAX_CHIPS = 10;
Loading

0 comments on commit 9bee608

Please sign in to comment.