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/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..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,12 +11,13 @@ 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,
DotHttpErrorManagerService,
DotMessageService,
+ DotPropertiesService,
DotUiColorsService
} from '@dotcms/data-access';
import { LoggerService, LoginService } from '@dotcms/dotcms-js';
@@ -66,6 +67,13 @@ describe('DotToolbarUserComponent', () => {
provide: DotReportIssueService,
useValue: { reportIssue: jest.fn(() => of('')) }
},
+ {
+ provide: DotPropertiesService,
+ useValue: {
+ getKey: jest.fn(() => of('true')),
+ getFeatureFlagWithDefault: jest.fn(() => of(true))
+ }
+ },
{ provide: DotUiColorsService, useClass: MockDotUiColorsService },
DotToolbarUserStore
],
@@ -241,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?.();
@@ -257,4 +285,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..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
@@ -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,30 @@ 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