Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<div class="flex gap-2 items-center flex-wrap justify-center" data-testId="category-list">
<div class="flex flex-wrap items-center justify-start gap-2" data-testId="category-list">
@for (category of $categoriesToShow(); track category.key) {
<p-chip
[pTooltip]="category.path || category.value"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,13 @@ describe('DotCategoryFieldChipsComponent', () => {
expect(spectator.component).toBeTruthy();
});

it('should left-align the chips list container', () => {
spectator.detectChanges();
const container = spectator.query(byTestId('category-list'));
expect(container.classList).toContain('justify-start');
expect(container.classList).not.toContain('justify-center');
});

it('should the max input be equal to constant by default', () => {
spectator.detectChanges();
expect(spectator.component.$max()).toBe(MAX_CHIPS);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,8 @@
<p-button
(click)="closedDialog.emit()"
[label]="'Cancel' | dm"
class="p-button-outlined"
[text]="true"
severity="secondary"
data-testId="dialog-cancel" />
<p-button (click)="confirmCategories()" [label]="'Apply' | dm" data-testId="dialog-apply" />
</ng-template>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ import { expect, it } from '@jest/globals';
import { byTestId, createComponentFactory, mockProvider, Spectator } from '@ngneat/spectator/jest';
import { of } from 'rxjs';

import { By } from '@angular/platform-browser';

import { Button } from 'primeng/button';
import { Dialog } from 'primeng/dialog';

import { DotHttpErrorManagerService, DotMessageService } from '@dotcms/data-access';
Expand Down Expand Up @@ -94,6 +97,21 @@ describe('DotCategoryFieldDialogComponent', () => {
expect(addConfirmedCategoriesSky).toHaveBeenCalled();
});

it('should render `Cancel` as tertiary (text + secondary) and `Apply` as primary', () => {
const cancelButton = spectator.fixture.debugElement
.query(By.css('[data-testId="dialog-cancel"]'))
.injector.get(Button);
const applyButton = spectator.fixture.debugElement
.query(By.css('[data-testId="dialog-apply"]'))
.injector.get(Button);

expect(cancelButton.text).toBe(true);
expect(cancelButton.severity).toBe('secondary');

expect(applyButton.text).toBeFalsy();
expect(applyButton.severity).toBeFalsy();
});

it('should render the CategoryFieldCategoryList component', () => {
expect(spectator.query(DotCategoryFieldCategoryListComponent)).not.toBeNull();
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,23 @@
</div>
}

<div class="dot-category-field__actions flex flex-wrap items-center px-0 justify-end">
<div class="dot-category-field__actions flex flex-wrap items-center justify-end px-0">
@if ($hasSelectedCategories()) {
<button
type="button"
(click)="clearAllSelected()"
Comment thread
adrianjm-dotCMS marked this conversation as resolved.
[disabled]="$isDisabled()"
[label]="'edit.content.category-field.clear-all' | dm"
class="p-button-sm p-button-text p-button-secondary"
data-testId="clear-all-btn"
pButton></button>
}
<button
type="button"
(click)="openCategoriesDialog()"
[disabled]="store.isDialogOpen() || $isDisabled()"
[label]="'edit.content.category-field.show-categories-dialog' | dm"
class="p-button-sm p-button-text p-button-secondary"
class="p-button-sm m-1"
data-testId="show-dialog-btn"
Comment thread
adrianjm-dotCMS marked this conversation as resolved.
pButton></button>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { MockComponent } from 'ng-mocks';
import { of } from 'rxjs';

import { Component } from '@angular/core';
import { fakeAsync } from '@angular/core/testing';
import { fakeAsync, tick } from '@angular/core/testing';
import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms';

import { DotHttpErrorManagerService, DotMessageService } from '@dotcms/data-access';
Expand Down Expand Up @@ -96,6 +96,67 @@ describe('DotCategoryFieldComponent', () => {
expect(selectBtn.type).toBe('button');
});

it('should render the `Select` button as primary', () => {
spectator.detectChanges();
const selectBtn = spectator.query<HTMLButtonElement>(byTestId('show-dialog-btn'));
expect(selectBtn.classList).not.toContain('p-button-secondary');
expect(selectBtn.classList).not.toContain('p-button-text');
});

it('should render a `Clear all` button when there are selected categories', () => {
spectator.detectChanges();
expect(spectator.query(byTestId('clear-all-btn'))).not.toBeNull();
});

it('should invoke `clearAllSelected` method when the `Clear all` button is clicked', () => {
spectator.detectChanges();
const clearAllBtn = spectator.query(byTestId('clear-all-btn'));
const clearAllSelectedSpy = jest.spyOn(spectator.component, 'clearAllSelected');
expect(clearAllBtn).not.toBeNull();

spectator.click(clearAllBtn);

expect(clearAllSelectedSpy).toHaveBeenCalled();
});

it('should clear the store selection and emit an empty value when `Clear all` is clicked', fakeAsync(() => {
spectator.detectChanges();
spectator.component.ngOnInit();
spectator.detectChanges();
expect(spectator.component.store.selected().length).toBe(2);

// `onChange` is the ControlValueAccessor callback the effect() in
// ngOnInit calls whenever `store.selected()` changes; spying on it
// directly is more reliable in this test harness than reading the
// value back off the shared host FormGroup (see the disabled tests
// at the bottom of this file for the same limitation).
const onChangeSpy = jest.spyOn(
spectator.component as unknown as { onChange: (value: unknown) => void },
'onChange'
);

const clearAllBtn = spectator.query(byTestId('clear-all-btn'));
spectator.click(clearAllBtn);
spectator.detectChanges();
spectator.flushEffects();
tick();

expect(spectator.component.store.selected().length).toBe(0);
expect(onChangeSpy).toHaveBeenCalledWith([]);
}));

it('should not render the `Clear all` button after clearing all selected categories', fakeAsync(() => {
spectator.detectChanges();
spectator.component.ngOnInit();
spectator.detectChanges();
const clearAllBtn = spectator.query(byTestId('clear-all-btn'));
spectator.click(clearAllBtn);
spectator.detectChanges();
tick();

expect(spectator.query(byTestId('clear-all-btn'))).toBeNull();
}));

it('should display the category list with chips when there are categories', async () => {
spectator.detectChanges();
spectator.component.ngOnInit();
Expand Down Expand Up @@ -145,6 +206,32 @@ describe('DotCategoryFieldComponent', () => {

expect(spectator.query(byTestId('category-chip-list'))).toBeNull();
});

it('should not render the `Clear all` button when there are no categories', () => {
spectator = createHost(
`<form [formGroup]="formGroup">
<dot-category-field [field]="field" [contentlet]="contentlet" formControlName="categorias" [hasError]="hasError" />
</form>`,
{
hostProps: {
formGroup: FAKE_FORM_GROUP,
field: CATEGORY_FIELD_MOCK,
contentlet: {
...CATEGORY_FIELD_CONTENTLET_MOCK,
[CATEGORY_FIELD_MOCK.variable]: []
},
hasError: false
}
}
);

service = spectator.inject(CategoriesService, true);
service.getSelectedHierarchy.mockReturnValue(of([]));

spectator.detectChanges();

expect(spectator.query(byTestId('clear-all-btn'))).toBeNull();
});
});

describe('Interactions', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,20 @@ export class DotCategoryFieldComponent
this.onTouched();
}

/**
* Clear all selected categories from the field without opening the dialog.
*
* @memberof DotEditContentCategoryFieldComponent
*/
clearAllSelected(): void {
if (this.$isDisabled()) {
return;
}

this.store.removeRootSelected(this.store.selected().map((category) => category.key));
this.onTouched();
}

override writeValue(value: string[]): void {
super.writeValue(value);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@
(completeMethod)="onSearch($event)"
(keydown.enter)="onEnterKey($event)">
<ng-template pTemplate="removetokenicon">
<i class="pi pi-times"></i>
<span class="flex items-center leading-none">
<i class="pi pi-times text-xs"></i>
</span>
</ng-template>
</p-autoComplete>
10 changes: 10 additions & 0 deletions core-web/libs/ui/src/lib/theme/theme.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,12 +88,22 @@ export const CustomLaraPreset = definePreset(Lara, {
// without per-template classes. PrimeNG has no chip size token, so this
// is expressed as CSS — same mechanism as card/confirmpopup. Content
// status badges use `p-tag` (see the `tag` block below), not chips.
//
// The remove icon is flipped to the left of the label app-wide via flexbox
// `order` (`.p-chip` is `display:flex`): PrimeNG's Chip template always
// renders the remove icon after the label with no input to reorder it, so
// this is the only way to achieve it without forking the component. DOM
// order (and keyboard focus order) is unaffected — only the visual order
// changes.
css: `
.p-chip {
height: calc(var(--spacing) * 7); /* 1.75rem */
padding: 0 calc(var(--spacing) * 2); /* 0.5rem */
font-size: var(--text-xs); /* 0.75rem */
}
.p-chip .p-chip-remove-icon {
order: -1;
}
`
},
tag: {
Expand Down
3 changes: 0 additions & 3 deletions dotCMS/src/main/resources/dotmarketing-config.properties
Original file line number Diff line number Diff line change
Expand Up @@ -870,9 +870,6 @@ FEATURE_FLAG_UVE_LEGACY_SCRIPT_INJECTION=false
## Enhanced locale selector v2 in the edit-content sidebar
FEATURE_FLAG_LOCALE_SELECTOR_V2=true

## Enhanced locale selector v2 in the edit-content sidebar
FEATURE_FLAG_LOCALE_SELECTOR_V2=true

## libvips image engine toggle. Off by default (legacy Java2D engine). The new image
## editor reads this (via the configuration endpoint) to gate the libvips-only AVIF
## output format. Declared here so the endpoint returns an explicit boolean instead
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6314,6 +6314,7 @@ edit.content.unsaved.changes.discard=Discard changes
edit.content.unsaved.changes.keep=Keep editing

edit.content.category-field.show-categories-dialog=Select
edit.content.category-field.clear-all=Clear all
edit.content.category-field.dialog.header.select-categories=Select categories
edit.content.category-field.dialog.button.clear-all=Clear all

Expand Down
Loading