Skip to content

Commit f071314

Browse files
KevinDavilaDotCMSrjvelazcozJaaalkevindaviladevnicobytes
authored
chore(content-drive): Add Base Type filter (dotCMS#33029)
This PR introduces the new filter by Base Type. Also have some code from Content Type filter. And fix some UI issues from PrimeNG MultiSelects around the all project Base Type filter: https://github.com/user-attachments/assets/51650b02-c6ad-41b2-834e-c967c8553bd9 Multi Select before: https://github.com/user-attachments/assets/e4d82812-d2f9-4660-bfd8-79901c480818 Multi Select now: https://github.com/user-attachments/assets/047fd1b6-695d-4e6f-b454-67d386665b31 This PR fixes: dotCMS#32600 --------- Co-authored-by: Rafael Velazco <[email protected]> Co-authored-by: Jalinson Diaz <[email protected]> Co-authored-by: Kevin <[email protected]> Co-authored-by: Nicolas Molina Monroy <[email protected]>
1 parent d55585c commit f071314

29 files changed

+886
-130
lines changed

core-web/libs/dotcms-scss/angular/dotcms-theme/components/form/_multiselect.scss

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,11 @@
44

55
.p-multiselect {
66
@extend #form-field-base;
7-
min-width: 13rem;
7+
min-width: 14rem;
88
padding-right: 0;
99
min-height: $field-height-md;
10-
height: auto;
10+
height: 2.5em;
11+
width: 100%;
1112

1213
&:not(.p-multiselect-open):hover {
1314
@extend #form-field-hover;
@@ -31,7 +32,7 @@
3132
padding: $spacing-1 0;
3233
height: auto;
3334
flex-wrap: wrap;
34-
overflow: visible;
35+
overflow: hidden;
3536
}
3637
}
3738
}
@@ -64,6 +65,10 @@
6465

6566
.p-multiselect-label {
6667
gap: $spacing-0;
68+
display: block;
69+
cursor: pointer;
70+
overflow: hidden;
71+
// max-width: 10.70em;
6772
}
6873

6974
.p-multiselect-token {
@@ -74,6 +79,7 @@
7479

7580
.p-multiselect-panel {
7681
@extend #field-panel;
82+
min-width: 20rem;
7783

7884
.p-multiselect-header {
7985
@extend #field-panel-header;
@@ -103,6 +109,10 @@
103109
.p-multiselect-item {
104110
@extend #field-panel-item;
105111

112+
&[data-p-highlight="true"] {
113+
@extend #field-panel-item-highlight;
114+
}
115+
106116
&.p-highlight {
107117
@extend #field-panel-item-highlight;
108118
}

core-web/libs/dotcms-scss/angular/dotcms-theme/components/form/common.scss

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -178,11 +178,12 @@ $select-border-size: 2px;
178178
border-radius: $border-radius-sm;
179179
color: $color-palette-primary;
180180
font-size: $font-size-sm;
181-
display: flex;
181+
display: inline-flex;
182182
align-items: center;
183183
justify-content: center;
184184
gap: $spacing-0;
185185
flex-direction: row-reverse;
186+
margin-right: $spacing-0;
186187
}
187188

188189
#field-chip-token {
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { Route } from '@angular/router';
22

3+
import { DotContentTypeService } from '@dotcms/data-access';
34
import { GlobalStore } from '@dotcms/store';
45

56
import { DotContentDriveShellComponent } from './lib/dot-content-drive-shell/dot-content-drive-shell.component';
@@ -8,6 +9,6 @@ export const DotContentDriveRoutes: Route[] = [
89
{
910
path: '',
1011
component: DotContentDriveShellComponent,
11-
providers: [GlobalStore]
12+
providers: [GlobalStore, DotContentTypeService]
1213
}
1314
];
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<p-multiSelect
2+
[(ngModel)]="$selectedBaseTypes"
3+
[options]="$state.baseTypes()"
4+
optionLabel="label"
5+
display="chip"
6+
optionValue="name"
7+
[placeholder]="'content-drive.base-type.placeholder' | dm"
8+
[showToggleAll]="true"
9+
(onChange)="onChange()" />

core-web/libs/portlets/dot-content-drive/portlet/src/lib/components/dot-content-drive-toolbar/components/dot-content-drive-base-type-selector/dot-content-drive-base-type-selector.component.scss

Whitespace-only changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import { createComponentFactory, mockProvider, Spectator, SpyObject } from '@ngneat/spectator/jest';
2+
import { of } from 'rxjs';
3+
4+
import { By } from '@angular/platform-browser';
5+
6+
import { MultiSelect, MultiSelectChangeEvent } from 'primeng/multiselect';
7+
8+
import { DotContentTypeService, DotMessageService } from '@dotcms/data-access';
9+
10+
import { DotContentDriveBaseTypeSelectorComponent } from './dot-content-drive-base-type-selector.component';
11+
12+
import { MOCK_BASE_TYPES } from '../../../../shared/mocks';
13+
import { DotContentDriveStore } from '../../../../store/dot-content-drive.store';
14+
15+
describe('DotContentDriveBaseTypeSelectorComponent', () => {
16+
let spectator: Spectator<DotContentDriveBaseTypeSelectorComponent>;
17+
let component: DotContentDriveBaseTypeSelectorComponent;
18+
let store: SpyObject<InstanceType<typeof DotContentDriveStore>>;
19+
let contentTypeService: SpyObject<DotContentTypeService>;
20+
21+
const createComponent = createComponentFactory({
22+
component: DotContentDriveBaseTypeSelectorComponent,
23+
providers: [
24+
mockProvider(DotMessageService, {
25+
get: jest.fn()
26+
}),
27+
mockProvider(DotContentDriveStore, {
28+
patchFilters: jest.fn(),
29+
removeFilter: jest.fn(),
30+
getFilterValue: jest.fn()
31+
}),
32+
mockProvider(DotContentTypeService, {
33+
getAllContentTypes: jest.fn().mockReturnValue(of(MOCK_BASE_TYPES))
34+
})
35+
],
36+
detectChanges: false
37+
});
38+
39+
beforeEach(() => {
40+
spectator = createComponent();
41+
component = spectator.component;
42+
store = spectator.inject(DotContentDriveStore, true);
43+
contentTypeService = spectator.inject(DotContentTypeService);
44+
store.getFilterValue.mockReturnValue([]);
45+
});
46+
47+
it('should fetch content types and filter out FORM type', () => {
48+
spectator.detectChanges();
49+
50+
expect(contentTypeService.getAllContentTypes).toHaveBeenCalled();
51+
expect(component.$state().baseTypes).toEqual([
52+
{ name: 'Content', label: 'Content', types: null },
53+
{ name: 'Widget', label: 'Widget', types: null },
54+
{ name: 'FileAsset', label: 'FileAsset', types: null }
55+
]);
56+
});
57+
58+
it('should set selectedBaseTypes when store has baseType filter', () => {
59+
store.getFilterValue.mockReturnValue(['1', '2']);
60+
61+
spectator.detectChanges();
62+
63+
expect(store.getFilterValue).toHaveBeenCalledWith('baseType');
64+
expect(component.$selectedBaseTypes()).toEqual(['CONTENT', 'WIDGET']);
65+
});
66+
67+
it('should patch filters with keys when selectedBaseTypes has values', () => {
68+
spectator.detectChanges();
69+
70+
const multiSelectDebugElement = spectator.fixture.debugElement.query(
71+
By.directive(MultiSelect)
72+
);
73+
74+
spectator.triggerEventHandler(multiSelectDebugElement, 'ngModelChange', [
75+
'CONTENT',
76+
'WIDGET'
77+
]);
78+
79+
expect(component.$selectedBaseTypes()).toEqual(['CONTENT', 'WIDGET']);
80+
81+
spectator.triggerEventHandler(MultiSelect, 'onChange', {} as MultiSelectChangeEvent);
82+
83+
expect(store.patchFilters).toHaveBeenCalledWith({
84+
baseType: ['1', '2']
85+
});
86+
});
87+
88+
it('should remove filter when selectedBaseTypes is empty', () => {
89+
component.$selectedBaseTypes.set([]);
90+
91+
const multiSelectDebugElement = spectator.fixture.debugElement.query(
92+
By.directive(MultiSelect)
93+
);
94+
95+
spectator.triggerEventHandler(multiSelectDebugElement, 'ngModelChange', []);
96+
97+
spectator.triggerEventHandler(MultiSelect, 'onChange', {} as MultiSelectChangeEvent);
98+
99+
expect(store.removeFilter).toHaveBeenCalledWith('baseType');
100+
});
101+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import { patchState, signalState } from '@ngrx/signals';
2+
import { of } from 'rxjs';
3+
4+
import { Component, inject, model } from '@angular/core';
5+
import { FormsModule } from '@angular/forms';
6+
7+
import { CheckboxModule } from 'primeng/checkbox';
8+
import { MultiSelectModule } from 'primeng/multiselect';
9+
10+
import { debounceTime, distinctUntilChanged, map, take } from 'rxjs/operators';
11+
12+
import { DotContentTypeService } from '@dotcms/data-access';
13+
import { DotMessagePipe } from '@dotcms/ui';
14+
15+
import { DEBOUNCE_TIME, MAP_NUMBERS_TO_BASE_TYPES } from '../../../../shared/constants';
16+
import { DotContentDriveStore } from '../../../../store/dot-content-drive.store';
17+
18+
@Component({
19+
selector: 'dot-content-drive-base-type-selector',
20+
templateUrl: './dot-content-drive-base-type-selector.component.html',
21+
styleUrl: './dot-content-drive-base-type-selector.component.scss',
22+
imports: [MultiSelectModule, FormsModule, CheckboxModule, DotMessagePipe],
23+
standalone: true
24+
})
25+
export class DotContentDriveBaseTypeSelectorComponent {
26+
$selectedBaseTypes = model<string[]>([]);
27+
28+
readonly #store = inject(DotContentDriveStore);
29+
readonly #dotContentTypeService = inject(DotContentTypeService);
30+
31+
readonly $state = signalState({
32+
baseTypes: []
33+
});
34+
35+
ngOnInit() {
36+
this.getCurrentBaseTypes();
37+
38+
this.#dotContentTypeService
39+
.getAllContentTypes()
40+
.pipe(
41+
take(1),
42+
map((response) => response.filter((item) => item.name !== 'FORM'))
43+
)
44+
.subscribe((response) => {
45+
patchState(this.$state, {
46+
baseTypes: response
47+
});
48+
});
49+
}
50+
51+
getCurrentBaseTypes() {
52+
const baseTypes = this.#store.getFilterValue('baseType') as string[];
53+
54+
if (baseTypes?.length > 0) {
55+
const values = baseTypes.map((key) => MAP_NUMBERS_TO_BASE_TYPES[key]).filter(Boolean);
56+
57+
this.$selectedBaseTypes.set(values);
58+
}
59+
}
60+
61+
onChange() {
62+
of(this.$selectedBaseTypes() ?? [])
63+
.pipe(
64+
debounceTime(DEBOUNCE_TIME), // Debounce to avoid spamming the server
65+
distinctUntilChanged()
66+
)
67+
.subscribe((value) => {
68+
if (value.length > 0) {
69+
// Get all keys from MAP_NUMBERS_TO_BASE_TYPES where the values match the array
70+
const keys = value
71+
.map(
72+
(val) =>
73+
Object.entries(MAP_NUMBERS_TO_BASE_TYPES).find(
74+
([_key, v]) => v === val
75+
)?.[0]
76+
)
77+
.filter(Boolean);
78+
79+
this.#store.patchFilters({
80+
baseType: keys
81+
});
82+
} else {
83+
this.#store.removeFilter('baseType');
84+
}
85+
});
86+
}
87+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<p-multiSelect
2+
[filter]="true"
3+
[options]="$state.contentTypes()"
4+
[(ngModel)]="$selectedContentTypes"
5+
optionLabel="name"
6+
display="chip"
7+
showClear="false"
8+
[lazy]="true"
9+
[placeholder]="'content-drive.content-type.placeholder' | dm"
10+
[loading]="$state.loading()"
11+
(onChange)="onChange()"
12+
(onClear)="onChange()"
13+
(onFilter)="onFilter($event)">
14+
<ng-template pTemplate="empty">
15+
@if ($state.loading()) {
16+
<p-skeleton
17+
height="32px"
18+
width="100%"
19+
data-testid="content-type-field-skeleton"></p-skeleton>
20+
} @else {
21+
{{ 'content-drive.content-type-field.empty-state' | dm }}
22+
}
23+
</ng-template>
24+
</p-multiSelect>

core-web/libs/portlets/dot-content-drive/portlet/src/lib/components/dot-content-drive-toolbar/components/dot-content-drive-content-type-field/dot-content-drive-content-type-field.component.scss

Whitespace-only changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
describe('DotContentDriveContentTypeFieldComponent', () => {
2+
// TODO: This will be implemented in the ContentType filter PR.
3+
describe('Component Initialization', () => {
4+
it('should create', () => {
5+
expect(true).toBeTruthy();
6+
});
7+
});
8+
});

0 commit comments

Comments
 (0)