Skip to content

Commit 41aab00

Browse files
authored
feat(Content Palette): Disable Non-Allowed Favorite Content Types (#34235)
This PR fixes: #34228 ### Video https://github.com/user-attachments/assets/dbe194bd-9332-458d-b40b-5f0ca45dddde
1 parent 3294194 commit 41aab00

File tree

14 files changed

+431
-35
lines changed

14 files changed

+431
-35
lines changed

core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/dot-uve-palette/components/dot-uve-palette-contenttype/dot-uve-palette-contenttype.component.html

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,10 @@
77
</div>
88

99
<!-- Content type icon and name -->
10-
<div class="content">
10+
<div
11+
class="content"
12+
[pTooltip]="'uve.palette.item.disabled.tooltip' | dm"
13+
[tooltipDisabled]="!contentType.disabled">
1114
<div class="icon">
1215
@if (contentType.icon) {
1316
<i class="material-icons material-icons-outlined">
@@ -21,6 +24,6 @@
2124
</div>
2225

2326
<!-- Chevron -->
24-
<div class="chevron" (click)="onSelectContentType.emit(contentType.variable)">
27+
<div class="chevron" (click)="onChevronClick(contentType)">
2528
<i class="pi pi-chevron-right"></i>
2629
</div>

core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/dot-uve-palette/components/dot-uve-palette-contenttype/dot-uve-palette-contenttype.component.scss

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,27 @@
2222
}
2323
}
2424

25+
&.disabled {
26+
cursor: not-allowed;
27+
opacity: 0.6;
28+
29+
&:hover {
30+
border-color: $color-palette-border-light;
31+
background: $white;
32+
box-shadow: none;
33+
34+
.drag-handle,
35+
.chevron {
36+
opacity: 0;
37+
}
38+
}
39+
40+
.drag-handle,
41+
.chevron {
42+
cursor: not-allowed;
43+
}
44+
}
45+
2546
// Child elements
2647
.content {
2748
display: flex;

core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/dot-uve-palette/components/dot-uve-palette-contenttype/dot-uve-palette-contenttype.component.spec.ts

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,17 @@
11
import { createHostFactory, SpectatorHost } from '@ngneat/spectator/jest';
22

33
import { Component } from '@angular/core';
4+
import { By } from '@angular/platform-browser';
45

6+
import { Tooltip } from 'primeng/tooltip';
7+
8+
import { DotMessageService } from '@dotcms/data-access';
59
import { DotCMSContentType } from '@dotcms/dotcms-models';
610

711
import { DotUVEPaletteContenttypeComponent } from './dot-uve-palette-contenttype.component';
812

13+
import { DotCMSPaletteContentType } from '../../models';
14+
915
@Component({
1016
selector: 'dot-test-host',
1117
standalone: false,
@@ -49,7 +55,16 @@ describe('DotUVEPaletteContenttypeComponent', () => {
4955
const createHost = createHostFactory({
5056
component: DotUVEPaletteContenttypeComponent,
5157
host: TestHostComponent,
52-
imports: [DotUVEPaletteContenttypeComponent]
58+
imports: [DotUVEPaletteContenttypeComponent],
59+
providers: [
60+
{
61+
provide: DotMessageService,
62+
useValue: {
63+
// Keep it deterministic for tests: return the key as-is
64+
get: jest.fn((key: string) => key)
65+
}
66+
}
67+
]
5368
});
5469

5570
beforeEach(() => {
@@ -214,6 +229,46 @@ describe('DotUVEPaletteContenttypeComponent', () => {
214229
});
215230
});
216231

232+
describe('Tooltip behavior', () => {
233+
it('should enable tooltip when contentType is disabled', () => {
234+
const disabledContentType: DotCMSPaletteContentType = {
235+
...spectator.hostComponent.contentType,
236+
disabled: true
237+
};
238+
239+
spectator.setHostInput({
240+
contentType: disabledContentType as unknown as DotCMSContentType
241+
});
242+
spectator.detectChanges();
243+
244+
const contentDebugEl = spectator.fixture.debugElement.query(By.css('.content'));
245+
const tooltip = contentDebugEl.injector.get(Tooltip);
246+
247+
expect(tooltip).toBeTruthy();
248+
expect(tooltip.disabled).toBe(false);
249+
expect(tooltip.content).toBe('uve.palette.item.disabled.tooltip');
250+
});
251+
252+
it('should disable tooltip when contentType is not disabled', () => {
253+
const enabledContentType: DotCMSPaletteContentType = {
254+
...spectator.hostComponent.contentType,
255+
disabled: false
256+
};
257+
258+
spectator.setHostInput({
259+
contentType: enabledContentType as unknown as DotCMSContentType
260+
});
261+
spectator.detectChanges();
262+
263+
const contentDebugEl = spectator.fixture.debugElement.query(By.css('.content'));
264+
const tooltip = contentDebugEl.injector.get(Tooltip);
265+
266+
expect(tooltip).toBeTruthy();
267+
expect(tooltip.disabled).toBe(true);
268+
expect(tooltip.content).toBe('uve.palette.item.disabled.tooltip');
269+
});
270+
});
271+
217272
describe('Component Structure', () => {
218273
it('should have correct CSS classes structure', () => {
219274
expect(spectator.query('.drag-handle')).toBeTruthy();

core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/dot-uve-palette/components/dot-uve-palette-contenttype/dot-uve-palette-contenttype.component.ts

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,29 +7,36 @@ import {
77
output
88
} from '@angular/core';
99

10-
import { DotCMSContentType } from '@dotcms/dotcms-models';
10+
import { TooltipModule } from 'primeng/tooltip';
11+
12+
import { DotMessagePipe } from '@dotcms/ui';
13+
14+
import { DotCMSPaletteContentType } from '../../models';
1115

1216
@Component({
1317
selector: 'dot-uve-palette-contenttype',
14-
imports: [],
18+
imports: [TooltipModule, DotMessagePipe],
1519
templateUrl: './dot-uve-palette-contenttype.component.html',
1620
styleUrl: './dot-uve-palette-contenttype.component.scss',
1721
changeDetection: ChangeDetectionStrategy.OnPush,
1822
host: {
1923
'[attr.data-type]': '"content-type"',
20-
'[attr.draggable]': 'true',
24+
'[attr.draggable]': '$draggable()',
2125
'[class.list-view]': '$isListView()',
26+
'[class.disabled]': '$isDisabled()',
2227
'[attr.data-item]': '$dataItem()'
2328
}
2429
})
2530
export class DotUVEPaletteContenttypeComponent {
2631
$view = input<'grid' | 'list'>('grid', { alias: 'view' });
27-
$contentType = input.required<DotCMSContentType>({ alias: 'contentType' });
32+
$contentType = input.required<DotCMSPaletteContentType>({ alias: 'contentType' });
2833

2934
readonly onSelectContentType = output<string>();
3035
readonly contextMenu = output<MouseEvent>();
3136

3237
readonly $isListView = computed(() => this.$view() === 'list');
38+
readonly $isDisabled = computed(() => !!this.$contentType().disabled);
39+
readonly $draggable = computed(() => !this.$isDisabled());
3340
readonly $dataItem = computed(() => {
3441
const contentType = this.$contentType();
3542

@@ -43,6 +50,13 @@ export class DotUVEPaletteContenttypeComponent {
4350
});
4451
});
4552

53+
protected onChevronClick(contentType: DotCMSPaletteContentType) {
54+
if (contentType.disabled) {
55+
return;
56+
}
57+
this.onSelectContentType.emit(contentType.variable);
58+
}
59+
4660
@HostListener('contextmenu', ['$event'])
4761
protected onContextMenu(event: MouseEvent) {
4862
event.preventDefault();

core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/dot-uve-palette/components/dot-uve-palette-list/dot-uve-palette-list.component.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,13 +35,14 @@ import {
3535
DotFavoriteContentTypeService,
3636
DotMessageService
3737
} from '@dotcms/data-access';
38-
import { DEFAULT_VARIANT_ID, DotCMSContentType } from '@dotcms/dotcms-models';
38+
import { DEFAULT_VARIANT_ID } from '@dotcms/dotcms-models';
3939
import { GlobalStore } from '@dotcms/store';
4040
import { DotMessagePipe } from '@dotcms/ui';
4141

4242
import { DotPaletteListStore } from './store/store';
4343

4444
import {
45+
DotCMSPaletteContentType,
4546
DotPaletteListStatus,
4647
DotPaletteSearchParams,
4748
DotPaletteSortOption,
@@ -300,7 +301,7 @@ export class DotUvePaletteListComponent implements OnInit {
300301
this.#resetSearch();
301302
}
302303

303-
protected onContextMenu(contentType: DotCMSContentType) {
304+
protected onContextMenu(contentType: DotCMSPaletteContentType) {
304305
const isFavorite = this.#dotFavoriteContentTypeService.isFavorite(contentType.id);
305306
const label = isFavorite
306307
? 'uve.palette.menu.favorite.option.remove'
@@ -331,7 +332,7 @@ export class DotUvePaletteListComponent implements OnInit {
331332
* Remove a content type from favorites.
332333
* @param contentType - The content type to remove.
333334
*/
334-
#removeFavorite(contentType: DotCMSContentType) {
335+
#removeFavorite(contentType: DotCMSPaletteContentType) {
335336
this.#paletteListStore.removeFavorite(contentType.id);
336337
this.#messageService.add({
337338
severity: 'success',
@@ -345,7 +346,7 @@ export class DotUvePaletteListComponent implements OnInit {
345346
* Add a content type to favorites.
346347
* @param contentType - The content type to add.
347348
*/
348-
#addFavorite(contentType: DotCMSContentType) {
349+
#addFavorite(contentType: DotCMSPaletteContentType) {
349350
this.#paletteListStore.addFavorite(contentType);
350351
this.#messageService.add({
351352
severity: 'success',

core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/dot-uve-palette/components/dot-uve-palette-list/store/store.spec.ts

Lines changed: 49 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,16 @@ import { describe, expect, it, beforeEach, afterEach, jest } from '@jest/globals
22
import { createServiceFactory, SpectatorService } from '@ngneat/spectator/jest';
33
import { of, throwError } from 'rxjs';
44

5+
jest.mock('../../../utils', () => {
6+
// jest.requireActual is typed as unknown, cast so we can spread and reference exports
7+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
8+
const actual = jest.requireActual('../../../utils') as any;
9+
return {
10+
...actual,
11+
buildPaletteFavorite: jest.fn(actual.buildPaletteFavorite)
12+
};
13+
});
14+
515
import {
616
DotESContentService,
717
DotFavoriteContentTypeService,
@@ -12,12 +22,14 @@ import { DEFAULT_VARIANT_ID, DotCMSContentlet, DotCMSContentType } from '@dotcms
1222

1323
import { DotPaletteListStore } from './store';
1424

25+
import { UVEStore } from '../../../../../../store/dot-uve.store';
1526
import {
27+
DotCMSPaletteContentType,
1628
DotPaletteListStatus,
1729
DotUVEPaletteListTypes,
1830
DotUVEPaletteListView
1931
} from '../../../models';
20-
import { EMPTY_PAGINATION } from '../../../utils';
32+
import { buildPaletteFavorite, EMPTY_PAGINATION } from '../../../utils';
2133

2234
// ===== Mock Data =====
2335

@@ -65,6 +77,7 @@ describe('DotPaletteListStore', () => {
6577
let dotESContentService: jest.Mocked<DotESContentService>;
6678
let dotFavoriteContentTypeService: jest.Mocked<DotFavoriteContentTypeService>;
6779
let dotLocalstorageService: jest.Mocked<DotLocalstorageService>;
80+
let uveStore: { $allowedContentTypes: jest.Mock };
6881

6982
// ===== Test Helper Functions =====
7083

@@ -138,6 +151,13 @@ describe('DotPaletteListStore', () => {
138151
service: DotPaletteListStore,
139152
providers: [
140153
DotPaletteListStore,
154+
{
155+
provide: UVEStore,
156+
useValue: {
157+
// Default to empty map so favorites are marked as disabled unless tests override.
158+
$allowedContentTypes: jest.fn().mockReturnValue({})
159+
}
160+
},
141161
{
142162
provide: DotLocalstorageService,
143163
useValue: {
@@ -157,6 +177,7 @@ describe('DotPaletteListStore', () => {
157177
dotESContentService = spectator.inject(DotESContentService);
158178
dotFavoriteContentTypeService = spectator.inject(DotFavoriteContentTypeService);
159179
dotLocalstorageService = spectator.inject(DotLocalstorageService);
180+
uveStore = spectator.inject(UVEStore) as unknown as { $allowedContentTypes: jest.Mock };
160181

161182
// Setup default mock return values
162183
pageContentTypeService.get.mockReturnValue(
@@ -451,10 +472,10 @@ describe('DotPaletteListStore', () => {
451472
expect(dotFavoriteContentTypeService.add).toHaveBeenCalledWith(mockContentTypes[0]);
452473
// Favorites are sorted alphabetically by name: Blog, Events, News
453474
const expectedOrder = [
454-
mockContentTypes[0], // Blog
455-
extraFavorite, // Events
456-
mockContentTypes[1] // News
457-
];
475+
{ ...mockContentTypes[0], disabled: true }, // Blog
476+
{ ...extraFavorite, disabled: true }, // Events
477+
{ ...mockContentTypes[1], disabled: true } // News
478+
] as DotCMSPaletteContentType[];
458479
expect(store.contenttypes()).toEqual(expectedOrder);
459480
});
460481

@@ -477,7 +498,24 @@ describe('DotPaletteListStore', () => {
477498
expect(dotFavoriteContentTypeService.remove).toHaveBeenCalledWith(
478499
mockContentTypes[0].id
479500
);
480-
expect(store.contenttypes()).toEqual(remainingFavorites);
501+
expect(store.contenttypes()).toEqual([
502+
{ ...remainingFavorites[0], disabled: true }
503+
] as DotCMSPaletteContentType[]);
504+
});
505+
506+
it('should pass allowedContentTypes to buildPaletteFavorite when refreshing favorites state', () => {
507+
store.getContentTypes({ listType: DotUVEPaletteListTypes.FAVORITES });
508+
509+
(buildPaletteFavorite as unknown as jest.Mock).mockClear();
510+
uveStore.$allowedContentTypes.mockReturnValueOnce({ blog: true, banner: true });
511+
512+
store.setContentTypesFromFavorite(mockContentTypes);
513+
514+
expect(buildPaletteFavorite).toHaveBeenCalledWith(
515+
expect.objectContaining({
516+
allowedContentTypes: { blog: true, banner: true }
517+
})
518+
);
481519
});
482520

483521
it('should not refresh store when removing favorites outside favorites view', () => {
@@ -522,7 +560,11 @@ describe('DotPaletteListStore', () => {
522560
store.getContentTypes({ listType: DotUVEPaletteListTypes.FAVORITES });
523561

524562
expect(dotFavoriteContentTypeService.getAll).toHaveBeenCalled();
525-
expect(store.contenttypes()).toEqual(mockContentTypes);
563+
expect(store.contenttypes()).toEqual(
564+
mockContentTypes.map(
565+
(ct) => ({ ...ct, disabled: true }) as DotCMSPaletteContentType
566+
)
567+
);
526568
});
527569

528570
it('should update search params with provided values', () => {

core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/dot-uve-palette/components/dot-uve-palette-list/store/store.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
} from '@dotcms/data-access';
2121
import { DEFAULT_VARIANT_ID, DotCMSContentType } from '@dotcms/dotcms-models';
2222

23+
import { UVEStore } from '../../../../../../store/dot-uve.store';
2324
import {
2425
BASETYPES_FOR_CONTENT,
2526
BASETYPES_FOR_WIDGET,
@@ -98,6 +99,7 @@ export const DotPaletteListStore = signalStore(
9899
const pageContentTypeService = inject(DotPageContentTypeService);
99100
const dotESContentService = inject(DotESContentService);
100101
const dotFavoriteContentTypeService = inject(DotFavoriteContentTypeService);
102+
const uveStore = inject(UVEStore);
101103

102104
const getData = () => {
103105
const { listType, ...params } = store.searchParams();
@@ -121,7 +123,8 @@ export const DotPaletteListStore = signalStore(
121123
buildPaletteFavorite({
122124
contentTypes,
123125
filter: params.filter || '',
124-
page: params.page
126+
page: params.page,
127+
allowedContentTypes: uveStore.$allowedContentTypes()
125128
})
126129
)
127130
);
@@ -188,11 +191,13 @@ export const DotPaletteListStore = signalStore(
188191
withMethods((store) => {
189192
const params = store.searchParams;
190193
const dotFavoriteContentTypeService = inject(DotFavoriteContentTypeService);
194+
const uveStore = inject(UVEStore);
191195
const updateFavoriteState = (contentTypes: DotCMSContentType[]) => {
192196
const response = buildPaletteFavorite({
193197
contentTypes,
194198
filter: params.filter(),
195-
page: params.page() || 1
199+
page: params.page() || 1,
200+
allowedContentTypes: uveStore?.$allowedContentTypes()
196201
});
197202
patchState(store, response);
198203
};

0 commit comments

Comments
 (0)