From 97ca90390af3e2650249e860dac57166e9246ffe Mon Sep 17 00:00:00 2001 From: Adrian Molina Date: Mon, 6 Jul 2026 14:08:40 -0400 Subject: [PATCH 1/3] fix(edit-content): category field button styles, clear-all, chip alignment & X position (#36379) - Apply is primary / Cancel is tertiary (text + secondary) in the "Select categories" dialog, matching the convention used in other recent dialogs. - Select button on the category field is now primary. - Added a field-level "Clear all" so users can remove every selected category without opening the dialog; the field value auto-empties via the existing selected-categories effect. - Fixed chips centering on wrap (justify-center -> justify-start). - Moved the chip remove ("X") icon to the left of the label app-wide via a theme-level flexbox order rule, since PrimeNG's Chip template has no input to reorder it. This also propagates to the Tags field, whose multi-value tokens render as p-chip internally. - Vertically centered the Tags field remove icon to match Category's chip markup (unrelated visual inconsistency spotted while comparing both chip implementations). Also removes a duplicated FEATURE_FLAG_LOCALE_SELECTOR_V2 line in dotmarketing-config.properties (unrelated one-line cleanup). Co-Authored-By: Claude Sonnet 5 --- .../dot-category-field-chips.component.html | 2 +- .../dot-category-field-dialog.component.html | 3 ++- .../dot-category-field.component.html | 14 ++++++++++++-- .../dot-category-field.component.ts | 14 ++++++++++++++ .../components/tag-field/tag-field.component.html | 4 +++- core-web/libs/ui/src/lib/theme/theme.config.ts | 10 ++++++++++ .../main/resources/dotmarketing-config.properties | 3 --- .../webapp/WEB-INF/messages/Language.properties | 1 + 8 files changed, 43 insertions(+), 8 deletions(-) diff --git a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/components/dot-category-field-chips/dot-category-field-chips.component.html b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/components/dot-category-field-chips/dot-category-field-chips.component.html index 6fc2519e0df4..893f0a9d18e3 100644 --- a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/components/dot-category-field-chips/dot-category-field-chips.component.html +++ b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/components/dot-category-field-chips/dot-category-field-chips.component.html @@ -1,4 +1,4 @@ -
+
@for (category of $categoriesToShow(); track category.key) { diff --git a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/components/dot-category-field/dot-category-field.component.html b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/components/dot-category-field/dot-category-field.component.html index edcc55d77097..9dc28f73e9d8 100644 --- a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/components/dot-category-field/dot-category-field.component.html +++ b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/components/dot-category-field/dot-category-field.component.html @@ -8,13 +8,23 @@
} -
+
+ @if ($hasSelectedCategories()) { + + }
diff --git a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/components/dot-category-field/dot-category-field.component.ts b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/components/dot-category-field/dot-category-field.component.ts index 6684c2e34d82..4a64d20d9bf6 100644 --- a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/components/dot-category-field/dot-category-field.component.ts +++ b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/components/dot-category-field/dot-category-field.component.ts @@ -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); diff --git a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-tag-field/components/tag-field/tag-field.component.html b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-tag-field/components/tag-field/tag-field.component.html index dc3802d8c019..a0ef1f3c09f2 100644 --- a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-tag-field/components/tag-field/tag-field.component.html +++ b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-tag-field/components/tag-field/tag-field.component.html @@ -24,6 +24,8 @@ (completeMethod)="onSearch($event)" (keydown.enter)="onEnterKey($event)"> - + + + diff --git a/core-web/libs/ui/src/lib/theme/theme.config.ts b/core-web/libs/ui/src/lib/theme/theme.config.ts index e6ac594b4d26..ad49969e45af 100644 --- a/core-web/libs/ui/src/lib/theme/theme.config.ts +++ b/core-web/libs/ui/src/lib/theme/theme.config.ts @@ -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: { diff --git a/dotCMS/src/main/resources/dotmarketing-config.properties b/dotCMS/src/main/resources/dotmarketing-config.properties index 8a31f9db6689..513c52d2a650 100644 --- a/dotCMS/src/main/resources/dotmarketing-config.properties +++ b/dotCMS/src/main/resources/dotmarketing-config.properties @@ -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 diff --git a/dotCMS/src/main/webapp/WEB-INF/messages/Language.properties b/dotCMS/src/main/webapp/WEB-INF/messages/Language.properties index 683c04dc8728..a581a38eb27a 100644 --- a/dotCMS/src/main/webapp/WEB-INF/messages/Language.properties +++ b/dotCMS/src/main/webapp/WEB-INF/messages/Language.properties @@ -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 From fc079ed35dfa5c3d12bb18d9ca95ade0146aadd5 Mon Sep 17 00:00:00 2001 From: Adrian Molina Date: Mon, 6 Jul 2026 14:31:02 -0400 Subject: [PATCH 2/3] test(edit-content): cover category field clear-all, button severities & chip alignment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds unit coverage for the behavior added in the previous commit: - Field-level "Clear all" button renders only with selections, clears the store and emits an empty value, invokes clearAllSelected() on click. - Select button no longer carries secondary/text classes (primary). - Dialog's Cancel/Apply render as tertiary (text+secondary) / primary respectively, asserted via the Button component instance rather than CSS class strings. - Chips container uses justify-start instead of justify-center. Skips unit-testing the chip remove-icon `order: -1` CSS rule itself, since it's a global theme.config.ts rule jsdom doesn't apply — not meaningfully testable without a visual/e2e check. Co-Authored-By: Claude Sonnet 5 --- ...dot-category-field-chips.component.spec.ts | 7 ++ ...ot-category-field-dialog.component.spec.ts | 18 ++++ .../dot-category-field.component.spec.ts | 89 ++++++++++++++++++- 3 files changed, 113 insertions(+), 1 deletion(-) diff --git a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/components/dot-category-field-chips/dot-category-field-chips.component.spec.ts b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/components/dot-category-field-chips/dot-category-field-chips.component.spec.ts index 6220e95e4a6b..6005717052fb 100644 --- a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/components/dot-category-field-chips/dot-category-field-chips.component.spec.ts +++ b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/components/dot-category-field-chips/dot-category-field-chips.component.spec.ts @@ -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); diff --git a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/components/dot-category-field-dialog/dot-category-field-dialog.component.spec.ts b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/components/dot-category-field-dialog/dot-category-field-dialog.component.spec.ts index dc690541d569..27ca6b773f20 100644 --- a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/components/dot-category-field-dialog/dot-category-field-dialog.component.spec.ts +++ b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/components/dot-category-field-dialog/dot-category-field-dialog.component.spec.ts @@ -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'; @@ -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(); }); diff --git a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/components/dot-category-field/dot-category-field.component.spec.ts b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/components/dot-category-field/dot-category-field.component.spec.ts index 834c27d5e9fd..4760207a4447 100644 --- a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/components/dot-category-field/dot-category-field.component.spec.ts +++ b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/components/dot-category-field/dot-category-field.component.spec.ts @@ -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'; @@ -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(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(); @@ -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( + `
+ + `, + { + 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', () => { From 888a1169e26a7e64dea355a9965c388bfe3e8daa Mon Sep 17 00:00:00 2001 From: Adrian Molina Date: Mon, 6 Jul 2026 14:36:01 -0400 Subject: [PATCH 3/3] refactor(edit-content): adjust chip alignment in category field component Rearranges the class order in the category field chips component's HTML to improve alignment. The `justify-start` class is now positioned after `flex-wrap` for better readability and consistency with other components. This change enhances the layout without altering functionality. --- .../dot-category-field-chips.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/components/dot-category-field-chips/dot-category-field-chips.component.html b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/components/dot-category-field-chips/dot-category-field-chips.component.html index 893f0a9d18e3..bda6b23730b1 100644 --- a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/components/dot-category-field-chips/dot-category-field-chips.component.html +++ b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/components/dot-category-field-chips/dot-category-field-chips.component.html @@ -1,4 +1,4 @@ -
+
@for (category of $categoriesToShow(); track category.key) {