Skip to content

Commit 96ff409

Browse files
feat(style-editor): Improved empty message (#35496)
- Added and to manage access to the style editor and permissions tabs based on feature flags and user permissions. - Introduced to resolve permissions for displaying the permissions tab. - Updated routing to include guards and resolvers for the new tabs. - Enhanced component templates to reflect the new tab structure and logic. - Added unit tests for guards and resolver to ensure correct functionality. This implementation improves user experience by conditionally displaying tabs based on user permissions and feature availability. ### Proposed Changes * change 1 * change 2 ### Checklist - [ ] Tests - [ ] Translations - [ ] Security Implications Contemplated (add notes if applicable) ### Additional Info ** any additional useful context or info ** ### Screenshots Original | Updated :-------------------------:|:-------------------------: ** original screenshot ** | ** updated screenshot ** This PR fixes: #35466
1 parent e5ec72b commit 96ff409

19 files changed

Lines changed: 523 additions & 91 deletions

core-web/apps/dotcms-ui/src/app/portlets/shared/dot-content-types-edit/components/fields/content-type-fields-tab/content-type-fields-tab.component.spec.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { dotcmsContentTypeFieldBasicMock, MockDotMessageService } from '@dotcms/
1717
import { ContentTypeFieldsTabComponent } from '.';
1818

1919
import { DOTTestBed } from '../../../../../../test/dot-test-bed';
20+
import { DotMaxlengthDirective } from '../../../../../../view/directives/dot-maxlength/dot-maxlength.directive';
2021

2122
const tabField: DotCMSContentTypeField = {
2223
...dotcmsContentTypeFieldBasicMock,
@@ -59,7 +60,7 @@ describe('ContentTypeFieldsTabComponent', () => {
5960
beforeEach(waitForAsync(() => {
6061
DOTTestBed.configureTestingModule({
6162
declarations: [ContentTypeFieldsTabComponent, DotTestHostComponent],
62-
imports: [TooltipModule, ButtonModule, DotMessagePipe],
63+
imports: [TooltipModule, ButtonModule, DotMessagePipe, DotMaxlengthDirective],
6364
providers: [
6465
DotAlertConfirmService,
6566
{

core-web/apps/dotcms-ui/src/app/portlets/shared/dot-content-types-edit/components/layout/content-types-layout.component.html

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,26 @@
11
<div class="h-full min-h-0 flex flex-col">
2-
<p-tabs [value]="0" class="flex flex-col grow min-h-0">
2+
<p-tabs
3+
[value]="$activeTab()"
4+
(valueChange)="onTabChange($event)"
5+
class="flex flex-col grow min-h-0">
36
<p-tablist
47
class="shrink-0"
58
[pt]="{ root: { class: '!bg-surface-50 border-b border-gray-200' } }">
6-
<p-tab [value]="0">
9+
<p-tab value="fields">
710
{{ 'contenttypes.tab.fields.header' | dm }}
811
</p-tab>
912
@if ($contentType() && $showStyleEditorTab()) {
10-
<p-tab [value]="1" data-testid="style-editor-tab">
13+
<p-tab value="style-editor" data-testid="style-editor-tab">
1114
{{ 'contenttypes.tab.style.editor.header' | dm }}
1215
</p-tab>
1316
}
14-
@if ($contentType() && showPermissionsTab | async) {
15-
<p-tab [value]="2">
17+
@if ($contentType() && $showPermissionsTab()) {
18+
<p-tab value="permissions">
1619
{{ 'contenttypes.tab.permissions.header' | dm }}
1720
</p-tab>
1821
}
1922
@if ($contentType()) {
20-
<p-tab [value]="$contentType() && (showPermissionsTab | async) ? 3 : 2">
23+
<p-tab value="push-history">
2124
{{ 'contenttypes.tab.publisher.push.history.header' | dm }}
2225
</p-tab>
2326
}
@@ -42,7 +45,7 @@
4245
</div>
4346
</p-tablist>
4447
<p-tabpanels class="grow basis-0 min-h-0 p-0!">
45-
<p-tabpanel [value]="0" class="h-full min-h-0">
48+
<p-tabpanel value="fields" class="h-full min-h-0">
4649
<div class="flex h-full min-h-0" id="content-type-form-layout">
4750
<div
4851
class="flex flex-col grow min-h-0 overflow-x-hidden overflow-y-auto bg-gray-200 p-6"
@@ -61,12 +64,15 @@
6164
</div>
6265
</p-tabpanel>
6366
@if ($contentType() && $showStyleEditorTab()) {
64-
<p-tabpanel [value]="1" class="h-full min-h-0" data-testid="style-editor-panel">
67+
<p-tabpanel
68+
value="style-editor"
69+
class="h-full min-h-0"
70+
data-testid="style-editor-panel">
6571
<dot-style-editor-builder [contentType]="$contentType()" />
6672
</p-tabpanel>
6773
}
68-
@if ($contentType() && showPermissionsTab | async) {
69-
<p-tabpanel [value]="2" class="h-full">
74+
@if ($contentType() && $showPermissionsTab()) {
75+
<p-tabpanel value="permissions" class="h-full">
7076
<div class="h-full">
7177
<dot-portlet-box class="h-full">
7278
<dot-iframe class="h-full" [src]="permissionURL" />
@@ -75,9 +81,7 @@
7581
</p-tabpanel>
7682
}
7783
@if ($contentType()) {
78-
<p-tabpanel
79-
[value]="$contentType() && (showPermissionsTab | async) ? 3 : 2"
80-
class="h-full">
84+
<p-tabpanel value="push-history" class="h-full">
8185
<div class="h-full">
8286
<dot-portlet-box class="h-full">
8387
<dot-iframe class="h-full" [src]="pushHistoryURL" />

core-web/apps/dotcms-ui/src/app/portlets/shared/dot-content-types-edit/components/layout/content-types-layout.component.spec.ts

Lines changed: 28 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,14 @@ Object.defineProperty(window, 'matchMedia', {
1515
}))
1616
});
1717

18-
import { BehaviorSubject, of } from 'rxjs';
18+
import { EMPTY, of } from 'rxjs';
1919

2020
import { provideHttpClient } from '@angular/common/http';
2121
import { provideHttpClientTesting } from '@angular/common/http/testing';
2222
import { Component, DebugElement, EventEmitter, Input, Output } from '@angular/core';
23-
import { ComponentFixture, fakeAsync, TestBed } from '@angular/core/testing';
23+
import { ComponentFixture, TestBed } from '@angular/core/testing';
2424
import { By } from '@angular/platform-browser';
25+
import { ActivatedRoute, Router } from '@angular/router';
2526
import { RouterTestingModule } from '@angular/router/testing';
2627

2728
import { MenuItem } from 'primeng/api';
@@ -35,12 +36,11 @@ import {
3536
DotHttpErrorManagerService,
3637
DotIframeService,
3738
DotMessageService,
38-
DotPropertiesService,
3939
DotRouterService,
4040
DotUiColorsService
4141
} from '@dotcms/data-access';
4242
import { DotcmsEventsService, LoggerService, LoginService } from '@dotcms/dotcms-js';
43-
import { DotCMSContentType } from '@dotcms/dotcms-models';
43+
import { DotCMSContentType, FeaturedFlags } from '@dotcms/dotcms-models';
4444
import {
4545
DotApiLinkComponent,
4646
DotCopyButtonComponent,
@@ -136,12 +136,8 @@ const fakeContentType: DotCMSContentType = {
136136
describe('ContentTypesLayoutComponent', () => {
137137
let fixture: ComponentFixture<TestHostComponent>;
138138
let de: DebugElement;
139-
let featureFlagSubject: BehaviorSubject<boolean>;
140139

141140
beforeEach(() => {
142-
// Default: feature enabled. Tests that need it disabled can emit false.
143-
featureFlagSubject = new BehaviorSubject<boolean>(true);
144-
145141
const messageServiceMock = new MockDotMessageService({
146142
'contenttypes.sidebar.components.title': 'Field Title',
147143
'contenttypes.tab.fields.header': 'Fields Header Tab',
@@ -234,9 +230,18 @@ describe('ContentTypesLayoutComponent', () => {
234230
useValue: { confirm: jest.fn(), alert: jest.fn() }
235231
},
236232
{
237-
provide: DotPropertiesService,
233+
provide: ActivatedRoute,
238234
useValue: {
239-
getFeatureFlag: jest.fn().mockReturnValue(featureFlagSubject.asObservable())
235+
snapshot: {
236+
data: {
237+
featuredFlags: {
238+
[FeaturedFlags.FEATURE_FLAG_UVE_STYLE_EDITOR]: true
239+
},
240+
tabPermissions: { showPermissionsTab: true }
241+
}
242+
},
243+
firstChild: null,
244+
events: EMPTY
240245
}
241246
}
242247
]
@@ -274,7 +279,7 @@ describe('ContentTypesLayoutComponent', () => {
274279
});
275280

276281
it('should not have a Permissions tab', () => {
277-
const pTabPanel = de.query(By.css('p-tabpanel[value="2"]'));
282+
const pTabPanel = de.query(By.css('p-tabpanel[value="permissions"]'));
278283
expect(pTabPanel).toBeFalsy();
279284
});
280285

@@ -287,21 +292,21 @@ describe('ContentTypesLayoutComponent', () => {
287292
expect(fieldDragDropService.setBagOptions).toHaveBeenCalledTimes(1);
288293
});
289294

290-
it('should have dot-portlet-box in the Permissions tab after it has been clicked', fakeAsync(() => {
295+
it('should navigate to the route and immediately update $activeTab when clicking a tab', () => {
291296
fixture.componentRef.setInput('contentType', fakeContentType);
292297
fixture.detectChanges();
293298

294-
const tabs = de.queryAll(By.css('p-tab'));
295-
// tabs[0]=Fields, [1]=StyleEditor, [2]=Permissions
296-
tabs[2].nativeElement.click();
297-
fixture.detectChanges();
299+
const router = fixture.debugElement.injector.get(Router);
300+
jest.spyOn(router, 'navigate');
298301

299-
fixture.whenStable().then(() => {
300-
const panels = de.queryAll(By.css('p-tabpanel'));
301-
const contentTypePushHistoryPortletBox = panels[2].query(By.css('dot-portlet-box'));
302-
expect(contentTypePushHistoryPortletBox).not.toBeNull();
303-
});
304-
}));
302+
de.componentInstance.onTabChange('permissions');
303+
304+
expect(de.componentInstance.$activeTab()).toBe('permissions');
305+
expect(router.navigate).toHaveBeenCalledWith(
306+
['permissions'],
307+
expect.objectContaining({ relativeTo: expect.anything() })
308+
);
309+
});
305310

306311
describe('Edit toolBar', () => {
307312
beforeEach(() => {
@@ -335,13 +340,9 @@ describe('ContentTypesLayoutComponent', () => {
335340

336341
describe('Tabs', () => {
337342
let iframe: DebugElement;
338-
let dotCurrentUserService: DotCurrentUserService;
339343

340344
beforeEach(() => {
341345
fixture.componentRef.setInput('contentType', fakeContentType);
342-
dotCurrentUserService = fixture.debugElement.injector.get(DotCurrentUserService);
343-
jest.spyOn(dotCurrentUserService, 'hasAccessToPortlet').mockReturnValue(of(true));
344-
345346
fixture.detectChanges();
346347
});
347348

@@ -498,7 +499,7 @@ describe('ContentTypesLayoutComponent', () => {
498499
});
499500

500501
it('should hide the style editor tab when feature flag is disabled', () => {
501-
featureFlagSubject.next(false);
502+
de.componentInstance.$showStyleEditorTab.set(false);
502503
fixture.detectChanges();
503504

504505
const styleEditorPanel = de.query(By.css('[data-testid="style-editor-panel"]'));

core-web/apps/dotcms-ui/src/app/portlets/shared/dot-content-types-edit/components/layout/content-types-layout.component.ts

Lines changed: 29 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,3 @@
1-
import { Observable } from 'rxjs';
2-
3-
import { AsyncPipe } from '@angular/common';
41
import {
52
Component,
63
ElementRef,
@@ -10,9 +7,11 @@ import {
107
inject,
118
input,
129
output,
10+
signal,
1311
viewChild
1412
} from '@angular/core';
15-
import { toSignal } from '@angular/core/rxjs-interop';
13+
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
14+
import { ActivatedRoute, NavigationEnd, Router } from '@angular/router';
1615

1716
import { MenuItem } from 'primeng/api';
1817
import { ButtonModule } from 'primeng/button';
@@ -21,12 +20,9 @@ import { MenuModule } from 'primeng/menu';
2120
import { SplitButtonModule } from 'primeng/splitbutton';
2221
import { TabsModule } from 'primeng/tabs';
2322

24-
import {
25-
DotCurrentUserService,
26-
DotEventsService,
27-
DotMessageService,
28-
DotPropertiesService
29-
} from '@dotcms/data-access';
23+
import { filter, map } from 'rxjs/operators';
24+
25+
import { DotEventsService, DotMessageService } from '@dotcms/data-access';
3026
import { DotCMSContentType, FeaturedFlags } from '@dotcms/dotcms-models';
3127
import { DotClipboardUtil, DotMessagePipe } from '@dotcms/ui';
3228

@@ -43,7 +39,6 @@ import { DotStyleEditorBuilderComponent } from '../style-editor/dot-style-editor
4339
templateUrl: 'content-types-layout.component.html',
4440
providers: [DotClipboardUtil],
4541
imports: [
46-
AsyncPipe,
4742
TabsModule,
4843
SplitButtonModule,
4944
ButtonModule,
@@ -61,9 +56,9 @@ export class ContentTypesLayoutComponent implements OnInit {
6156
#dotMessageService = inject(DotMessageService);
6257
#fieldDragDropService = inject(FieldDragDropService);
6358
#dotEventsService = inject(DotEventsService);
64-
#dotCurrentUserService = inject(DotCurrentUserService);
65-
#dotPropertiesService = inject(DotPropertiesService);
6659
#dotClipboardUtil = inject(DotClipboardUtil);
60+
#router = inject(Router);
61+
#route = inject(ActivatedRoute);
6762

6863
$contentType = input.required<DotCMSContentType>({ alias: 'contentType' });
6964
openEditDialog = output<unknown>();
@@ -74,11 +69,14 @@ export class ContentTypesLayoutComponent implements OnInit {
7469
permissionURL: string;
7570
pushHistoryURL: string;
7671
contentTypeNameInputSize: number;
77-
showPermissionsTab: Observable<boolean>;
78-
readonly $showStyleEditorTab = toSignal(
79-
this.#dotPropertiesService.getFeatureFlag(FeaturedFlags.FEATURE_FLAG_UVE_STYLE_EDITOR),
80-
{ initialValue: false }
72+
readonly $showStyleEditorTab = signal<boolean>(
73+
this.#route.snapshot.data['featuredFlags']?.[FeaturedFlags.FEATURE_FLAG_UVE_STYLE_EDITOR] ??
74+
false
75+
);
76+
readonly $showPermissionsTab = signal<boolean>(
77+
this.#route.snapshot.data['tabPermissions']?.showPermissionsTab ?? false
8178
);
79+
readonly $activeTab = signal(this.#route.firstChild?.snapshot.url[0]?.path ?? 'fields');
8280
addToMenuContentType = false;
8381

8482
actions: MenuItem[];
@@ -115,7 +113,6 @@ export class ContentTypesLayoutComponent implements OnInit {
115113
});
116114

117115
ngOnInit(): void {
118-
this.showPermissionsTab = this.#dotCurrentUserService.hasAccessToPortlet('permissions');
119116
this.#fieldDragDropService.setBagOptions();
120117
this.loadActions();
121118
}
@@ -128,6 +125,15 @@ export class ContentTypesLayoutComponent implements OnInit {
128125
this.pushHistoryURL = `/html/content_types/push_history.jsp?contentTypeId=${ct.id}&popup=true`;
129126
}
130127
});
128+
129+
// Keep $activeTab in sync with browser back/forward navigation.
130+
this.#router.events
131+
.pipe(
132+
filter((e) => e instanceof NavigationEnd),
133+
map(() => this.#route.firstChild?.snapshot.url[0]?.path ?? 'fields'),
134+
takeUntilDestroyed()
135+
)
136+
.subscribe((tab) => this.$activeTab.set(tab));
131137
}
132138

133139
/**
@@ -178,6 +184,11 @@ export class ContentTypesLayoutComponent implements OnInit {
178184
}
179185
}
180186

187+
onTabChange(tab: unknown): void {
188+
this.$activeTab.set(tab as string);
189+
this.#router.navigate([tab as string], { relativeTo: this.#route });
190+
}
191+
181192
private loadActions(): void {
182193
this.actions = [
183194
{

0 commit comments

Comments
 (0)