Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -428,10 +428,30 @@ describe('DotCustomEventHandlerService', () => {
})
);

expect(router.navigate).toHaveBeenCalledWith(['content/new/test']);
expect(router.navigate).toHaveBeenCalledWith(['content/new/test'], {
queryParams: {}
});
expect(router.navigate).toHaveBeenCalledTimes(1);
});

it('should create a contentlet with folderPath query param', () => {
service.handle(
new CustomEvent('ng-event', {
detail: {
name: 'create-contentlet',
data: {
contentType: 'test',
folderPath: 'default/level1/level2/'
}
}
})
);

expect(router.navigate).toHaveBeenCalledWith(['content/new/test'], {
queryParams: { folderPath: 'default/level1/level2/' }
});
});

it('should edit a workflow task using legacy handler regardless of feature flag', () => {
service.handle(
new CustomEvent('ng-event', {
Expand Down Expand Up @@ -490,7 +510,9 @@ describe('DotCustomEventHandlerService', () => {
})
);

expect(router.navigate).toHaveBeenCalledWith(['content/new/test']);
expect(router.navigate).toHaveBeenCalledWith(['content/new/test'], {
queryParams: {}
});
expect(router.navigate).toHaveBeenCalledTimes(1);
});

Expand Down Expand Up @@ -545,7 +567,7 @@ describe('DotCustomEventHandlerService', () => {
})
);

expect(router.navigate).not.toHaveBeenCalledWith(['content/new/test']);
expect(router.navigate).not.toHaveBeenCalled();
});
Comment thread
nicobytes marked this conversation as resolved.

it('should edit a workflow task using legacy handler even when content type is not in list', () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Injectable, inject } from '@angular/core';
import { Router } from '@angular/router';
import { Params, Router } from '@angular/router';

import { take } from 'rxjs/operators';

Expand Down Expand Up @@ -120,7 +120,14 @@ export class DotCustomEventHandlerService {
return this.createContentletLegacy($event);
}

this.router.navigate([`content/new/${$event.detail.data.contentType}`]);
const queryParams: Params = {};
if ($event.detail.data.folderPath) {
queryParams['folderPath'] = $event.detail.data.folderPath;
}

this.router.navigate([`content/new/${$event.detail.data.contentType}`], {
queryParams
});
});
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,41 @@ describe('DotEditContentFormResolutions', () => {
const result = resolutionValue[FIELD_TYPES.HOST_FOLDER](contentlet, field);
expect(result).toBe('');
});

describe('with queryParams', () => {
it('should return folderPath from queryParams when contentlet is null', () => {
const result = resolutionValue[FIELD_TYPES.HOST_FOLDER](null, mockField, {
folderPath: 'default/level1/level2/'
});
expect(result).toBe('default/level1/level2/');
});

it('should return folderPath from queryParams when contentlet is undefined', () => {
const result = resolutionValue[FIELD_TYPES.HOST_FOLDER](undefined, mockField, {
folderPath: 'default/level1/'
});
expect(result).toBe('default/level1/');
});

it('should prefer folderPath over field defaultValue when contentlet is null', () => {
const result = resolutionValue[FIELD_TYPES.HOST_FOLDER](null, mockField, {
folderPath: 'myhost/folder1/'
});
expect(result).toBe('myhost/folder1/');
});

it('should fall back to defaultValue when queryParams has no folderPath', () => {
const result = resolutionValue[FIELD_TYPES.HOST_FOLDER](null, mockField, {});
expect(result).toBe('default value');
});

it('should ignore queryParams when contentlet has valid hostName and url', () => {
const result = resolutionValue[FIELD_TYPES.HOST_FOLDER](mockContentlet, mockField, {
folderPath: 'default/should-be-ignored/'
});
expect(result).toBe('https://example.com');
});
});
});

describe('categoryResolutionFn', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
} from '@dotcms/dotcms-models';

import { FIELD_TYPES } from '../../models/dot-edit-content-field.enum';
import { EditContentQueryParams } from '../../store/edit-content.store';
import { getSingleSelectableFieldOptions } from '../../utils/functions.util';
import { getRelationshipFromContentlet } from '../../utils/relationshipFromContentlet';

Expand All @@ -13,11 +14,13 @@ import { getRelationshipFromContentlet } from '../../utils/relationshipFromConte
*
* @param {Object} contentlet - The contentlet object.
* @param {Object} field - The field object.
* @param {EditContentQueryParams} queryParams - Optional query params from the URL.
* @returns {*} The resolved value for the field.
*/
export type FnResolutionValue<T> = (
contentlet: DotCMSContentlet,
field: DotCMSContentTypeField
field: DotCMSContentTypeField,
queryParams?: EditContentQueryParams
) => T;

/**
Expand Down Expand Up @@ -70,10 +73,10 @@ const textFieldResolutionFn: FnResolutionValue<string> = (contentlet, field) =>
* @param field - The field object containing the default value
* @returns The resolved host folder path or the field's default value
*/
const hostFolderResolutionFn: FnResolutionValue<string> = (contentlet, field) => {
// Early return if contentlet is invalid or missing required properties
const hostFolderResolutionFn: FnResolutionValue<string> = (contentlet, field, queryParams) => {
// For new content, prefer folderPath from query params over field default
if (!contentlet?.hostName || !contentlet?.url) {
return field?.defaultValue || '';
return queryParams?.folderPath || field?.defaultValue || '';
}

const { hostName, url, baseType } = contentlet;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -539,7 +539,8 @@ export class DotEditContentFormComponent implements OnInit {
return null;
}

const value = resolutionFn(contentlet, field);
const queryParams = this.$store.queryParams();
const value = resolutionFn(contentlet, field, queryParams);

return getFinalCastedValue(value, field) ?? null;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -292,5 +292,73 @@ describe('DotEditContentStore', () => {
spectator.inject(DotContentTypeService).getContentTypeWithRender
).toHaveBeenCalledWith('test-content-type');
});

it('should store folderPath from query params', () => {
if (mockActivatedRoute.snapshot) {
mockActivatedRoute.snapshot.params = { contentType: 'test-content-type' };
mockActivatedRoute.snapshot.queryParams = {
folderPath: 'default/level1/level2/'
};
}

const mockContentType: Partial<DotCMSContentType> = {
id: 'test-content-type',
name: 'Test Type'
};
spectator
.inject(DotContentTypeService)
.getContentTypeWithRender.mockReturnValue(of(mockContentType as DotCMSContentType));
spectator.inject(DotWorkflowsActionsService).getDefaultActions.mockReturnValue(of([]));

store.initializeAsPortlet();

expect(store.queryParams()).toEqual({ folderPath: 'default/level1/level2/' });
});

it('should keep default empty queryParams when no query params are present', () => {
if (mockActivatedRoute.snapshot) {
mockActivatedRoute.snapshot.params = { contentType: 'test-content-type' };
mockActivatedRoute.snapshot.queryParams = {};
}

const mockContentType: Partial<DotCMSContentType> = {
id: 'test-content-type',
name: 'Test Type'
};
spectator
.inject(DotContentTypeService)
.getContentTypeWithRender.mockReturnValue(of(mockContentType as DotCMSContentType));
spectator.inject(DotWorkflowsActionsService).getDefaultActions.mockReturnValue(of([]));

store.initializeAsPortlet();

expect(store.queryParams()).toEqual({});
});

it('should set queryParams before calling initializeNewContent', () => {
if (mockActivatedRoute.snapshot) {
mockActivatedRoute.snapshot.params = { contentType: 'test-content-type' };
mockActivatedRoute.snapshot.queryParams = {
folderPath: 'default/folder1/'
};
}

const mockContentType: Partial<DotCMSContentType> = {
id: 'test-content-type',
name: 'Test Type'
};
spectator
.inject(DotContentTypeService)
.getContentTypeWithRender.mockReturnValue(of(mockContentType as DotCMSContentType));
spectator.inject(DotWorkflowsActionsService).getDefaultActions.mockReturnValue(of([]));

store.initializeAsPortlet();

// queryParams should be set and content type service should also be called
expect(store.queryParams()).toEqual({ folderPath: 'default/folder1/' });
expect(
spectator.inject(DotContentTypeService).getContentTypeWithRender
).toHaveBeenCalledWith('test-content-type');
});
});
});
32 changes: 30 additions & 2 deletions core-web/libs/edit-content/src/lib/store/edit-content.store.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { signalStore, withHooks, withState, withMethods } from '@ngrx/signals';
import { patchState, signalStore, withHooks, withState, withMethods } from '@ngrx/signals';

import { inject } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
Expand Down Expand Up @@ -146,6 +146,21 @@ export interface EditContentState {
* (Redux DevTools, state snapshots, hydration).
*/
hiddenFields: Record<string, boolean>;

/**
* Query params captured from the URL when initializing as a portlet.
* Used to pre-fill form fields (e.g., folderPath for Host or Folder).
*/
queryParams: EditContentQueryParams;
}

/**
* Supported query params for the edit-content route.
* Add new properties here as more params are needed.
*/
export interface EditContentQueryParams {
/** Pre-fill path for the Host or Folder field. Format: "hostname/folder1/folder2/" */
folderPath?: string;
}

export const initialRootState: EditContentState = {
Expand Down Expand Up @@ -240,7 +255,10 @@ export const initialRootState: EditContentState = {
originalContentlet: null,

// Field visibility state (controlled by BridgeAPI)
hiddenFields: {} as Record<string, boolean>
hiddenFields: {} as Record<string, boolean>,

// Query params from URL
queryParams: {}
};

/**
Expand Down Expand Up @@ -316,6 +334,16 @@ export const DotEditContentStore = signalStore(

// Use the ActivatedRoute that was injected in the closure
const params = activatedRoute.snapshot?.params;
const queryParams = activatedRoute.snapshot?.queryParams;

// Store query params first (synchronous) so they are available
// when the async content initialization completes and the form reads them.
const supportedQueryParams: EditContentQueryParams = {};
if (queryParams?.['folderPath']) {
supportedQueryParams.folderPath = queryParams['folderPath'];
}

patchState(store, { queryParams: supportedQueryParams });

if (params) {
const contentType = params['contentType'];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1562,7 +1562,10 @@ Structure defaultFileAssetStructure = CacheLocator.getContentTypeCache().getStru
"</div>";
}

var currentPageAssetFolderMap = null;

function showPageAssetPopUp(folderMap){
currentPageAssetFolderMap = folderMap;
hidePopUp('context_menu_popup_'+folderMap.inode);
var faDialog = dijit.byId("addPageAssetDialog");
if (faDialog) {
Expand Down Expand Up @@ -1600,15 +1603,19 @@ Structure defaultFileAssetStructure = CacheLocator.getContentTypeCache().getStru
"</div>";
}

function createContentlet(url, contentType) {
function createContentlet(url, contentType, folderPath) {
url = url + "&lang=" + selectedLang;
var eventData = {
url: url,
contentType: contentType
};
if (folderPath) {
eventData.folderPath = folderPath;
}
var customEvent = document.createEvent("CustomEvent");
customEvent.initCustomEvent("ng-event", false, false, {
name: "create-contentlet",
data: {
url,
contentType
}
data: eventData
});
document.dispatchEvent(customEvent);
var dialog = dijit.byId("addPageAssetDialog") || dijit.byId("addFileAssetDialog");
Expand All @@ -1623,10 +1630,19 @@ Structure defaultFileAssetStructure = CacheLocator.getContentTypeCache().getStru

if(!selected){
showDotCMSErrorMessage('<%= UtilMethods.escapeSingleQuotes(LanguageUtil.get(pageContext, "Please-select-a-valid-htmlpage-asset-type")) %>');
return;
}

var folderPath = '';
if (currentPageAssetFolderMap && currentPageAssetFolderMap.fullPath) {
var hostName = currentPageAssetFolderMap.fullPath.split(':')[0];
var cmsFolderPath = currentPageAssetFolderMap.folderPath || '/';
folderPath = cmsFolderPath === '/' ? hostName : hostName + cmsFolderPath;
}

var loc='<portlet:actionURL windowState="<%= WindowState.MAXIMIZED.toString() %>"><portlet:param name="struts_action" value="/ext/contentlet/edit_contentlet" /><portlet:param name="cmd" value="new" /></portlet:actionURL>&selectedStructure=' + selected +'&folder='+folderInode+'&referer=' + escape(refererVar);
createContentlet(loc, selected.item.velocityVarName);

createContentlet(loc, selected.item.velocityVarName, folderPath);
}


Expand Down
Loading