diff --git a/.gitignore b/.gitignore
index 054c298a51fc..6ebe9ab28c4f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -179,3 +179,5 @@ dotCMS/dependencies.gradle
!/examples/nextjs/.npmrc
.nx/
+.cursorrules
+.cursorignore
\ No newline at end of file
diff --git a/core-web/libs/dotcms-scss/shared/_colors.scss b/core-web/libs/dotcms-scss/shared/_colors.scss
index a97692781437..a1173a3e7d35 100644
--- a/core-web/libs/dotcms-scss/shared/_colors.scss
+++ b/core-web/libs/dotcms-scss/shared/_colors.scss
@@ -23,7 +23,7 @@ $color-palette-primary-op-90: var(--color-palette-primary-op-90);
$color-palette-primary-shade: $color-palette-primary-600;
$color-palette-primary: $color-palette-primary-500;
-$color-palette-primary-tint: $color-palette-primary-200;
+$color-palette-primary-tint: $color-palette-primary-100;
$color-palette-secondary-100: var(--color-palette-secondary-100);
$color-palette-secondary-200: var(--color-palette-secondary-200);
diff --git a/core-web/libs/edit-content/src/lib/components/dot-edit-content-field/dot-edit-content-field.component.html b/core-web/libs/edit-content/src/lib/components/dot-edit-content-field/dot-edit-content-field.component.html
index 115d82c9bd5a..0d95a67ff9a0 100644
--- a/core-web/libs/edit-content/src/lib/components/dot-edit-content-field/dot-edit-content-field.component.html
+++ b/core-web/libs/edit-content/src/lib/components/dot-edit-content-field/dot-edit-content-field.component.html
@@ -151,6 +151,6 @@
}
}
}
-@if (field.hint) {
+@if (field.hint && field.fieldType !== fieldTypes.RELATIONSHIP) {
{{ field.hint }}
}
diff --git a/core-web/libs/edit-content/src/lib/components/dot-edit-content-field/dot-edit-content-field.component.spec.ts b/core-web/libs/edit-content/src/lib/components/dot-edit-content-field/dot-edit-content-field.component.spec.ts
index 874b2ba95039..a59d75fd05a7 100644
--- a/core-web/libs/edit-content/src/lib/components/dot-edit-content-field/dot-edit-content-field.component.spec.ts
+++ b/core-web/libs/edit-content/src/lib/components/dot-edit-content-field/dot-edit-content-field.component.spec.ts
@@ -4,7 +4,8 @@ import { EditorComponent } from '@tinymce/tinymce-angular';
import { MockComponent } from 'ng-mocks';
import { of } from 'rxjs';
-import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { provideHttpClient } from '@angular/common/http';
+import { provideHttpClientTesting } from '@angular/common/http/testing';
import { Provider, signal, Type } from '@angular/core';
import { ControlContainer, FormGroupDirective } from '@angular/forms';
import { By } from '@angular/platform-browser';
@@ -213,7 +214,7 @@ describe.each([...FIELDS_TO_BE_RENDER])('DotEditContentFieldComponent all fields
let spectator: Spectator;
const createComponent = createComponentFactory({
- imports: [HttpClientTestingModule, ...(fieldTestBed?.imports || [])],
+ imports: [...(fieldTestBed?.imports || [])],
declarations: [...(fieldTestBed?.declarations || [])],
component: DotEditContentFieldComponent,
componentViewProviders: [
@@ -222,7 +223,13 @@ describe.each([...FIELDS_TO_BE_RENDER])('DotEditContentFieldComponent all fields
useValue: createFormGroupDirectiveMock()
}
],
- providers: [FormGroupDirective, mockProvider(DotHttpErrorManagerService)]
+ providers: [
+ FormGroupDirective,
+ provideHttpClient(),
+ provideHttpClientTesting(),
+ ...(fieldTestBed?.providers || []),
+ mockProvider(DotHttpErrorManagerService)
+ ]
});
beforeEach(async () => {
@@ -242,11 +249,13 @@ describe.each([...FIELDS_TO_BE_RENDER])('DotEditContentFieldComponent all fields
expect(label?.textContent).toContain(fieldMock.name);
});
- it('should render the hint if present', () => {
- spectator.detectChanges();
- const hint = spectator.query(byTestId(`hint-${fieldMock.variable}`));
- expect(hint?.textContent).toContain(fieldMock.hint);
- });
+ if (fieldMock.fieldType !== FIELD_TYPES.RELATIONSHIP) {
+ it('should render the hint if present', () => {
+ spectator.detectChanges();
+ const hint = spectator.query(byTestId(`hint-${fieldMock.variable}`));
+ expect(hint?.textContent).toContain(fieldMock.hint);
+ });
+ }
it('should render the correct field type', () => {
spectator.detectChanges();
diff --git a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-relationship-field/components/dot-select-existing-content/components/pagination/pagination.component.html b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-relationship-field/components/dot-select-existing-content/components/pagination/pagination.component.html
deleted file mode 100644
index 1be37dd3eabb..000000000000
--- a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-relationship-field/components/dot-select-existing-content/components/pagination/pagination.component.html
+++ /dev/null
@@ -1,26 +0,0 @@
-@let currentPage = $currentPage();
-@let totalPages = $totalPages();
-
-
-
- {{ currentPage }} of {{ totalPages }}
-
-
-
diff --git a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-relationship-field/components/dot-select-existing-content/dot-select-existing-content.component.html b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-relationship-field/components/dot-select-existing-content/dot-select-existing-content.component.html
index 0d942a65b77d..474a86d1fc92 100644
--- a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-relationship-field/components/dot-select-existing-content/dot-select-existing-content.component.html
+++ b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-relationship-field/components/dot-select-existing-content/dot-select-existing-content.component.html
@@ -5,6 +5,7 @@
[header]="'dot.file.relationship.dialog.select.existing.content' | dm"
[modal]="true"
[(visible)]="$visible"
+ (onHide)="emitSelectedItems()"
dataKey="id"
appendTo="body"
width="90%"
@@ -14,9 +15,9 @@
[value]="data"
selectionMode="multiple"
[(selection)]="$selectedItems"
- [first]="pagination.offset"
[loading]="store.isLoading()"
[paginator]="true"
+ [first]="pagination.offset"
[rows]="pagination.rowsPerPage"
[globalFilterFields]="['title', 'step', 'description']"
styleClass="p-datatable-sm p-datatable-existing-content">
@@ -35,13 +36,12 @@
-
-
-
+
@@ -111,7 +111,10 @@
(onClick)="closeDialog()"
[text]="true"
severity="primary" />
-
+
diff --git a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-relationship-field/components/dot-select-existing-content/dot-select-existing-content.component.spec.ts b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-relationship-field/components/dot-select-existing-content/dot-select-existing-content.component.spec.ts
new file mode 100644
index 000000000000..ec2a9dc5ed84
--- /dev/null
+++ b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-relationship-field/components/dot-select-existing-content/dot-select-existing-content.component.spec.ts
@@ -0,0 +1,88 @@
+import { Spectator, createComponentFactory } from '@ngneat/spectator/jest';
+
+import { DotMessageService } from '@dotcms/data-access';
+import { RelationshipFieldItem } from '@dotcms/edit-content/fields/dot-edit-content-relationship-field/models/relationship.models';
+import { MockDotMessageService } from '@dotcms/utils-testing';
+
+import { DotSelectExistingContentComponent } from './dot-select-existing-content.component';
+import { ExistingContentStore } from './store/existing-content.store';
+
+describe('DotSelectExistingContentComponent', () => {
+ let spectator: Spectator;
+ let store: InstanceType;
+
+ const mockRelationshipItem = (id: string): RelationshipFieldItem => ({
+ id,
+ title: `Test Content ${id}`,
+ language: '1',
+ state: {
+ label: 'Published',
+ styleClass: 'small-chip'
+ },
+ description: 'Test description',
+ step: 'Step 1',
+ lastUpdate: new Date().toISOString()
+ });
+
+ const messageServiceMock = new MockDotMessageService({
+ 'dot.file.relationship.dialog.apply.one.entry': 'Apply 1 entry',
+ 'dot.file.relationship.dialog.apply.entries': 'Apply {0} entries'
+ });
+
+ const createComponent = createComponentFactory({
+ component: DotSelectExistingContentComponent,
+ componentProviders: [ExistingContentStore],
+ providers: [{ provide: DotMessageService, useValue: messageServiceMock }],
+ detectChanges: false
+ });
+
+ beforeEach(() => {
+ spectator = createComponent();
+ store = spectator.inject(ExistingContentStore, true);
+ spectator.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(spectator.component).toBeTruthy();
+ expect(store).toBeTruthy();
+ });
+
+ describe('Dialog Visibility', () => {
+ it('should set visibility to false when closeDialog is called', () => {
+ spectator.component.$visible.set(true);
+ spectator.component.closeDialog();
+ expect(spectator.component.$visible()).toBeFalsy();
+ });
+ });
+
+ describe('Selected Items State', () => {
+ it('should disable apply button when no items are selected', () => {
+ spectator.component.$selectedItems.set([]);
+ expect(spectator.component.$isApplyDisabled()).toBeTruthy();
+ });
+
+ it('should enable apply button when items are selected', () => {
+ const mockContent = [mockRelationshipItem('1')];
+ spectator.component.$selectedItems.set(mockContent);
+ expect(spectator.component.$isApplyDisabled()).toBeFalsy();
+ });
+ });
+
+ describe('Apply Button Label', () => {
+ it('should show singular label when one item is selected', () => {
+ const mockContent = [mockRelationshipItem('1')];
+ spectator.component.$selectedItems.set(mockContent);
+
+ const label = spectator.component.$applyLabel();
+ expect(label).toBe('Apply 1 entry');
+ });
+
+ it('should show plural label when multiple items are selected', () => {
+ const mockContent = [mockRelationshipItem('1'), mockRelationshipItem('2')];
+ spectator.component.$selectedItems.set(mockContent);
+
+ const label = spectator.component.$applyLabel();
+ expect(label).toBe('Apply 2 entries');
+ });
+ });
+});
diff --git a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-relationship-field/components/dot-select-existing-content/dot-select-existing-content.component.ts b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-relationship-field/components/dot-select-existing-content/dot-select-existing-content.component.ts
index bb471f25be86..0e066ed75105 100644
--- a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-relationship-field/components/dot-select-existing-content/dot-select-existing-content.component.ts
+++ b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-relationship-field/components/dot-select-existing-content/dot-select-existing-content.component.ts
@@ -1,5 +1,5 @@
import { DatePipe } from '@angular/common';
-import { ChangeDetectionStrategy, Component, computed, inject, model } from '@angular/core';
+import { ChangeDetectionStrategy, Component, computed, inject, model, output } from '@angular/core';
import { ButtonModule } from 'primeng/button';
import { DialogModule } from 'primeng/dialog';
@@ -14,9 +14,11 @@ import { TableModule } from 'primeng/table';
import { DotMessageService } from '@dotcms/data-access';
import { DotMessagePipe } from '@dotcms/ui';
-import { PaginationComponent } from './components/pagination/pagination.component';
import { SearchComponent } from './components/search/search.compoment';
-import { Content, ExistingContentStore } from './store/existing-content.store';
+import { ExistingContentStore } from './store/existing-content.store';
+
+import { RelationshipFieldItem } from '../../models/relationship.models';
+import { PaginationComponent } from '../pagination/pagination.component';
@Component({
selector: 'dot-select-existing-content',
@@ -65,7 +67,7 @@ export class DotSelectExistingContentComponent {
* A signal that holds the selected items.
* It is used to store the selected content items.
*/
- $selectedItems = model([]);
+ $selectedItems = model([]);
/**
* A computed signal that determines if the apply button is disabled.
@@ -88,6 +90,12 @@ export class DotSelectExistingContentComponent {
return this.#dotMessage.get(messageKey, selectedItems.length.toString());
});
+ /**
+ * A signal that sends the selected items when the dialog is closed.
+ * It is used to notify the parent component that the user has selected content items.
+ */
+ onSelectItems = output();
+
/**
* A method that closes the existing content dialog.
* It sets the visibility signal to false, hiding the dialog.
@@ -95,4 +103,13 @@ export class DotSelectExistingContentComponent {
closeDialog() {
this.$visible.set(false);
}
+
+ /**
+ * Closes the existing content dialog and sends the selected items to the parent component.
+ * It sets the visibility signal to false, hiding the dialog, and emits the selected items
+ * through the "selectItems" output signal.
+ */
+ emitSelectedItems() {
+ this.onSelectItems.emit(this.$selectedItems());
+ }
}
diff --git a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-relationship-field/components/dot-select-existing-content/store/existing-content.store.ts b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-relationship-field/components/dot-select-existing-content/store/existing-content.store.ts
index 88ff348283d2..c65414f675bc 100644
--- a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-relationship-field/components/dot-select-existing-content/store/existing-content.store.ts
+++ b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-relationship-field/components/dot-select-existing-content/store/existing-content.store.ts
@@ -1,4 +1,4 @@
-import { faker } from '@faker-js/faker';
+import { tapResponse } from '@ngrx/operators';
import {
patchState,
signalStore,
@@ -7,21 +7,19 @@ import {
withMethods,
withState
} from '@ngrx/signals';
+import { rxMethod } from '@ngrx/signals/rxjs-interop';
+import { pipe } from 'rxjs';
-import { computed } from '@angular/core';
+import { computed, inject } from '@angular/core';
-import { ComponentStatus } from '@dotcms/dotcms-models';
+import { tap, switchMap } from 'rxjs/operators';
-export interface Content {
- id: string;
- title: string;
- step: string;
- description: string;
- lastUpdate: string;
-}
+import { ComponentStatus } from '@dotcms/dotcms-models';
+import { RelationshipFieldItem } from '@dotcms/edit-content/fields/dot-edit-content-relationship-field/models/relationship.models';
+import { RelationshipFieldService } from '@dotcms/edit-content/fields/dot-edit-content-relationship-field/services/relationship-field.service';
export interface ExistingContentState {
- data: Content[];
+ data: RelationshipFieldItem[];
status: ComponentStatus;
pagination: {
offset: number;
@@ -50,38 +48,54 @@ export const ExistingContentStore = signalStore(
isLoading: computed(() => state.status() === ComponentStatus.LOADING),
totalPages: computed(() => Math.ceil(state.data().length / state.pagination().rowsPerPage))
})),
- withMethods((store) => ({
- loadContent() {
- const mockData = Array.from({ length: 100 }, () => ({
- id: faker.string.uuid(),
- title: faker.lorem.sentence(),
- step: faker.helpers.arrayElement(['Draft', 'Published', 'Archived']),
- description: faker.lorem.paragraph(),
- lastUpdate: faker.date.recent().toISOString()
- }));
- patchState(store, {
- data: mockData
- });
- },
- nextPage() {
- patchState(store, {
- pagination: {
- ...store.pagination(),
- offset: store.pagination().offset + store.pagination().rowsPerPage,
- currentPage: store.pagination().currentPage + 1
- }
- });
- },
- previousPage() {
- patchState(store, {
- pagination: {
- ...store.pagination(),
- offset: store.pagination().offset - store.pagination().rowsPerPage,
- currentPage: store.pagination().currentPage - 1
- }
- });
- }
- })),
+ withMethods((store) => {
+ const relationshipFieldService = inject(RelationshipFieldService);
+
+ return {
+ /**
+ * Initiates the loading of content by setting the status to LOADING and fetching content from the service.
+ * @returns {Observable} An observable that completes when the content has been loaded.
+ */
+ loadContent: rxMethod(
+ pipe(
+ tap(() => patchState(store, { status: ComponentStatus.LOADING })),
+ switchMap(() =>
+ relationshipFieldService.getContent().pipe(
+ tapResponse({
+ next: (data) =>
+ patchState(store, { data, status: ComponentStatus.LOADED }),
+ error: () => patchState(store, { status: ComponentStatus.ERROR })
+ })
+ )
+ )
+ )
+ ),
+ /**
+ * Advances the pagination to the next page and updates the state accordingly.
+ */
+ nextPage: () => {
+ patchState(store, {
+ pagination: {
+ ...store.pagination(),
+ offset: store.pagination().offset + store.pagination().rowsPerPage,
+ currentPage: store.pagination().currentPage + 1
+ }
+ });
+ },
+ /**
+ * Moves the pagination to the previous page and updates the state accordingly.
+ */
+ previousPage: () => {
+ patchState(store, {
+ pagination: {
+ ...store.pagination(),
+ offset: store.pagination().offset - store.pagination().rowsPerPage,
+ currentPage: store.pagination().currentPage - 1
+ }
+ });
+ }
+ };
+ }),
withHooks({
onInit: (store) => {
store.loadContent();
diff --git a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-relationship-field/components/pagination/pagination.component.html b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-relationship-field/components/pagination/pagination.component.html
new file mode 100644
index 000000000000..ee6cb1baa8b0
--- /dev/null
+++ b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-relationship-field/components/pagination/pagination.component.html
@@ -0,0 +1,55 @@
+@let currentPage = $currentPage();
+@let totalPages = $totalPages();
+@let currentPageReportLayout = $currentPageReportLayout();
+
+
+ @switch ($currentPageReportLayout()) {
+ @case ('center') {
+
+
+ {{ currentPage }} of {{ totalPages }}
+
+
+ }
+ @default {
+
+ {{ currentPage }} of {{ totalPages }}
+
+
+ }
+ }
+
diff --git a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-relationship-field/components/dot-select-existing-content/components/pagination/pagination.component.ts b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-relationship-field/components/pagination/pagination.component.ts
similarity index 77%
rename from core-web/libs/edit-content/src/lib/fields/dot-edit-content-relationship-field/components/dot-select-existing-content/components/pagination/pagination.component.ts
rename to core-web/libs/edit-content/src/lib/fields/dot-edit-content-relationship-field/components/pagination/pagination.component.ts
index bc683e77c09a..54542646e9de 100644
--- a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-relationship-field/components/dot-select-existing-content/components/pagination/pagination.component.ts
+++ b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-relationship-field/components/pagination/pagination.component.ts
@@ -21,6 +21,14 @@ export class PaginationComponent {
*/
$currentPage = input.required({ alias: 'currentPage' });
+ /**
+ * A signal that holds the current page report layout.
+ * It is used to determine the layout of the current page report in the pagination component.
+ */
+ $currentPageReportLayout = input<'center' | 'left'>('center', {
+ alias: 'currentPageReportLayout'
+ });
+
/**
* An output signal that emits when the previous page button is clicked.
*/
diff --git a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-relationship-field/dot-edit-content-relationship-field.component.html b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-relationship-field/dot-edit-content-relationship-field.component.html
index f8c09e9d590d..ccc2e97d92f6 100644
--- a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-relationship-field/dot-edit-content-relationship-field.component.html
+++ b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-relationship-field/dot-edit-content-relationship-field.component.html
@@ -1,21 +1,41 @@
-
+@let pagination = store.pagination();
+@let hitText = $hitText();
+@let showPagination = store.totalPages() > 1;
+
+
-
+
-
+ |
{{ 'dot.file.relationship.field.empty.message' | dm }}
@@ -23,13 +43,50 @@
|
-
-
- {{ item.title }} |
- {{ item.language }} |
- {{ item.state }} |
+
+
+
+
+ |
+
+ {{ item.title }}
+ |
+ {{ item.language }} |
+
+
+ |
+
+
+ |
+ @if (hitText || showPagination) {
+
+
+ @if (hitText) {
+ {{ hitText }}
+ }
+ @if (showPagination) {
+
+ }
+
+
+ }
-
+
diff --git a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-relationship-field/dot-edit-content-relationship-field.component.scss b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-relationship-field/dot-edit-content-relationship-field.component.scss
index 82bedffdcf4a..a213bc8cdabd 100644
--- a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-relationship-field/dot-edit-content-relationship-field.component.scss
+++ b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-relationship-field/dot-edit-content-relationship-field.component.scss
@@ -2,12 +2,23 @@
::ng-deep {
p-table {
- .p-datatable-relationship {
- border: 1px solid $color-palette-gray-300;
- border-radius: $border-radius-md;
+ .p-datatable-relationship.p-datatable-sm {
.p-datatable-header {
background-color: $color-palette-gray-100;
}
+ .p-datatable-wrapper {
+ border: 1px solid $color-palette-gray-300;
+ border-radius: $border-radius-md;
+ }
+ .p-paginator {
+ display: none;
+ }
+ .p-datatable-footer {
+ border: 0;
+ padding-left: 0;
+ padding-right: 0;
+ font-weight: lighter;
+ }
}
}
}
diff --git a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-relationship-field/dot-edit-content-relationship-field.component.ts b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-relationship-field/dot-edit-content-relationship-field.component.ts
index a11efaa55429..d413299dc68c 100644
--- a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-relationship-field/dot-edit-content-relationship-field.component.ts
+++ b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-relationship-field/dot-edit-content-relationship-field.component.ts
@@ -1,6 +1,7 @@
import {
ChangeDetectionStrategy,
Component,
+ computed,
forwardRef,
inject,
input,
@@ -10,6 +11,7 @@ import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { MenuItem } from 'primeng/api';
import { ButtonModule } from 'primeng/button';
+import { ChipModule } from 'primeng/chip';
import { DialogService } from 'primeng/dynamicdialog';
import { MenuModule } from 'primeng/menu';
import { TableModule } from 'primeng/table';
@@ -19,6 +21,7 @@ import { DotCMSContentTypeField } from '@dotcms/dotcms-models';
import { DotSelectExistingContentComponent } from '@dotcms/edit-content/fields/dot-edit-content-relationship-field/components/dot-select-existing-content/dot-select-existing-content.component';
import { DotMessagePipe } from '@dotcms/ui';
+import { PaginationComponent } from './components/pagination/pagination.component';
import { RelationshipFieldStore } from './store/relationship-field.store';
@Component({
@@ -29,7 +32,9 @@ import { RelationshipFieldStore } from './store/relationship-field.store';
ButtonModule,
MenuModule,
DotSelectExistingContentComponent,
- DotMessagePipe
+ DotMessagePipe,
+ ChipModule,
+ PaginationComponent
],
providers: [
RelationshipFieldStore,
@@ -92,6 +97,16 @@ export class DotEditContentRelationshipFieldComponent implements ControlValueAcc
*/
$field = input.required({ alias: 'field' });
+ /**
+ * A computed signal that holds the hint text for the relationship field.
+ * This text is displayed in the table header to provide additional information about the field.
+ */
+ $hitText = computed(() => {
+ const field = this.$field();
+
+ return field.hint || null;
+ });
+
/**
* Set the value of the field.
* If the value is empty, nothing happens.
@@ -134,4 +149,13 @@ export class DotEditContentRelationshipFieldComponent implements ControlValueAcc
* A callback function that is called when the field is touched.
*/
private onTouched: (() => void) | null = null;
+
+ /**
+ * Deletes an item from the store at the specified index.
+ *
+ * @param index - The index of the item to delete.
+ */
+ deleteItem(id: string) {
+ this.store.deleteItem(id);
+ }
}
diff --git a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-relationship-field/models/relationship.models.ts b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-relationship-field/models/relationship.models.ts
new file mode 100644
index 000000000000..d0edddf7596b
--- /dev/null
+++ b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-relationship-field/models/relationship.models.ts
@@ -0,0 +1,12 @@
+export interface RelationshipFieldItem {
+ id: string;
+ title: string;
+ language: string;
+ description: string;
+ step: string;
+ lastUpdate: string;
+ state: {
+ label: string;
+ styleClass: string;
+ };
+}
diff --git a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-relationship-field/services/relationship-field.service.ts b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-relationship-field/services/relationship-field.service.ts
new file mode 100644
index 000000000000..410ff294095d
--- /dev/null
+++ b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-relationship-field/services/relationship-field.service.ts
@@ -0,0 +1,100 @@
+import { faker } from '@faker-js/faker';
+import { Observable, of } from 'rxjs';
+
+import { Injectable } from '@angular/core';
+
+import { DotCMSContentlet } from '@dotcms/dotcms-models';
+import { RelationshipFieldItem } from '@dotcms/edit-content/fields/dot-edit-content-relationship-field/models/relationship.models';
+
+@Injectable({
+ providedIn: 'root'
+})
+export class RelationshipFieldService {
+ /**
+ * Gets relationship content items
+ * @returns Observable of RelationshipFieldItem array
+ */
+ getContent(count = 100): Observable {
+ const contentlets = this.#generateMockContentlets(count);
+ const relationshipContent = this.#mapContentletsToRelationshipItems(contentlets);
+
+ return of(relationshipContent);
+ }
+
+ /**
+ * Generates mock contentlets for testing purposes
+ * @returns Array of DotCMSContentlet
+ */
+ #generateMockContentlets(count: number): DotCMSContentlet[] {
+ return faker.helpers.multiple(() => this.#createMockContentlet(), { count });
+ }
+
+ /**
+ * Creates a single mock contentlet
+ * @returns DotCMSContentlet
+ */
+ #createMockContentlet(): DotCMSContentlet {
+ return {
+ identifier: faker.string.uuid(),
+ inode: faker.string.uuid(),
+ title: faker.lorem.words({ min: 1, max: 10 }),
+ contentType: faker.helpers.arrayElement(['News', 'Blog', 'Product', 'Event']),
+ baseType: 'CONTENT',
+ languageId: faker.number.int({ min: 1, max: 5 }),
+ folder: faker.system.directoryPath(),
+ hostName: faker.internet.domainName(),
+ modUser: faker.internet.userName(),
+ modDate: faker.date.recent().toISOString(),
+ owner: faker.internet.userName(),
+ sortOrder: faker.number.int({ min: 1, max: 100 }),
+ live: faker.datatype.boolean(),
+ working: true,
+ archived: false,
+ locked: faker.datatype.boolean(),
+ hasLiveVersion: faker.datatype.boolean(),
+ hasTitleImage: faker.datatype.boolean(),
+ url: faker.internet.url(),
+ titleImage: faker.image.url(),
+ stInode: faker.string.uuid(),
+ host: faker.internet.domainName(),
+ modUserName: faker.internet.userName()
+ };
+ }
+
+ /**
+ * Maps contentlets to relationship field items
+ * @param contentlets Array of DotCMSContentlet to be mapped
+ * @returns Array of RelationshipFieldItem
+ */
+ #mapContentletsToRelationshipItems(contentlets: DotCMSContentlet[]): RelationshipFieldItem[] {
+ return contentlets.map((content) => ({
+ id: content.identifier,
+ title: content.title,
+ language: faker.helpers.arrayElement([
+ 'English (en-us)',
+ 'Spanish (es-es)',
+ 'French (fr-fr)'
+ ]),
+ state: this.#getRandomState(),
+ description: faker.lorem.sentence(3),
+ step: faker.helpers.arrayElement(['Published', 'Editing', 'Archived', 'QA', 'New']),
+ lastUpdate: content.modDate
+ }));
+ }
+
+ #getRandomState(): { label: string; styleClass: string } {
+ const label = faker.helpers.arrayElement(['Changed', 'Published', 'Draft', 'Archived']);
+
+ const styleClasses = {
+ Changed: 'p-chip-sm p-chip-blue',
+ Published: 'p-chip-sm p-chip-success',
+ Draft: 'p-chip-sm p-chip-warning',
+ Archived: 'p-chip-sm p-chip-error'
+ };
+
+ return {
+ label,
+ styleClass: styleClasses[label]
+ };
+ }
+}
diff --git a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-relationship-field/store/relationship-field.store.ts b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-relationship-field/store/relationship-field.store.ts
index b7d0440cf54e..2cd8977cfa54 100644
--- a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-relationship-field/store/relationship-field.store.ts
+++ b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-relationship-field/store/relationship-field.store.ts
@@ -1,13 +1,23 @@
-import { patchState, signalStore, withMethods, withState } from '@ngrx/signals';
+import { tapResponse } from '@ngrx/operators';
+import {
+ patchState,
+ signalStore,
+ withComputed,
+ withHooks,
+ withMethods,
+ withState
+} from '@ngrx/signals';
+import { rxMethod } from '@ngrx/signals/rxjs-interop';
+import { pipe } from 'rxjs';
+
+import { computed, inject } from '@angular/core';
+
+import { switchMap, tap } from 'rxjs/operators';
import { ComponentStatus } from '@dotcms/dotcms-models';
-export interface RelationshipFieldItem {
- id: string;
- title: string;
- language: string;
- state: string;
-}
+import { RelationshipFieldItem } from '../models/relationship.models';
+import { RelationshipFieldService } from '../services/relationship-field.service';
export interface RelationshipFieldState {
data: RelationshipFieldItem[];
@@ -25,7 +35,7 @@ const initialState: RelationshipFieldState = {
pagination: {
offset: 0,
currentPage: 1,
- rowsPerPage: 10
+ rowsPerPage: 6
}
};
@@ -34,12 +44,93 @@ const initialState: RelationshipFieldState = {
* This store manages the state and actions related to the relationship field.
*/
export const RelationshipFieldStore = signalStore(
+ { providedIn: 'root' },
withState(initialState),
- withMethods((store) => ({
- setData(data: RelationshipFieldItem[]) {
- patchState(store, {
- data
- });
+ withComputed((state) => ({
+ totalPages: computed(() => Math.ceil(state.data().length / state.pagination().rowsPerPage))
+ })),
+ withMethods((store) => {
+ const relationshipFieldService = inject(RelationshipFieldService);
+
+ return {
+ /**
+ * Sets the data in the state.
+ * @param {RelationshipFieldItem[]} data - The data to be set.
+ */
+ setData(data: RelationshipFieldItem[]) {
+ patchState(store, {
+ data
+ });
+ },
+ /**
+ * Adds new data to the existing data in the state.
+ * @param {RelationshipFieldItem[]} data - The new data to be added.
+ */
+ addData(data: RelationshipFieldItem[]) {
+ const currentData = store.data();
+
+ const existingIds = new Set(currentData.map((item) => item.id));
+ const uniqueNewData = data.filter((item) => !existingIds.has(item.id));
+ patchState(store, {
+ data: [...currentData, ...uniqueNewData]
+ });
+ },
+ /**
+ * Deletes an item from the store at the specified index.
+ * @param index - The index of the item to delete.
+ */
+ deleteItem(id: string) {
+ patchState(store, {
+ data: store.data().filter((item) => item.id !== id)
+ });
+ },
+ /**
+ * Loads the data for the relationship field by fetching content from the service.
+ * It updates the state with the loaded data and sets the status to LOADED.
+ */
+ loadData: rxMethod(
+ pipe(
+ tap(() => patchState(store, { status: ComponentStatus.LOADING })),
+ switchMap(() =>
+ relationshipFieldService.getContent(10).pipe(
+ tapResponse({
+ next: (data) =>
+ patchState(store, { data, status: ComponentStatus.LOADED }),
+ error: () => patchState(store, { status: ComponentStatus.ERROR })
+ })
+ )
+ )
+ )
+ ),
+ /**
+ * Advances the pagination to the next page and updates the state accordingly.
+ */
+ nextPage: () => {
+ patchState(store, {
+ pagination: {
+ ...store.pagination(),
+ offset: store.pagination().offset + store.pagination().rowsPerPage,
+ currentPage: store.pagination().currentPage + 1
+ }
+ });
+ },
+ /**
+ * Moves the pagination to the previous page and updates the state accordingly.
+ */
+ previousPage: () => {
+ patchState(store, {
+ pagination: {
+ ...store.pagination(),
+ offset: store.pagination().offset - store.pagination().rowsPerPage,
+ currentPage: store.pagination().currentPage - 1
+ }
+ });
+ }
+ };
+ }),
+ withHooks({
+ onInit: (store) => {
+ store.loadData();
}
- }))
+ })
);
diff --git a/dotCMS/src/main/webapp/WEB-INF/messages/Language.properties b/dotCMS/src/main/webapp/WEB-INF/messages/Language.properties
index e5486c3538be..f35021eb43b2 100644
--- a/dotCMS/src/main/webapp/WEB-INF/messages/Language.properties
+++ b/dotCMS/src/main/webapp/WEB-INF/messages/Language.properties
@@ -1221,7 +1221,7 @@ dot.file.field.import.from.url.error.file.not.supported.message=This type of fil
dot.file.field.no.link.found=No link found
dot.file.relationship.dialog.select.existing.content=Select Existing Content
dot.file.relationship.dialog.per.page={0} per page
-dot.file.relationship.dialog.selected.items=Selected {0} items
+dot.file.relationship.dialog.selected.items=Show selected entries: {0}
dot.file.relationship.dialog.apply.one.entry=Apply 1 entry
dot.file.relationship.dialog.apply.entries=Apply {0} entries
dot.file.relationship.field.empty.message=Relate content by clicking on the Plus Button