Skip to content
Closed
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 @@ -94,7 +94,15 @@ const FIELD_TYPES_COMPONENTS: Record<FIELD_TYPES, Type<unknown> | DotEditFieldTe
[FIELD_TYPES.TEXT]: DotEditContentTextFieldComponent,
[FIELD_TYPES.RELATIONSHIP]: {
component: DotEditContentRelationshipFieldComponent,
providers: [mockProvider(DialogService)]
providers: [
mockProvider(DialogService),
{
provide: DotEditContentStore,
useValue: {
contentType: signal(null)
}
}
]
},
[FIELD_TYPES.FILE]: {
component: DotEditContentFileFieldComponent,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import { TableModule, TableRowReorderEvent } from 'primeng/table';
import { filter } from 'rxjs/operators';

import { DotMessageService } from '@dotcms/data-access';
import { DotCMSContentlet, DotCMSContentTypeField } from '@dotcms/dotcms-models';
import { DotCMSContentlet, DotCMSContentTypeField, DotCMSFieldTypes } from '@dotcms/dotcms-models';
import { DotMessagePipe } from '@dotcms/ui';

import { RelationshipFieldStore } from './../../store/relationship-field.store';
Expand All @@ -34,6 +34,7 @@ import { DotSelectExistingContentComponent } from './../dot-select-existing-cont
import { PaginationComponent } from './../pagination/pagination.component';

import { EditContentDialogData } from '../../../../models/dot-edit-content-dialog.interface';
import { DotEditContentStore } from '../../../../store/edit-content.store';
import { ContentletStatusPipe } from '../../../../pipes/contentlet-status.pipe';
import { LanguagePipe } from '../../../../pipes/language.pipe';
import { BaseControlValueAccessor } from '../../../shared/base-control-value-accesor';
Expand Down Expand Up @@ -78,6 +79,12 @@ export class DotRelationshipFieldComponent
*/
readonly #dotMessageService = inject(DotMessageService);

/**
* A readonly instance of the DotEditContentStore injected into the component.
* Used to access the parent content type and determine host/folder preselection.
*/
readonly #editContentStore = inject(DotEditContentStore);

/**
* A readonly private field that holds a reference to the `DestroyRef` service.
* This service is injected into the component to manage the destruction lifecycle.
Expand Down Expand Up @@ -219,6 +226,27 @@ export class DotRelationshipFieldComponent
return;
}

const contentlet = this.$contentlet();
const parentContentType = this.#editContentStore.contentType();
const hasHostFolderField =
parentContentType?.fields?.some((f) => f.fieldType === DotCMSFieldTypes.HOST_FOLDER) ??
false;

let siteOrFolderPreselection = null;
if (hasHostFolderField && contentlet) {
if (contentlet.folder && contentlet.folder !== 'SYSTEM_FOLDER') {
siteOrFolderPreselection = {
value: `folder:${contentlet.folder}`,
label: contentlet.hostName || contentlet.host
};
} else if (contentlet.host) {
siteOrFolderPreselection = {
value: `site:${contentlet.host}`,
label: contentlet.hostName || contentlet.host
};
}
}

this.#dialogRef = this.#dialogService.open(DotSelectExistingContentComponent, {
appendTo: 'body',
baseZIndex: 10000,
Expand All @@ -235,7 +263,8 @@ export class DotRelationshipFieldComponent
data: {
contentTypeId: contentType.id,
selectionMode: this.store.selectionMode(),
currentItemsIds: this.store.data().map((item) => item.inode)
currentItemsIds: this.store.data().map((item) => item.inode),
siteOrFolderPreselection
},
templates: {
header: HeaderComponent,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,12 @@ import { TreeSelectModule } from 'primeng/treeselect';

import { DotMessageService } from '@dotcms/data-access';
import { TreeNodeItem, TreeNodeSelectItem } from '@dotcms/dotcms-models';
import { GlobalStore } from '@dotcms/store';
import { DotMessagePipe, DotTruncatePathPipe, DotBrowsingService } from '@dotcms/ui';
import { MockDotMessageService } from '@dotcms/utils-testing';

import { ExistingContentStore } from '../../../../store/existing-content.store';

import { SiteFieldComponent } from './site-field.component';
import { SiteFieldStore } from './site-field.store';

Expand Down Expand Up @@ -70,6 +73,18 @@ describe('SiteFieldComponent', () => {
mockProvider(DotBrowsingService, {
getSitesTreePath: jest.fn().mockReturnValue(of(mockSites)),
getFoldersTreeNode: jest.fn().mockReturnValue(of(mockFolders))
}),
mockProvider(GlobalStore, {
siteDetails: jest.fn().mockReturnValue({
identifier: '123',
hostname: 'demo.dotcms.com',
aliases: null
}),
currentSiteId: jest.fn().mockReturnValue('123')
}),
mockProvider(ExistingContentStore, {
siteOrFolderPreselection: jest.fn().mockReturnValue(null),
isLoading: jest.fn().mockReturnValue(false)
})
]
});
Expand Down Expand Up @@ -227,6 +242,28 @@ describe('SiteFieldComponent', () => {
describe('ControlValueAccessor Implementation', () => {
const testValue = 'test-site-id';

it('should handle writeValue with a valid site value', () => {
const setInitialSelectionSpy = jest.spyOn(store, 'setInitialSelection');
spectator.detectChanges();

component.writeValue('site:123');

expect(setInitialSelectionSpy).toHaveBeenCalledWith('123', 'site', 'demo.dotcms.com');
});

it('should handle writeValue with a folder value', () => {
const setInitialSelectionSpy = jest.spyOn(store, 'setInitialSelection');
spectator.detectChanges();

component.writeValue('folder:folder-456');

expect(setInitialSelectionSpy).toHaveBeenCalledWith(
'folder-456',
'folder',
'folder-456'
);
});

it('should write value to form control', () => {
spectator.detectChanges();

Expand Down Expand Up @@ -327,3 +364,100 @@ describe('SiteFieldComponent', () => {
});
});
});

describe('SiteFieldComponent - Dialog Config Preselection', () => {
const MOCK_SITE_ID = '123';
const MOCK_HOSTNAME = 'demo.dotcms.com';
const MOCK_FOLDER_ID = 'folder-789';

let spectator: Spectator<SiteFieldComponent>;
let component: SiteFieldComponent;
let store: InstanceType<typeof SiteFieldStore>;

const messageServiceMock = new MockDotMessageService({
'dot.file.relationship.dialog.search.language.failed': 'Failed to load languages'
});

const mockSites: TreeNodeItem[] = [
{
label: MOCK_HOSTNAME,
data: {
id: MOCK_SITE_ID,
hostname: MOCK_HOSTNAME,
path: '',
type: 'site'
},
icon: 'pi pi-globe',
leaf: false,
children: []
}
];

const mockFolders = {
parent: {
id: 'parent-id',
hostName: MOCK_HOSTNAME,
path: '/parent',
addChildrenAllowed: true
},
folders: []
};

const createComponent = createComponentFactory({
component: SiteFieldComponent,
imports: [ReactiveFormsModule, TreeSelectModule, DotTruncatePathPipe, DotMessagePipe],
componentProviders: [SiteFieldStore],
providers: [
{ provide: DotMessageService, useValue: messageServiceMock },
mockProvider(DotBrowsingService, {
getSitesTreePath: jest.fn().mockReturnValue(of(mockSites)),
getFoldersTreeNode: jest.fn().mockReturnValue(of(mockFolders))
}),
mockProvider(GlobalStore, {
siteDetails: jest.fn().mockReturnValue({
identifier: MOCK_SITE_ID,
hostname: MOCK_HOSTNAME,
aliases: null
}),
currentSiteId: jest.fn().mockReturnValue(MOCK_SITE_ID)
}),
mockProvider(ExistingContentStore, {
siteOrFolderPreselection: jest.fn().mockReturnValue({
value: `folder:${MOCK_FOLDER_ID}`,
label: MOCK_HOSTNAME
}),
isLoading: jest.fn().mockReturnValue(false)
})
]
});

beforeEach(() => {
spectator = createComponent({
detectChanges: false
});
component = spectator.component;
store = spectator.inject(SiteFieldStore, true);
});

it('should use preselection label from dialog config when value matches', () => {
const setInitialSelectionSpy = jest.spyOn(store, 'setInitialSelection');
spectator.detectChanges();

component.writeValue(`folder:${MOCK_FOLDER_ID}`);

expect(setInitialSelectionSpy).toHaveBeenCalledWith(
MOCK_FOLDER_ID,
'folder',
MOCK_HOSTNAME
);
});

it('should fall back to GlobalStore when value does not match preselection', () => {
const setInitialSelectionSpy = jest.spyOn(store, 'setInitialSelection');
spectator.detectChanges();

component.writeValue(`site:${MOCK_SITE_ID}`);

expect(setInitialSelectionSpy).toHaveBeenCalledWith(MOCK_SITE_ID, 'site', MOCK_HOSTNAME);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {
ChangeDetectionStrategy,
Component,
OnInit,
computed,
effect,
forwardRef,
inject,
Expand All @@ -16,9 +17,11 @@ import {

import { TreeSelect, TreeSelectModule } from 'primeng/treeselect';

import { GlobalStore } from '@dotcms/store';
import { DotMessagePipe, DotTruncatePathPipe } from '@dotcms/ui';

import { SiteFieldStore } from './site-field.store';
import { ExistingContentStore } from '../../../../store/existing-content.store';

/**
* Component for selecting a site from a tree structure.
Expand Down Expand Up @@ -46,12 +49,27 @@ export class SiteFieldComponent implements ControlValueAccessor, OnInit {
*/
protected readonly store = inject(SiteFieldStore);

/**
* Global store for accessing current site details when pre-populating.
*/
readonly #globalStore = inject(GlobalStore);

/**
* Existing content store for accessing preselection data passed from the relationship field.
*/
readonly #existingContentStore = inject(ExistingContentStore);

/**
* Form control for the site selection.
* Binds to the TreeSelect component and manages the selected site value.
*/
readonly siteControl = new FormControl<string>('');

/**
* Computed property that returns the selected node.
*/
readonly $nodeSelected = computed(() => this.store.nodeSelected());

/**
* View child for the TreeSelect component.
* Allows access to the TreeSelect component's tree view child.
Expand Down Expand Up @@ -106,11 +124,35 @@ export class SiteFieldComponent implements ControlValueAccessor, OnInit {
/**
* Writes a new value to the form control.
* Implements ControlValueAccessor method to update the control's value programmatically.
* Handles both clearing (falsy value) and pre-populating (e.g., "site:{id}").
*/
writeValue(value: string): void {
if (!value) {
this.siteControl.setValue('');
this.store.clearSelection();

return;
}

if (value.includes(':')) {
const [type, id] = value.split(':');
if (id && (type === 'site' || type === 'folder')) {
const preselection = this.#existingContentStore.siteOrFolderPreselection();
let label: string;

if (preselection?.value === value && preselection?.label) {
label = preselection.label;
} else {
const siteDetails = this.#globalStore.siteDetails();
label =
type === 'site' && siteDetails?.identifier === id
? siteDetails.hostname
: id;
}

this.store.setInitialSelection(id, type, label);
this.siteControl.setValue(this.store.nodeSelected() as unknown as string);
}
}
}

Expand Down
Loading