Skip to content

Commit e41085a

Browse files
authored
fix(content drive): Redirects to content search after closing edit dialog (dotCMS#33726)
## Summary This PR fixes the issue where Content Drive redirects to the old content search page after closing the edit content dialog. The fix ensures proper navigation back to Content Drive with the correct context preserved. https://github.com/user-attachments/assets/81d5cb2b-0c36-45d2-9584-983dd70eccfd **Key Changes:** - **Content Drive Navigation**: Implemented redirect logic in `DotContentletWrapperComponent` to detect Content Drive context (via `CD_` query parameters) and navigate back to Content Drive instead of old content search - **Query Parameter Mapping**: Added bidirectional parameter mapping utilities (`mapParamsFromEditContentlet` and `mapQueryParamsToCDParams`) to handle Content Drive context across edit flows - **Navigation Service Updates**: Enhanced `DotContentDriveNavigationService` to include Content Drive query parameters when navigating to old content editor - **Router Service Enhancement**: Updated `DotRouterService.gotoPortlet()` to support `queryParams` option for preserving navigation context - **Localization**: Added "Root" folder label translation for better UX - **Search Fixes**: Fixed special character handling in search with proper Lucene escaping (dash, plus, etc.) - **Content Type Field**: Improved `linkedSignal` behavior to prevent unnecessary updates before content types are loaded Closes dotCMS#33722, dotCMS#33528
1 parent dff8227 commit e41085a

File tree

29 files changed

+589
-84
lines changed

29 files changed

+589
-84
lines changed

core-web/apps/dotcms-ui/src/app/view/components/dot-contentlet-editor/components/dot-contentlet-wrapper/dot-contentlet-wrapper.component.spec.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,37 @@ describe('DotContentletWrapperComponent', () => {
201201
expect(dotRouterService.goToEditPage).not.toHaveBeenCalled();
202202
});
203203

204+
it('should close the dialog and navigate to content-drive when CD query params exist', () => {
205+
const contentDriveParams = {
206+
folderId: '123',
207+
path: '/images'
208+
};
209+
210+
Object.defineProperty(dotRouterService, 'currentPortlet', {
211+
value: {
212+
url: '/test?CD_folderId=123&CD_path=/images',
213+
id: '123'
214+
},
215+
writable: true
216+
});
217+
218+
jest.spyOn(dotRouterService, 'gotoPortlet');
219+
220+
dotIframeDialog.triggerEventHandler('custom', {
221+
detail: {
222+
name: 'close'
223+
}
224+
});
225+
226+
expect(dotAddContentletService.clear).toHaveBeenCalledTimes(1);
227+
expect(component.header).toBe('');
228+
expect(component.custom.emit).toHaveBeenCalledTimes(1);
229+
expect(component.shutdown.emit).toHaveBeenCalledTimes(1);
230+
expect(dotRouterService.gotoPortlet).toHaveBeenCalledWith('content-drive', {
231+
queryParams: contentDriveParams
232+
});
233+
});
234+
204235
it('should called goToEdit', () => {
205236
dotIframeDialog.triggerEventHandler('custom', {
206237
detail: {

core-web/apps/dotcms-ui/src/app/view/components/dot-contentlet-editor/components/dot-contentlet-wrapper/dot-contentlet-wrapper.component.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
DotRouterService,
99
DotIframeService
1010
} from '@dotcms/data-access';
11+
import { mapParamsFromEditContentlet } from '@dotcms/utils';
1112

1213
import { DotContentletEditorService } from '../../services/dot-contentlet-editor.service';
1314

@@ -150,6 +151,19 @@ export class DotContentletWrapperComponent {
150151
this.isContentletModified = false;
151152
this.header = '';
152153
this.shutdown.emit();
154+
155+
const searchParams = new URL(
156+
this.dotRouterService.currentPortlet.url,
157+
window.location.origin
158+
).searchParams;
159+
160+
const contentDriveParams = mapParamsFromEditContentlet(searchParams);
161+
162+
if (Object.keys(contentDriveParams).length) {
163+
this.dotRouterService.gotoPortlet('content-drive', {
164+
queryParams: contentDriveParams
165+
});
166+
}
153167
}
154168

155169
/**

core-web/libs/data-access/src/lib/dot-router/dot-router.service.spec.ts

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,11 @@ class RouterMock {
4040
return this._events.asObservable();
4141
}
4242

43-
getCurrentNavigation() {
43+
getCurrentNavigation(): { finalUrl: { queryParams: { [key: string]: string } } } | null {
4444
return {
45-
finalUrl: {}
45+
finalUrl: {
46+
queryParams: {}
47+
}
4648
};
4749
}
4850

@@ -61,7 +63,7 @@ class ActivatedRouteMock {
6163

6264
describe('DotRouterService', () => {
6365
let service: DotRouterService;
64-
let router;
66+
let router: InstanceType<typeof RouterMock>;
6567

6668
beforeEach(waitForAsync(() => {
6769
const testbed = TestBed.configureTestingModule({
@@ -84,7 +86,7 @@ describe('DotRouterService', () => {
8486
});
8587

8688
service = testbed.inject(DotRouterService);
87-
router = testbed.inject(Router);
89+
router = testbed.inject(Router) as unknown as InstanceType<typeof RouterMock>;
8890
}));
8991

9092
it('should set current url value', () => {
@@ -250,7 +252,10 @@ describe('DotRouterService', () => {
250252

251253
it('should got to porlet by URL', () => {
252254
service.gotoPortlet('/c/test');
253-
expect(router.createUrlTree).toHaveBeenCalledWith(['/c/test'], { queryParamsHandling: '' });
255+
expect(router.createUrlTree).toHaveBeenCalledWith(['/c/test'], {
256+
queryParamsHandling: '',
257+
queryParams: {}
258+
});
254259
expect(router.navigateByUrl).toHaveBeenCalledWith(['/c/test'], { replaceUrl: false });
255260
});
256261

@@ -260,13 +265,36 @@ describe('DotRouterService', () => {
260265
replaceUrl: false
261266
});
262267
expect(router.createUrlTree).toHaveBeenCalledWith(['/c/test?filter="Blog"'], {
263-
queryParamsHandling: 'preserve'
268+
queryParamsHandling: 'preserve',
269+
queryParams: {}
264270
});
265271
expect(router.navigateByUrl).toHaveBeenCalledWith(['/c/test?filter="Blog"'], {
266272
replaceUrl: false
267273
});
268274
});
269275

276+
it('should go to porlet by URL with queryParams', () => {
277+
const queryParams = {
278+
folderId: '123',
279+
path: '/images'
280+
};
281+
282+
service.gotoPortlet('/c/content-drive', {
283+
queryParams
284+
});
285+
286+
expect(router.createUrlTree).toHaveBeenCalledWith(['/c/content-drive'], {
287+
queryParamsHandling: '',
288+
queryParams: {
289+
folderId: '123',
290+
path: '/images'
291+
}
292+
});
293+
expect(router.navigateByUrl).toHaveBeenCalledWith(['/c/content-drive'], {
294+
replaceUrl: false
295+
});
296+
});
297+
270298
it('should return the correct Portlet Id', () => {
271299
expect(service.getPortletId('#/c/content?test=value')).toBe('content');
272300
expect(service.getPortletId('/c/add/content?fds=ds')).toBe('content');

core-web/libs/data-access/src/lib/dot-router/dot-router.service.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -338,8 +338,15 @@ export class DotRouterService {
338338
* @memberof DotRouterService
339339
*/
340340
gotoPortlet(link: string, navigateToPorletOptions?: DotNavigateToOptions): Promise<boolean> {
341-
const { replaceUrl = false, queryParamsHandling = '' } = navigateToPorletOptions || {};
342-
const url = this.router.createUrlTree([link], { queryParamsHandling });
341+
const {
342+
replaceUrl = false,
343+
queryParamsHandling = '',
344+
queryParams = {}
345+
} = navigateToPorletOptions || {};
346+
const url = this.router.createUrlTree([link], {
347+
queryParamsHandling,
348+
queryParams
349+
});
343350

344351
return this.router.navigateByUrl(url, { replaceUrl });
345352
}

core-web/libs/dotcms-models/src/lib/navigation/navigate-to-options.model.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,5 @@ import { QueryParamsHandling } from '@angular/router';
33
export interface DotNavigateToOptions {
44
replaceUrl?: boolean;
55
queryParamsHandling?: QueryParamsHandling;
6+
queryParams?: Record<string, string>;
67
}

core-web/libs/portlets/dot-content-drive/portlet/src/lib/components/dot-content-drive-sidebar/dot-content-drive-sidebar.component.spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,14 @@ import {
1313
DotContentDriveUploadFiles,
1414
DotTreeFolderComponent,
1515
DotFolderTreeNodeItem,
16-
DotContentDriveMoveItems
16+
DotContentDriveMoveItems,
17+
ALL_FOLDER
1718
} from '@dotcms/portlets/content-drive/ui';
1819
import { GlobalStore } from '@dotcms/store';
1920

2021
import { DotContentDriveSidebarComponent } from './dot-content-drive-sidebar.component';
2122

2223
import { DotContentDriveStore } from '../../store/dot-content-drive.store';
23-
import { ALL_FOLDER } from '../../utils/tree-folder.utils';
2424

2525
describe('DotContentDriveSidebarComponent', () => {
2626
let spectator: Spectator<DotContentDriveSidebarComponent>;

core-web/libs/portlets/dot-content-drive/portlet/src/lib/components/dot-content-drive-sidebar/dot-content-drive-sidebar.component.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
import { TreeNodeCollapseEvent, TreeNodeExpandEvent, TreeNodeSelectEvent } from 'primeng/tree';
1111

1212
import {
13+
ALL_FOLDER,
1314
DotContentDriveMoveItems,
1415
DotContentDriveUploadFiles,
1516
DotTreeFolderComponent
@@ -95,7 +96,7 @@ export class DotContentDriveSidebarComponent {
9596
protected onNodeCollapse(event: TreeNodeCollapseEvent): void {
9697
const { node } = event;
9798

98-
if (node.key === 'ALL_FOLDER') {
99+
if (node.key === ALL_FOLDER.key) {
99100
node.expanded = true;
100101
return;
101102
}

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.spec.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -551,6 +551,13 @@ describe('DotContentDriveContentTypeFieldComponent', () => {
551551

552552
describe('Content Type Selection & Store Integration', () => {
553553
beforeEach(() => {
554+
// Populate the state with content types so onChange can work
555+
patchState(spectator.component.$state, {
556+
contentTypes: MOCK_CONTENT_TYPES.filter(
557+
(ct) => ct.baseType !== DotCMSBaseTypesContentTypes.FORM && !ct.system
558+
)
559+
});
560+
554561
// Set up component with pre-selected content types for testing
555562
spectator.component.$selectedContentTypes.set([
556563
MOCK_CONTENT_TYPES[0], // blog

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.ts

Lines changed: 24 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -64,22 +64,29 @@ export class DotContentDriveContentTypeFieldComponent implements OnInit {
6464
contentTypes: []
6565
});
6666

67-
readonly $selectedContentTypes = linkedSignal<DotCMSContentType[]>(() => {
68-
const contentTypesParam = this.#store.getFilterValue('contentType') as string[];
67+
readonly $selectedContentTypes = linkedSignal<DotCMSContentType[]>(
68+
() => {
69+
const contentTypesParam = this.#store.getFilterValue('contentType') as string[];
6970

70-
if (!contentTypesParam) {
71-
return [];
72-
}
71+
if (!contentTypesParam) {
72+
return [];
73+
}
7374

74-
const contentType = this.$state.contentTypes();
75+
const contentType = this.$state.contentTypes();
7576

76-
// Get contentTypes using the contentTypesParam
77-
const contentTypes = contentType.filter((item) =>
78-
contentTypesParam.includes(item.variable)
79-
);
77+
// Get contentTypes using the contentTypesParam
78+
const contentTypes = contentType.filter((item) =>
79+
contentTypesParam.includes(item.variable)
80+
);
8081

81-
return contentTypes ?? [];
82-
});
82+
return contentTypes ?? [];
83+
},
84+
{
85+
// It will only trigger changes when we have content types to check for.
86+
equal: (a, b) =>
87+
!this.$state.contentTypes().length || (a?.length === b?.length && a === b)
88+
}
89+
);
8390

8491
/**
8592
* Maps the ensured content types to a string
@@ -165,6 +172,11 @@ export class DotContentDriveContentTypeFieldComponent implements OnInit {
165172
protected onChange() {
166173
const value = this.$selectedContentTypes();
167174

175+
// If there is no content types, don't update the store
176+
if (!this.$state.contentTypes().length) {
177+
return;
178+
}
179+
168180
if (value?.length) {
169181
this.#store.patchFilters({
170182
contentType: value.map((item) => item.variable)

core-web/libs/portlets/dot-content-drive/portlet/src/lib/components/dot-content-drive-toolbar/components/dot-content-drive-search-input/dot-content-drive-search-input.component.spec.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,11 @@ import { IconFieldModule } from 'primeng/iconfield';
1313
import { InputIconModule } from 'primeng/inputicon';
1414
import { InputTextModule } from 'primeng/inputtext';
1515

16+
import { ALL_FOLDER } from '@dotcms/portlets/content-drive/ui';
17+
1618
import { DotContentDriveSearchInputComponent } from './dot-content-drive-search-input.component';
1719

1820
import { DotContentDriveStore } from '../../../../store/dot-content-drive.store';
19-
import { ALL_FOLDER } from '../../../../utils/tree-folder.utils';
2021

2122
describe('DotContentDriveSearchInputComponent', () => {
2223
let spectator: Spectator<DotContentDriveSearchInputComponent>;

0 commit comments

Comments
 (0)