Skip to content
Open
Show file tree
Hide file tree
Changes from 36 commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
c99d3a3
feat(websockets): migrate WebSocket stack to data-access and react to…
fmontes Mar 15, 2026
628e100
fix(websockets): address code quality issues from simplify review
fmontes Mar 15, 2026
f7d6ce4
style: apply prettier formatting to websocket-related files
fmontes Mar 15, 2026
b913441
refactor(dot-site): remove intermediate Subject from site event handling
fmontes Mar 15, 2026
cbcc6e2
test(dot-site): add WebSocket site event tests and fix DotEventsSocke…
fmontes Mar 15, 2026
975fc99
refactor(websockets): replace DotEventsSocketURL class with plain str…
fmontes Mar 15, 2026
2540751
refactor(websockets): simplify WS URL construction using window.locat…
fmontes Mar 15, 2026
9267850
refactor(websockets): inline WebSocket URL into service, remove token
fmontes Mar 15, 2026
765ceff
feat(websockets): handle SWITCH_SITE event in toolbar
fmontes Mar 15, 2026
58eb937
refactor(global-store): move site switching logic into GlobalStore
fmontes Mar 15, 2026
8307027
refactor(toolbar): extract SWITCH_SITE navigation into DotSiteNavigat…
fmontes Mar 15, 2026
5559864
refactor(toolbar): remove redundant navigation logic handled by DotSi…
fmontes Mar 16, 2026
5d530fa
refactor(site-switch): replace legacy SiteService.switchSite$ with Gl…
fmontes Mar 16, 2026
a64bb92
refactor(websockets): replace DotcmsEventsService with DotEventsSocket
fmontes Mar 16, 2026
3f04964
fix(websockets): address Copilot review findings
fmontes Mar 16, 2026
2363ba7
:wqMerge branch 'main' into fix-websockets
fmontes Apr 16, 2026
b8c4e9c
test(dot-templates): update spec to use GlobalStore mock for site-switch
fmontes Apr 16, 2026
d232396
test(dot-template-list): update spec to use GlobalStore mock for site…
fmontes Apr 16, 2026
1264326
test(dot-sub-nav): replace real GlobalStore with mockProvider to avoi…
fmontes Apr 16, 2026
5b25212
fix(with-websocket): avoid inject() in onDestroy default param to pre…
fmontes Apr 16, 2026
716e847
test(dot-site-navigation): fix spec structure and provider setup
fmontes Apr 16, 2026
49cfbc6
test(container-list): update spec to use GlobalStore mock for site-sw…
fmontes Apr 16, 2026
0612ac0
fix(dot-toolbar): use $currentSite signal alias in template
fmontes Apr 17, 2026
013b054
fix format
fmontes Apr 17, 2026
92fce41
fix(global-store, data-access): fix createServiceFactory misuse and M…
fmontes Apr 17, 2026
51e5154
fix(data-access): make exponential backoff test deterministic by mock…
fmontes Apr 17, 2026
1567181
fix format
fmontes Apr 17, 2026
789e5f0
fix: address Copilot review — subscription leak, eager init, test acc…
fmontes Apr 17, 2026
defaf15
feat(websocket): remove legacy DotcmsEventsService stack to eliminate…
fmontes Apr 17, 2026
3ed834a
fix(dotcms-ui): fix import order and unused EMPTY imports in spec files
fmontes Apr 17, 2026
590946f
fix(dotcms-js, dot-plugins): fix import order and deduplicate DotEven…
fmontes Apr 17, 2026
55b0ef1
fix(dotcms-js): suppress enforce-module-boundaries for DotEventsSocke…
fmontes Apr 17, 2026
bd96276
ci: trigger CI rerun
fmontes Apr 17, 2026
4619987
fix lint
fmontes Apr 17, 2026
38c4d50
fix format
fmontes Apr 17, 2026
dcca6e9
fix(global-store): export DotcmsEventsService from dotcms-js barrel a…
fmontes Apr 17, 2026
f57a33c
fix(websockets): address PR #35378 review feedback from Claude and Co…
claude Apr 18, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 0 additions & 13 deletions core-web/apps/dotcdn/src/app/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,6 @@ import { TextareaModule } from 'primeng/textarea';

import {
DotcmsConfigService,
DotcmsEventsService,
DotEventsSocket,
DotEventsSocketURL,
LoggerService,
LoginService,
SiteService,
Expand All @@ -28,13 +25,6 @@ import { DotIconComponent, DotSpinnerComponent } from '@dotcms/ui';
import { AppComponent } from './app.component';
import { DotCDNStore } from './dotcdn.component.store';

const dotEventSocketURLFactory = () => {
return new DotEventsSocketURL(
`${window.location.hostname}:${window.location.port}/api/ws/v1/system/events`,
window.location.protocol === 'https:'
);
};

@NgModule({
declarations: [AppComponent],
imports: [
Expand All @@ -59,9 +49,6 @@ const dotEventSocketURLFactory = () => {
StringUtils,
SiteService,
LoginService,
DotEventsSocket,
DotcmsEventsService,
{ provide: DotEventsSocketURL, useFactory: dotEventSocketURLFactory },
DotcmsConfigService,
DotCDNStore
],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,6 @@ import {
import {
ApiRoot,
DotcmsConfigService,
DotcmsEventsService,
DotEventsSocket,
DotEventsSocketURL,
DotPushPublishDialogService,
LoggerService,
LoginService,
Expand All @@ -52,7 +49,7 @@ import {

import { DotCustomEventHandlerService } from './dot-custom-event-handler.service';

import { dotEventSocketURLFactory, MockDotUiColorsService } from '../../../test/dot-test-bed';
import { MockDotUiColorsService } from '../../../test/dot-test-bed';
import { DotContentletEditorService } from '../../../view/components/dot-contentlet-editor/services/dot-contentlet-editor.service';
import { DotDownloadBundleDialogService } from '../dot-download-bundle-dialog/dot-download-bundle-dialog.service';
import { DotMenuService } from '../dot-menu.service';
Expand Down Expand Up @@ -94,10 +91,7 @@ describe('DotCustomEventHandlerService', () => {
{ provide: DotFormatDateService, useClass: DotFormatDateServiceMock },
UserModel,
StringUtils,
DotcmsEventsService,
LoggerService,
DotEventsSocket,
{ provide: DotEventsSocketURL, useFactory: dotEventSocketURLFactory },
DotcmsConfigService,
LoggerService,
DotCurrentUserService,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import {
createServiceFactory,
mockProvider,
SpectatorService,
SpyObject
} from '@ngneat/spectator/jest';
import { Subject } from 'rxjs';

import { DotRouterService } from '@dotcms/data-access';
import { DotSite } from '@dotcms/dotcms-models';
import { GlobalStore } from '@dotcms/store';
import { MockDotRouterService, mockSites } from '@dotcms/utils-testing';

import { DotSiteNavigationEffect } from './dot-site-navigation.effect';

describe('DotSiteNavigationEffect', () => {
let spectator: SpectatorService<DotSiteNavigationEffect>;
let dotRouterService: SpyObject<DotRouterService>;
let switchSiteSubject: Subject<DotSite>;

const createService = createServiceFactory({
service: DotSiteNavigationEffect,
providers: [{ provide: DotRouterService, useClass: MockDotRouterService }]
});

beforeEach(() => {
// switchSiteSubject must be assigned before createService() so the effect
// constructor receives the correct observable when it subscribes on instantiation.
switchSiteSubject = new Subject<DotSite>();

spectator = createService({
providers: [
mockProvider(GlobalStore, {
switchSiteEvent$: jest.fn().mockReturnValue(switchSiteSubject.asObservable())
})
]
});

dotRouterService = spectator.inject(DotRouterService);
});

it('should navigate to site browser when SWITCH_SITE fires on edit page', () => {
jest.spyOn(dotRouterService, 'isEditPage').mockReturnValue(true);
switchSiteSubject.next(mockSites[0] as unknown as DotSite);
expect(dotRouterService.goToSiteBrowser).toHaveBeenCalled();
});

it('should NOT navigate when SWITCH_SITE fires on a non-edit page', () => {
jest.spyOn(dotRouterService, 'isEditPage').mockReturnValue(false);
switchSiteSubject.next(mockSites[0] as unknown as DotSite);
expect(dotRouterService.goToSiteBrowser).not.toHaveBeenCalled();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { Injectable, inject } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';

import { DotRouterService } from '@dotcms/data-access';
import { GlobalStore } from '@dotcms/store';

/**
* App-level effect that navigates away from the edit page whenever
* another user/tab switches the current site via WebSocket.
*
* Provided eagerly in app.config.ts so it is active for the full
* application lifetime without being tied to any particular component.
*/
@Injectable({ providedIn: 'root' })
export class DotSiteNavigationEffect {
readonly #dotRouterService = inject(DotRouterService);

constructor() {
inject(GlobalStore)
.switchSiteEvent$()
.pipe(takeUntilDestroyed())
.subscribe(() => {
if (this.#dotRouterService.isEditPage()) {
this.#dotRouterService.goToSiteBrowser();
}
});
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { of } from 'rxjs';
import { of, Subject } from 'rxjs';

import { CommonModule } from '@angular/common';
import { HttpClient, provideHttpClient } from '@angular/common/http';
Expand Down Expand Up @@ -36,17 +36,20 @@ import {
} from '@dotcms/data-access';
import {
DotcmsConfigService,
DotcmsEventsService,
DotEventsSocket,
DotEventsSocketURL,
DotPushPublishDialogService,
LoggerService,
LoginService,
mockSites,
SiteService,
StringUtils
} from '@dotcms/dotcms-js';
import { CONTAINER_SOURCE, DotActionBulkResult, DotContainer } from '@dotcms/dotcms-models';
import {
CONTAINER_SOURCE,
DotActionBulkResult,
DotContainer,
DotSite
} from '@dotcms/dotcms-models';
import { GlobalStore } from '@dotcms/store';
import {
DotActionMenuButtonComponent,
DotAddToBundleComponent,
Expand All @@ -67,7 +70,6 @@ import { ContainerListComponent } from './container-list.component';
import { DotContainerListStore } from './store/dot-container-list.store';

import { DotContainersService } from '../../../api/services/dot-containers/dot-containers.service';
import { dotEventSocketURLFactory } from '../../../test/dot-test-bed';
import { DotEmptyStateComponent } from '../../../view/components/_common/dot-empty-state/dot-empty-state.component';
import { DotContentTypeSelectorComponent } from '../../../view/components/dot-content-type-selector/dot-content-type-selector.component';
import { ActionHeaderComponent } from '../../../view/components/dot-listing-data-table/action-header/action-header.component';
Expand Down Expand Up @@ -226,10 +228,13 @@ describe('ContainerListComponent', () => {
let siteService: SiteServiceMock;
let store: DotContainerListStore;
let paginatorService: PaginatorService;
let switchSiteSubject: Subject<DotSite>;

const messageServiceMock = new MockDotMessageService(messages);

beforeEach(async () => {
switchSiteSubject = new Subject<DotSite>();

await TestBed.configureTestingModule({
declarations: [],
imports: [
Expand Down Expand Up @@ -258,10 +263,8 @@ describe('ContainerListComponent', () => {
DialogService,
DotAlertConfirmService,
DotcmsConfigService,
DotcmsEventsService,
DotContainerListStore,
DotContainersService,
DotEventsSocket,
DotHttpErrorManagerService,
DotSiteBrowserService,
HttpClient,
Expand Down Expand Up @@ -290,11 +293,16 @@ describe('ContainerListComponent', () => {
}
},
{ provide: DotMessageService, useValue: messageServiceMock },
{ provide: DotEventsSocketURL, useFactory: dotEventSocketURLFactory },
{ provide: DotFormatDateService, useClass: DotFormatDateServiceMock },
{
provide: DotMessageDisplayService,
useClass: DotMessageDisplayServiceMock
},
{
provide: GlobalStore,
useValue: {
switchSiteEvent$: () => switchSiteSubject.asObservable()
}
}
],
schemas: [CUSTOM_ELEMENTS_SCHEMA, NO_ERRORS_SCHEMA]
Expand Down Expand Up @@ -515,16 +523,17 @@ describe('ContainerListComponent', () => {

it("should fetch containers when site is changed and it's not the first time", () => {
jest.spyOn(paginatorService, 'setExtraParams');
jest.spyOn(paginatorService, 'getFirstPage').mockReturnValue(of(containersMock));

siteService.setFakeCurrentSite(mockSites[1]);
switchSiteSubject.next(mockSites[1] as unknown as DotSite);

fixture.detectChanges();

expect(paginatorService.setExtraParams).toHaveBeenCalledWith(
'host',
mockSites[1].identifier
);
expect(paginatorService.get).toHaveBeenCalled();
expect(paginatorService.getFirstPage).toHaveBeenCalled();
});
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ import {
DotMessageService,
DotSiteBrowserService
} from '@dotcms/data-access';
import { SiteService } from '@dotcms/dotcms-js';
import {
CONTAINER_SOURCE,
DotActionBulkResult,
Expand All @@ -36,6 +35,7 @@ import {
DotMessageSeverity,
DotMessageType
} from '@dotcms/dotcms-models';
import { GlobalStore } from '@dotcms/store';
import {
DotActionMenuButtonComponent,
DotAddToBundleComponent,
Expand Down Expand Up @@ -84,7 +84,7 @@ export class ContainerListComponent implements OnDestroy {
private dotMessageService = inject(DotMessageService);
private dotMessageDisplayService = inject(DotMessageDisplayService);
private dialogService = inject(DialogService);
private siteService = inject(SiteService);
readonly #globalStore = inject(GlobalStore);

@ViewChild('actionsMenu')
actionsMenu: Menu;
Expand All @@ -106,9 +106,10 @@ export class ContainerListComponent implements OnDestroy {
this.selectedContainers = [];
});

this.siteService.switchSite$.subscribe(({ identifier }) =>
this.#store.getContainersByHost(identifier)
);
this.#globalStore
.switchSiteEvent$()
.pipe(takeUntil(this.destroy$))
.subscribe(({ identifier }) => this.#store.getContainersByHost(identifier));
}

ngOnDestroy(): void {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,15 +31,7 @@ import {
DotSiteBrowserService,
DotGlobalMessageService
} from '@dotcms/data-access';
import {
DotcmsConfigService,
DotcmsEventsService,
DotEventsSocket,
DotEventsSocketURL,
LoggerService,
LoginService,
StringUtils
} from '@dotcms/dotcms-js';
import { DotcmsConfigService, LoggerService, LoginService, StringUtils } from '@dotcms/dotcms-js';
import { DotCMSContentType } from '@dotcms/dotcms-models';
import { DotMessagePipe } from '@dotcms/ui';
import {
Expand All @@ -52,8 +44,6 @@ import { DotAddVariableComponent } from './dot-add-variable.component';
import { FilteredFieldTypes } from './dot-add-variable.models';
import { DOT_CONTENT_MAP, DotFieldsService } from './services/dot-fields.service';

import { dotEventSocketURLFactory } from '../../../../../test/dot-test-bed';

@Component({
selector: 'dot-form-dialog',
template: '<ng-content></ng-content>',
Expand Down Expand Up @@ -227,13 +217,10 @@ describe('DotAddVariableComponent', () => {
}
}
},
{ provide: DotEventsSocketURL, useFactory: dotEventSocketURLFactory },
StringUtils,
DotHttpErrorManagerService,
DotAlertConfirmService,
ConfirmationService,
DotcmsEventsService,
DotEventsSocket,
DotcmsConfigService,
{
provide: DotMessageDisplayService,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,20 +17,12 @@ import {
DotMessageDisplayService,
DotRouterService
} from '@dotcms/data-access';
import {
DotcmsEventsService,
DotEventsSocket,
DotEventsSocketURL,
LoggerService,
StringUtils
} from '@dotcms/dotcms-js';
import { LoggerService, StringUtils } from '@dotcms/dotcms-js';
import { CONTAINER_SOURCE } from '@dotcms/dotcms-models';
import { DotMessagePipe } from '@dotcms/ui';

import { DotContainerCreateComponent } from './dot-container-create.component';

import { dotEventSocketURLFactory } from '../../../test/dot-test-bed';

@Pipe({
name: 'dm'
})
Expand Down Expand Up @@ -91,12 +83,9 @@ describe('ContainerCreateComponent', () => {
DotGlobalMessageService,
DotHttpErrorManagerService,
DotMessageDisplayService,
DotcmsEventsService,
DotEventsSocket,
LoggerService,
StringUtils,
DotContentTypeService,
{ provide: DotEventsSocketURL, useFactory: dotEventSocketURLFactory }
DotContentTypeService
]
})
.overrideComponent(DotContainerCreateComponent, {
Expand Down
Loading
Loading