From 9f45d621428611eb1022c7dc2beade48c930515c Mon Sep 17 00:00:00 2001 From: Freddy Montes Date: Tue, 19 May 2026 16:02:14 -0600 Subject: [PATCH 1/3] feat(report-issue): gate toolbar menu item behind FEATURE_FLAG_REPORT_ISSUE_ENABLED (#35758) Adds an opt-in feature flag that hides the Report Issue user-menu entry by default. The check lives in the toolbar component (not the store) so it can be cleanly removed once the feature ships. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../dot-toolbar-user.component.spec.ts | 27 +++++++++++++++++ .../dot-toolbar-user.component.ts | 29 ++++++++++++++++--- .../dotcms-models/src/lib/shared-models.ts | 3 +- .../dotcms/featureflag/FeatureFlagName.java | 2 ++ .../api/v1/system/ConfigurationResource.java | 3 +- .../resources/dotmarketing-config.properties | 3 ++ 6 files changed, 61 insertions(+), 6 deletions(-) diff --git a/core-web/apps/dotcms-ui/src/app/view/components/dot-toolbar/components/dot-toolbar-user/dot-toolbar-user.component.spec.ts b/core-web/apps/dotcms-ui/src/app/view/components/dot-toolbar/components/dot-toolbar-user/dot-toolbar-user.component.spec.ts index 79a49dbdd88..d89406e2285 100644 --- a/core-web/apps/dotcms-ui/src/app/view/components/dot-toolbar/components/dot-toolbar-user/dot-toolbar-user.component.spec.ts +++ b/core-web/apps/dotcms-ui/src/app/view/components/dot-toolbar/components/dot-toolbar-user/dot-toolbar-user.component.spec.ts @@ -17,6 +17,7 @@ import { DotGlobalMessageService, DotHttpErrorManagerService, DotMessageService, + DotPropertiesService, DotUiColorsService } from '@dotcms/data-access'; import { LoggerService, LoginService } from '@dotcms/dotcms-js'; @@ -66,6 +67,12 @@ describe('DotToolbarUserComponent', () => { provide: DotReportIssueService, useValue: { reportIssue: jest.fn(() => of('')) } }, + { + provide: DotPropertiesService, + useValue: { + getFeatureFlagWithDefault: jest.fn(() => of(true)) + } + }, { provide: DotUiColorsService, useClass: MockDotUiColorsService }, DotToolbarUserStore ], @@ -257,4 +264,24 @@ describe('DotToolbarUserComponent', () => { expect(fixture.componentInstance.$showReportIssue()).toBe(true); })); + + it('should hide the report issue menu item when the feature flag is disabled', fakeAsync(() => { + const dotPropertiesService = TestBed.inject(DotPropertiesService); + (dotPropertiesService.getFeatureFlagWithDefault as jest.Mock).mockReturnValue(of(false)); + + // Rebuild the component so the new mock value is what vm$ sees. + fixture = TestBed.createComponent(DotToolbarUserComponent); + fixture.detectChanges(); + + let reportIssueItem: { id?: string } | undefined; + fixture.componentInstance.vm$.pipe(take(1)).subscribe((vm) => { + reportIssueItem = vm.items.find( + (item) => item.id === 'dot-toolbar-user-link-report-issue' + ); + }); + + tick(); + + expect(reportIssueItem).toBeUndefined(); + })); }); diff --git a/core-web/apps/dotcms-ui/src/app/view/components/dot-toolbar/components/dot-toolbar-user/dot-toolbar-user.component.ts b/core-web/apps/dotcms-ui/src/app/view/components/dot-toolbar/components/dot-toolbar-user/dot-toolbar-user.component.ts index 5a491d6c508..9abb755288c 100644 --- a/core-web/apps/dotcms-ui/src/app/view/components/dot-toolbar/components/dot-toolbar-user/dot-toolbar-user.component.ts +++ b/core-web/apps/dotcms-ui/src/app/view/components/dot-toolbar/components/dot-toolbar-user/dot-toolbar-user.component.ts @@ -1,4 +1,4 @@ -import { map } from 'rxjs'; +import { combineLatest, map, of } from 'rxjs'; import { AsyncPipe } from '@angular/common'; import { @@ -14,6 +14,10 @@ import { MenuItem } from 'primeng/api'; import { AvatarModule } from 'primeng/avatar'; import { Menu, MenuModule } from 'primeng/menu'; +import { catchError, startWith } from 'rxjs/operators'; + +import { DotPropertiesService } from '@dotcms/data-access'; +import { FeaturedFlags } from '@dotcms/dotcms-models'; import { DotGravatarDirective } from '@dotcms/ui'; import { DotToolbarUserStore } from './store/dot-toolbar-user.store'; @@ -39,12 +43,29 @@ import { DotMyAccountComponent } from '../dot-my-account/dot-my-account.componen }) export class DotToolbarUserComponent implements OnInit { readonly store = inject(DotToolbarUserStore); + readonly #dotPropertiesService = inject(DotPropertiesService); readonly $showReportIssue = signal(false); - readonly vm$ = this.store.vm$.pipe( - map((vm) => ({ + // Flag check lives here (not in the store) because this is meant to be + // removed after launch — easier to rip out from the component than to + // unwind from the store wiring. + readonly vm$ = combineLatest([ + this.store.vm$, + this.#dotPropertiesService + .getFeatureFlagWithDefault(FeaturedFlags.FEATURE_FLAG_REPORT_ISSUE_ENABLED, false) + // startWith renders the toolbar immediately at flag=off; catchError keeps it + // visible if /api/v1/configuration/config errors instead of taking it down. + .pipe(startWith(false), catchError(() => of(false))) + ]).pipe( + map(([vm, reportIssueEnabled]) => ({ ...vm, - items: this.withReportIssueCommand(vm.items) + items: this.withReportIssueCommand( + reportIssueEnabled + ? vm.items + : vm.items.filter( + (item) => item.id !== 'dot-toolbar-user-link-report-issue' + ) + ) })) ); $menu = viewChild('menu'); diff --git a/core-web/libs/dotcms-models/src/lib/shared-models.ts b/core-web/libs/dotcms-models/src/lib/shared-models.ts index f3daca73f54..8afa02e4eee 100644 --- a/core-web/libs/dotcms-models/src/lib/shared-models.ts +++ b/core-web/libs/dotcms-models/src/lib/shared-models.ts @@ -34,7 +34,8 @@ export const enum FeaturedFlags { FEATURE_FLAG_UVE_STYLE_EDITOR = 'FEATURE_FLAG_UVE_STYLE_EDITOR', FEATURE_FLAG_PAGE_SCANNER = 'FEATURE_FLAG_PAGE_SCANNER', FEATURE_FLAG_UVE_LEGACY_SCRIPT_INJECTION = 'FEATURE_FLAG_UVE_LEGACY_SCRIPT_INJECTION', - FEATURE_FLAG_NEW_BLOCK_EDITOR = 'FEATURE_FLAG_NEW_BLOCK_EDITOR' + FEATURE_FLAG_NEW_BLOCK_EDITOR = 'FEATURE_FLAG_NEW_BLOCK_EDITOR', + FEATURE_FLAG_REPORT_ISSUE_ENABLED = 'FEATURE_FLAG_REPORT_ISSUE_ENABLED' } export const enum DotConfigurationVariables { diff --git a/dotCMS/src/main/java/com/dotcms/featureflag/FeatureFlagName.java b/dotCMS/src/main/java/com/dotcms/featureflag/FeatureFlagName.java index b7d1fc0d4a4..687afd628bc 100644 --- a/dotCMS/src/main/java/com/dotcms/featureflag/FeatureFlagName.java +++ b/dotCMS/src/main/java/com/dotcms/featureflag/FeatureFlagName.java @@ -64,4 +64,6 @@ public interface FeatureFlagName { String FEATURE_FLAG_OPEN_SEARCH_PHASE = "FEATURE_FLAG_OPEN_SEARCH_PHASE"; String FEATURE_FLAG_NEW_BLOCK_EDITOR = "FEATURE_FLAG_NEW_BLOCK_EDITOR"; + + String FEATURE_FLAG_REPORT_ISSUE_ENABLED = "FEATURE_FLAG_REPORT_ISSUE_ENABLED"; } diff --git a/dotCMS/src/main/java/com/dotcms/rest/api/v1/system/ConfigurationResource.java b/dotCMS/src/main/java/com/dotcms/rest/api/v1/system/ConfigurationResource.java index dc8dc61f8c0..246672eda3a 100644 --- a/dotCMS/src/main/java/com/dotcms/rest/api/v1/system/ConfigurationResource.java +++ b/dotCMS/src/main/java/com/dotcms/rest/api/v1/system/ConfigurationResource.java @@ -72,7 +72,8 @@ public class ConfigurationResource implements Serializable { PageScannerResource.API_URL_PROPERTY, FeatureFlagName.FEATURE_FLAG_UVE_LEGACY_SCRIPT_INJECTION, FeatureFlagName.FEATURE_FLAG_NEW_BLOCK_EDITOR, - "REPORT_ISSUE_INCLUDE_USER_PII" })); + REPORT_ISSUE_INCLUDE_USER_PII, + FeatureFlagName.FEATURE_FLAG_REPORT_ISSUE_ENABLED })); private boolean isOnBlackList(final String key) { return null != JVMInfoResource.obfuscatePattern ? JVMInfoResource.obfuscatePattern.matcher(key).find() : false; diff --git a/dotCMS/src/main/resources/dotmarketing-config.properties b/dotCMS/src/main/resources/dotmarketing-config.properties index 06d920eb4f0..7fa9ad12b72 100644 --- a/dotCMS/src/main/resources/dotmarketing-config.properties +++ b/dotCMS/src/main/resources/dotmarketing-config.properties @@ -869,6 +869,9 @@ FEATURE_FLAG_UVE_LEGACY_SCRIPT_INJECTION=false ## New TipTap-v3 Block Editor (rollback safety: legacy editor renders by default) FEATURE_FLAG_NEW_BLOCK_EDITOR=false +## Report Issue entry in the toolbar user menu (opt-in) +FEATURE_FLAG_REPORT_ISSUE_ENABLED=false + STARTER_BUILD_VERSION=${starter.deploy.version} ##LTS properties to show EOL message From 74c4e9f45294a78c4ada11092e1cb3e06beff8b0 Mon Sep 17 00:00:00 2001 From: Freddy Montes Date: Wed, 20 May 2026 09:13:43 -0600 Subject: [PATCH 2/3] fix format --- .../dot-toolbar-user.component.spec.ts | 33 +++++++++++++++---- .../dot-toolbar-user.component.ts | 9 ++--- 2 files changed, 32 insertions(+), 10 deletions(-) diff --git a/core-web/apps/dotcms-ui/src/app/view/components/dot-toolbar/components/dot-toolbar-user/dot-toolbar-user.component.spec.ts b/core-web/apps/dotcms-ui/src/app/view/components/dot-toolbar/components/dot-toolbar-user/dot-toolbar-user.component.spec.ts index d89406e2285..9f3a3533661 100644 --- a/core-web/apps/dotcms-ui/src/app/view/components/dot-toolbar/components/dot-toolbar-user/dot-toolbar-user.component.spec.ts +++ b/core-web/apps/dotcms-ui/src/app/view/components/dot-toolbar/components/dot-toolbar-user/dot-toolbar-user.component.spec.ts @@ -11,7 +11,7 @@ import { By } from '@angular/platform-browser'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { RouterTestingModule } from '@angular/router/testing'; -import { take } from 'rxjs/operators'; +import { filter, take } from 'rxjs/operators'; import { DotGlobalMessageService, @@ -70,6 +70,7 @@ describe('DotToolbarUserComponent', () => { { provide: DotPropertiesService, useValue: { + getKey: jest.fn(() => of('true')), getFeatureFlagWithDefault: jest.fn(() => of(true)) } }, @@ -248,15 +249,35 @@ describe('DotToolbarUserComponent', () => { }); it('should open the report issue dialog from the menu item command', fakeAsync(() => { + jest.spyOn(loginService, 'watchUser').mockImplementation((callback) => { + callback({ + user: { + emailAddress: 'admin@dotcms.com', + name: 'Admin User', + fullName: 'Admin User' + }, + loginAsUser: null, + isLoginAs: false + } as any); + }); + fixture.detectChanges(); let reportIssueCommand: (() => void) | undefined; - fixture.componentInstance.vm$.pipe(take(1)).subscribe((vm) => { - reportIssueCommand = vm.items.find( - (item) => item.id === 'dot-toolbar-user-link-report-issue' - )?.command as (() => void) | undefined; - }); + // filter skips the startWith(false) emission where the item is absent. + fixture.componentInstance.vm$ + .pipe( + filter((vm) => + vm.items.some((item) => item.id === 'dot-toolbar-user-link-report-issue') + ), + take(1) + ) + .subscribe((vm) => { + reportIssueCommand = vm.items.find( + (item) => item.id === 'dot-toolbar-user-link-report-issue' + )?.command as (() => void) | undefined; + }); tick(); reportIssueCommand?.(); diff --git a/core-web/apps/dotcms-ui/src/app/view/components/dot-toolbar/components/dot-toolbar-user/dot-toolbar-user.component.ts b/core-web/apps/dotcms-ui/src/app/view/components/dot-toolbar/components/dot-toolbar-user/dot-toolbar-user.component.ts index 9abb755288c..4afb1acea0b 100644 --- a/core-web/apps/dotcms-ui/src/app/view/components/dot-toolbar/components/dot-toolbar-user/dot-toolbar-user.component.ts +++ b/core-web/apps/dotcms-ui/src/app/view/components/dot-toolbar/components/dot-toolbar-user/dot-toolbar-user.component.ts @@ -55,16 +55,17 @@ export class DotToolbarUserComponent implements OnInit { .getFeatureFlagWithDefault(FeaturedFlags.FEATURE_FLAG_REPORT_ISSUE_ENABLED, false) // startWith renders the toolbar immediately at flag=off; catchError keeps it // visible if /api/v1/configuration/config errors instead of taking it down. - .pipe(startWith(false), catchError(() => of(false))) + .pipe( + startWith(false), + catchError(() => of(false)) + ) ]).pipe( map(([vm, reportIssueEnabled]) => ({ ...vm, items: this.withReportIssueCommand( reportIssueEnabled ? vm.items - : vm.items.filter( - (item) => item.id !== 'dot-toolbar-user-link-report-issue' - ) + : vm.items.filter((item) => item.id !== 'dot-toolbar-user-link-report-issue') ) })) ); From dc39259beaab67f205ee096d57ec8443d86a13d7 Mon Sep 17 00:00:00 2001 From: Freddy Montes Date: Thu, 21 May 2026 08:44:49 -0600 Subject: [PATCH 3/3] fix(report-issue): handle HTML error responses and remove global error handler - Removed DotHttpErrorManagerService call so all submission errors show inline in the dialog instead of triggering a global error modal - HTML responses (e.g. dotCMS 404 page returned as string body) are now detected and replaced with a user-friendly message instead of dumping raw HTML into the error slot - Added report-an-issue.error.unavailable translation key Co-Authored-By: Claude Sonnet 4.6 --- .../dot-report-issue.component.spec.ts | 27 ++++++++++++------- .../dot-report-issue.component.ts | 8 +++--- .../WEB-INF/messages/Language.properties | 1 + 3 files changed, 24 insertions(+), 12 deletions(-) diff --git a/core-web/apps/dotcms-ui/src/app/view/components/dot-report-issue/dot-report-issue.component.spec.ts b/core-web/apps/dotcms-ui/src/app/view/components/dot-report-issue/dot-report-issue.component.spec.ts index 2c5a5d979b7..f9a13ea2f17 100644 --- a/core-web/apps/dotcms-ui/src/app/view/components/dot-report-issue/dot-report-issue.component.spec.ts +++ b/core-web/apps/dotcms-ui/src/app/view/components/dot-report-issue/dot-report-issue.component.spec.ts @@ -7,7 +7,6 @@ import { FileSelectEvent } from 'primeng/fileupload'; import { DotGlobalMessageService, - DotHttpErrorManagerService, DotMessageService, DotPropertiesService } from '@dotcms/data-access'; @@ -23,7 +22,6 @@ describe('DotReportIssueComponent', () => { const reportIssueMock = jest.fn(() => of('')); const successMock = jest.fn(); - const handleMock = jest.fn(() => of({})); const getKeyMock = jest.fn(() => of(true as string | boolean)); const createComponent = createComponentFactory({ @@ -35,9 +33,6 @@ describe('DotReportIssueComponent', () => { mockProvider(DotGlobalMessageService, { success: successMock }), - mockProvider(DotHttpErrorManagerService, { - handle: handleMock - }), mockProvider(DotMessageService, { get: (key: string) => key }), @@ -57,8 +52,6 @@ describe('DotReportIssueComponent', () => { reportIssueMock.mockReset(); reportIssueMock.mockReturnValue(of('')); successMock.mockReset(); - handleMock.mockReset(); - handleMock.mockReturnValue(of({})); getKeyMock.mockReset(); getKeyMock.mockReturnValue(of(true)); @@ -232,10 +225,27 @@ describe('DotReportIssueComponent', () => { expect(component.form.get('description')?.value).toBe('Broken publish button'); expect(component.screenshotFile()).toBe(screenshot); expect(component.shutdown.emit).not.toHaveBeenCalled(); - expect(handleMock).toHaveBeenCalled(); expect(component.errorMessage()).toBeTruthy(); }); + it('should show the fallback message when the backend returns an HTML error page', () => { + reportIssueMock.mockReturnValue( + throwError( + () => + new HttpErrorResponse({ + status: 404, + error: 'Not Found' + }) + ) + ); + + component.form.get('description')?.setValue('Broken publish button'); + component.save(); + + expect(component.isSubmitting()).toBe(false); + expect(component.errorMessage()).toBe('report-an-issue.error.unavailable'); + }); + it('should surface a backend media type error and stop loading', () => { const screenshot = new File(['image'], 'screenshot.png', { type: 'image/png' }); reportIssueMock.mockReturnValue( @@ -260,7 +270,6 @@ describe('DotReportIssueComponent', () => { expect(component.isSubmitting()).toBe(false); expect(component.errorMessage()).toBe('HTTP 415 Unsupported Media Type'); - expect(handleMock).toHaveBeenCalled(); expect(spectator.query('[data-testid="dot-report-issue-error-message"]'))?.toHaveText( 'HTTP 415 Unsupported Media Type' ); diff --git a/core-web/apps/dotcms-ui/src/app/view/components/dot-report-issue/dot-report-issue.component.ts b/core-web/apps/dotcms-ui/src/app/view/components/dot-report-issue/dot-report-issue.component.ts index 6449b56750f..e3caf5c9f65 100644 --- a/core-web/apps/dotcms-ui/src/app/view/components/dot-report-issue/dot-report-issue.component.ts +++ b/core-web/apps/dotcms-ui/src/app/view/components/dot-report-issue/dot-report-issue.component.ts @@ -31,7 +31,6 @@ import { map } from 'rxjs/operators'; import { DotGlobalMessageService, - DotHttpErrorManagerService, DotMessageService, DotPropertiesService } from '@dotcms/data-access'; @@ -71,7 +70,6 @@ export class DotReportIssueComponent { private readonly fb = inject(FormBuilder); private readonly dotMessageService = inject(DotMessageService); private readonly dotGlobalMessageService = inject(DotGlobalMessageService); - private readonly dotHttpErrorManagerService = inject(DotHttpErrorManagerService); private readonly dotReportIssueService = inject(DotReportIssueService); private readonly dotPropertiesService = inject(DotPropertiesService); private readonly location = inject(LOCATION_TOKEN); @@ -228,7 +226,6 @@ export class DotReportIssueComponent { error: (error) => { this.isSubmitting.set(false); this.errorMessage.set(this.getRequestErrorMessage(error)); - this.dotHttpErrorManagerService.handle(error).subscribe(); } }); } @@ -294,6 +291,11 @@ export class DotReportIssueComponent { }; if (typeof httpError.error === 'string') { + // HTML responses (e.g. dotCMS 404 pages) mean the endpoint is not reachable. + if (httpError.error.trimStart().startsWith('<')) { + return this.dotMessageService.get('report-an-issue.error.unavailable'); + } + try { const parsed = JSON.parse(httpError.error) as { message?: string }; return parsed.message || httpError.message || fallback; diff --git a/dotCMS/src/main/webapp/WEB-INF/messages/Language.properties b/dotCMS/src/main/webapp/WEB-INF/messages/Language.properties index 68651b08b97..1869550a183 100644 --- a/dotCMS/src/main/webapp/WEB-INF/messages/Language.properties +++ b/dotCMS/src/main/webapp/WEB-INF/messages/Language.properties @@ -3047,6 +3047,7 @@ report-an-issue.screenshot=Screenshot (optional) report-an-issue.screenshot.invalid-type=Screenshot must be a PNG, JPEG, or WebP image report-an-issue.screenshot.max-size=Screenshot must be 10 MB or smaller report-an-issue.error=Unable to submit the issue report +report-an-issue.error.unavailable=The report issue service is unavailable. If the issue continues, contact your administrator. report-an-issue.success=Issue report submitted successfully report-an-issue.anonymous=Send anonymously (do not include my name or email) report-an-issue.anonymous.enforced=Anonymous submission is enforced by your administrator.