From b38696b27d77dfce3cae73bc00521ef70a9281a4 Mon Sep 17 00:00:00 2001 From: Nicolas Molina Date: Wed, 18 Dec 2024 14:59:54 -0400 Subject: [PATCH 1/8] chore(edit-content): handle 1-1 with radio button --- .../dotcms-theme/components/form/_radiobutton.scss | 1 + .../dot-select-existing-content.component.html | 10 +++++----- .../dot-select-existing-content.component.ts | 1 + 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/core-web/libs/dotcms-scss/angular/dotcms-theme/components/form/_radiobutton.scss b/core-web/libs/dotcms-scss/angular/dotcms-theme/components/form/_radiobutton.scss index c8b90dfd3eda..e876056b3d57 100644 --- a/core-web/libs/dotcms-scss/angular/dotcms-theme/components/form/_radiobutton.scss +++ b/core-web/libs/dotcms-scss/angular/dotcms-theme/components/form/_radiobutton.scss @@ -1,6 +1,7 @@ @use "variables" as *; @import "common"; +p-tableradiobutton.p-element, p-radiobutton.p-element { gap: $spacing-1; 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 cf611578ffd5..0852c67f5480 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 @@ -9,7 +9,6 @@ (onShow)="onShowDialog()" (onHide)="emitSelectedItems()" [draggable]="false" - dataKey="id" appendTo="body" width="90%" [style]="{ width: '90%', 'max-width': '1040px', height: '90vh' }"> @@ -23,9 +22,10 @@ - + Title @@ -92,7 +92,7 @@ - +

{{ item.title }}

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 8917e5db9af7..235aefb1b011 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 @@ -68,6 +68,7 @@ export class DotSelectExistingContentComponent { * It is used to store the selected content items. */ $selectedItems = model([]); + $selectedItem = model(null); /** * A computed signal that determines if the apply button is disabled. From 226136c1074a54717981c4a44ddb8102fe82f9bb Mon Sep 17 00:00:00 2001 From: Nicolas Molina Date: Mon, 23 Dec 2024 17:06:10 -0400 Subject: [PATCH 2/8] chore(edit-content): restriccions to relationship field --- .../dot-select-existing-file.component.ts | 8 + ...dot-select-existing-content.component.html | 246 ++++++++---------- .../dot-select-existing-content.component.ts | 96 ++++--- ...-content-relationship-field.component.html | 26 +- ...it-content-relationship-field.component.ts | 149 ++++++++--- ...it-content-relationship-field.constants.ts | 9 + .../models/relationship.models.ts | 11 + .../store/relationship-field.store.ts | 40 ++- 8 files changed, 365 insertions(+), 220 deletions(-) diff --git a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/components/dot-select-existing-file/dot-select-existing-file.component.ts b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/components/dot-select-existing-file/dot-select-existing-file.component.ts index e6d3f1999960..5b371b096426 100644 --- a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/components/dot-select-existing-file/dot-select-existing-file.component.ts +++ b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/components/dot-select-existing-file/dot-select-existing-file.component.ts @@ -43,6 +43,10 @@ export class DotSelectExistingFileComponent implements OnInit { */ readonly store = inject(SelectExisingFileStore); + /** + * A readonly property that injects the `DotFileFieldUploadService` service. + * This service is used to manage the state and actions related to selecting existing files. + */ readonly #uploadService = inject(DotFileFieldUploadService); /** * A reference to the dynamic dialog instance. @@ -59,6 +63,10 @@ export class DotSelectExistingFileComponent implements OnInit { */ $sideBarRef = viewChild.required(DotSideBarComponent); + /** + * A readonly property that injects the `DynamicDialogConfig` service. + * This service is used to get the dialog data. + */ readonly #dialogConfig = inject(DynamicDialogConfig); constructor() { 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 289230e66019..e3caa50b0c0d 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 @@ -1,143 +1,121 @@ @let data = store.data(); @let columns = store.columns(); @let pagination = store.pagination(); +@let selectionMode = $selectionMode(); +@let totalItems = $items().length; -@if ($visible()) { - @defer { - - -
- - - {{ 'dot.file.relationship.dialog.search' | dm }} - -
-
- @if (store.errorMessage()) { -
-

{{ store.errorMessage() | dm }}

-
- } @else if (data.length === 0) { -
- -

{{ 'dot.file.relationship.dialog.search.empty.content' | dm }}

-
- } @else { - - -
-
-
- -
-
-

- {{ - 'dot.file.relationship.dialog.per.page' - | dm: [pagination.rowsPerPage.toString()] - }} -

-
-
- @if (data.length > pagination.rowsPerPage) { - - } +
+ @if (store.errorMessage()) { +
+

{{ store.errorMessage() | dm }}

+
+ } @else if (data.length === 0) { +
+ +

{{ 'dot.file.relationship.dialog.search.empty.content' | dm }}

+
+ } @else { + + +
+
+
+ +
+
+

+ {{ + 'dot.file.relationship.dialog.per.page' + | dm: [pagination.rowsPerPage.toString()] + }} +

- - - - - - - @for (column of columns; track $index) { - - {{ column.header }} - - - } - - {{ 'dot.file.relationship.dialog.menu.column' | dm }} - - - - - - - - - @for (column of columns; track $index) { - -

{{ item[column.field] }}

- - } - - - - -
- - } - -
-
-

- {{ - 'dot.file.relationship.dialog.selected.items' - | dm: [$selectedItems().length.toString()] - }} -

-
-
- -
+ @if (data.length > pagination.rowsPerPage) { + + }
- + + + + @if (selectionMode === 'multiple') { + + } + + @for (column of columns; track $index) { + + {{ column.header }} + + + } + + {{ 'dot.file.relationship.dialog.menu.column' | dm }} + + + + + + + @if (selectionMode === 'multiple') { + + } @else { + + } + + @for (column of columns; track $index) { + +

{{ item[column.field] }}

+ + } + + + + +
+ } -} \ No newline at end of file +
+
+

+ {{ 'dot.file.relationship.dialog.selected.items' | dm: [totalItems.toString()] }} +

+
+
+ + +
+
+
\ No newline at end of file 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 05fba42819ba..57a0b73d56a1 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,4 +1,3 @@ -import { DatePipe } from '@angular/common'; import { ChangeDetectionStrategy, Component, @@ -6,11 +5,14 @@ import { inject, input, model, - output + output, + OnInit, + signal } from '@angular/core'; import { ButtonModule } from 'primeng/button'; import { DialogModule } from 'primeng/dialog'; +import { DynamicDialogConfig, DynamicDialogRef } from 'primeng/dynamicdialog'; import { IconFieldModule } from 'primeng/iconfield'; import { InputGroupModule } from 'primeng/inputgroup'; import { InputIconModule } from 'primeng/inputicon'; @@ -25,9 +27,14 @@ import { DotMessagePipe } from '@dotcms/ui'; import { SearchComponent } from './components/search/search.compoment'; import { ExistingContentStore } from './store/existing-content.store'; -import { RelationshipFieldItem } from '../../models/relationship.models'; +import { RelationshipFieldItem, SelectionMode } from '../../models/relationship.models'; import { PaginationComponent } from '../pagination/pagination.component'; +type DialogData = { + contentTypeId: string; + selectionMode: SelectionMode; +}; + @Component({ selector: 'dot-select-existing-content', standalone: true, @@ -40,7 +47,6 @@ import { PaginationComponent } from '../pagination/pagination.component'; IconFieldModule, InputIconModule, InputTextModule, - DatePipe, PaginationComponent, InputGroupModule, OverlayPanelModule, @@ -51,7 +57,7 @@ import { PaginationComponent } from '../pagination/pagination.component'; providers: [ExistingContentStore], changeDetection: ChangeDetectionStrategy.OnPush }) -export class DotSelectExistingContentComponent { +export class DotSelectExistingContentComponent implements OnInit { /** * A readonly instance of the ExistingContentStore injected into the component. * This store is used to manage the state and actions related to the existing content. @@ -65,44 +71,60 @@ export class DotSelectExistingContentComponent { readonly #dotMessage = inject(DotMessageService); /** - * A signal that controls the visibility of the existing content dialog. - * When true, the dialog is shown allowing users to select existing content. - * When false, the dialog is hidden. + * A reference to the dynamic dialog instance. + * This is a read-only property that is injected using Angular's dependency injection. + * It provides access to the dialog's methods and properties. */ - $visible = model(false, { alias: 'visible' }); + readonly #dialogRef = inject(DynamicDialogRef); /** - * A signal that holds the selected items. - * It is used to store the selected content items. + * A readonly property that injects the `DynamicDialogConfig` service. + * This service is used to get the dialog data. */ - $selectedItems = model([]); - $selectedItem = model(null); + readonly #dialogConfig = inject(DynamicDialogConfig); /** - * A computed signal that determines if the apply button is disabled. - * It is disabled when no items are selected. + * A required input that holds the selection mode. + * It is used to determine the selection mode. */ - $isApplyDisabled = computed(() => this.$selectedItems().length === 0); + $selectionMode = signal(null); /** - * A required input that holds the content id. - * It is used to get the content type fields. + * A signal that holds the selected items. + * It is used to store the selected content items. */ - $contentTypeId = input.required({ alias: 'contentTypeId' }); + $selectedItems = model(null); + + /** + * A computed signal that holds the items. + * It is used to store the items. + */ + $items = computed(() => { + const selectedItems = this.$selectedItems(); + + if (selectedItems) { + const isArray = Array.isArray(selectedItems); + const items = isArray ? selectedItems : [selectedItems]; + + return items; + } + + return []; + }); /** * A computed signal that determines the label for the apply button. * It is used to display the appropriate message based on the number of selected items. */ $applyLabel = computed(() => { - const selectedItems = this.$selectedItems(); + const count = this.$items().length; const messageKey = - selectedItems.length === 1 + count === 1 ? 'dot.file.relationship.dialog.apply.one.entry' : 'dot.file.relationship.dialog.apply.entries'; - return this.#dotMessage.get(messageKey, selectedItems.length.toString()); + return this.#dotMessage.get(messageKey, count.toString()); }); /** @@ -111,12 +133,28 @@ export class DotSelectExistingContentComponent { */ onSelectItems = output(); + ngOnInit() { + const data: DialogData = this.#dialogConfig.data; + + if (!data.contentTypeId) { + throw new Error('Content type id is required'); + } + + if (!data.selectionMode) { + throw new Error('Selection mode is required'); + } + + this.store.loadContent(data.contentTypeId); + // TODO: remove this once we have the cardinality set + this.$selectionMode.set(data.selectionMode); + } + /** * A method that closes the existing content dialog. * It sets the visibility signal to false, hiding the dialog. */ closeDialog() { - this.$visible.set(false); + this.#dialogRef.close(); } /** @@ -125,7 +163,7 @@ export class DotSelectExistingContentComponent { * through the "selectItems" output signal. */ emitSelectedItems() { - this.onSelectItems.emit(this.$selectedItems()); + this.#dialogRef.close(this.$items()); } /** @@ -134,14 +172,8 @@ export class DotSelectExistingContentComponent { * @returns True if the item is selected, false otherwise. */ checkIfSelected(item: RelationshipFieldItem) { - return this.$selectedItems().some((selectedItem) => selectedItem.id === item.id); - } + const items = this.$items(); - /** - * Shows the existing content dialog and loads the content. - */ - onShowDialog() { - this.store.applyInitialState(); - this.store.loadContent(this.$contentTypeId()); + return items.some((selectedItem) => selectedItem.id === item.id); } } 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 4448f9c38768..df9925aef31c 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,14 +1,16 @@ @let pagination = store.pagination(); -@let hitText = $hitText(); +@let attributes = $attributes(); @let showPagination = store.totalPages() > 1; @let data = store.data(); - @@ -54,7 +56,10 @@ {{ item.language }} + {{ item.modDate }} + } - - - + \ No newline at end of file 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 a51b84b754c4..141782f67bab 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 @@ -2,26 +2,31 @@ import { ChangeDetectionStrategy, Component, computed, + DestroyRef, + effect, forwardRef, inject, - input, - signal + input } from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; 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 { DialogService, DynamicDialogRef } from 'primeng/dynamicdialog'; import { MenuModule } from 'primeng/menu'; import { TableModule } from 'primeng/table'; +import { filter } from 'rxjs/operators'; + import { DotMessageService } from '@dotcms/data-access'; 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 { RelationshipFieldItem } from './models/relationship.models'; import { RelationshipFieldStore } from './store/relationship-field.store'; @Component({ @@ -31,7 +36,6 @@ import { RelationshipFieldStore } from './store/relationship-field.store'; TableModule, ButtonModule, MenuModule, - DotSelectExistingContentComponent, DotMessagePipe, ChipModule, PaginationComponent @@ -50,6 +54,11 @@ import { RelationshipFieldStore } from './store/relationship-field.store'; changeDetection: ChangeDetectionStrategy.OnPush }) export class DotEditContentRelationshipFieldComponent implements ControlValueAccessor { + /** + * A readonly instance of the RelationshipFieldStore injected into the component. + * This store is used to manage the state and actions related to the relationship field. + */ + readonly store = inject(RelationshipFieldStore); /** * A readonly private field that injects the DotMessageService. * This service is used for handling message-related functionalities within the component. @@ -57,38 +66,50 @@ export class DotEditContentRelationshipFieldComponent implements ControlValueAcc readonly #dotMessageService = inject(DotMessageService); /** - * A signal that controls the visibility of the existing content dialog. - * When true, the dialog is shown allowing users to select existing content. - * When false, the dialog is hidden. + * A readonly private field that holds a reference to the `DestroyRef` service. + * This service is injected into the component to manage the destruction lifecycle. */ - $showExistingContentDialog = signal(false); + readonly #destroyRef = inject(DestroyRef); /** - * A signal that holds the menu items for the relationship field. - * These items control the visibility of the existing content dialog and the creation of new content. + * A readonly private field that holds an instance of the DialogService. + * This service is injected using Angular's dependency injection mechanism. + * It is used to manage dialog interactions within the component. */ - $menuItems = signal([ - { - label: this.#dotMessageService.get( - 'dot.file.relationship.field.table.existing.content' - ), - command: () => { - this.$showExistingContentDialog.update((value) => !value); - } - }, - { - label: this.#dotMessageService.get('dot.file.relationship.field.table.new.content'), - command: () => { - // TODO: Implement new content - } - } - ]); + readonly #dialogService = inject(DialogService); /** - * A readonly instance of the RelationshipFieldStore injected into the component. - * This store is used to manage the state and actions related to the relationship field. + * Reference to the dynamic dialog. It can be null if no dialog is currently open. + * + * @type {DynamicDialogRef | null} */ - readonly store = inject(RelationshipFieldStore); + #dialogRef: DynamicDialogRef | null = null; + + /** + * A signal that holds the menu items for the relationship field. + * These items control the visibility of the existing content dialog and the creation of new content. + */ + $menuItems = computed(() => { + const isDisabledCreateNewContent = this.store.isDisabledCreateNewContent(); + + return [ + { + label: this.#dotMessageService.get( + 'dot.file.relationship.field.table.existing.content' + ), + disabled: isDisabledCreateNewContent, + command: () => { + this.showExistingContentDialog(); + } + }, + { + label: this.#dotMessageService.get('dot.file.relationship.field.table.new.content'), + command: () => { + // TODO: Implement new content + } + } + ]; + }); /** * DotCMS Content Type Field @@ -98,23 +119,41 @@ export class DotEditContentRelationshipFieldComponent implements ControlValueAcc $field = input.required({ alias: 'field' }); /** - * A computed signal that holds the content type id for the relationship field. - * This id is used to get the content type fields. + * Creates an instance of DotEditContentRelationshipFieldComponent. + * It sets the cardinality of the relationship field based on the field's cardinality. + * + * @memberof DotEditContentRelationshipFieldComponent */ - $contentTypeId = computed(() => { - const field = this.$field(); + constructor() { + effect( + () => { + const field = this.$field(); - return field?.relationships?.velocityVar || null; - }); + const cardinality = field?.relationships?.cardinality ?? null; + + if (cardinality === null) { + return; + } + + this.store.setCardinality(cardinality); + }, + { + allowSignalWrites: true + } + ); + } /** - * 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. + * A computed signal that holds the attributes for the relationship field. + * This attributes are used to get the content type fields. */ - $hitText = computed(() => { + $attributes = computed(() => { const field = this.$field(); - return field.hint || null; + return { + contentTypeId: field?.relationships?.velocityVar || null, + hitText: field?.hint || null + }; }); /** @@ -168,4 +207,36 @@ export class DotEditContentRelationshipFieldComponent implements ControlValueAcc deleteItem(id: string) { this.store.deleteItem(id); } + + /** + * Shows the existing content dialog. + */ + showExistingContentDialog() { + this.#dialogRef = this.#dialogService.open(DotSelectExistingContentComponent, { + header: 'sdsd', + appendTo: 'body', + closeOnEscape: false, + draggable: false, + keepInViewport: false, + modal: true, + resizable: false, + position: 'center', + width: '90%', + height: '90vh', + style: { 'max-width': '1040px', 'max-height': '800px' }, + data: { + contentTypeId: this.$attributes().contentTypeId, + selectionMode: this.store.selectionMode() + } + }); + + this.#dialogRef.onClose + .pipe( + filter((file) => !!file), + takeUntilDestroyed(this.#destroyRef) + ) + .subscribe((items: RelationshipFieldItem[]) => { + this.store.addData(items); + }); + } } diff --git a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-relationship-field/dot-edit-content-relationship-field.constants.ts b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-relationship-field/dot-edit-content-relationship-field.constants.ts index 02983d8db822..de1950717c44 100644 --- a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-relationship-field/dot-edit-content-relationship-field.constants.ts +++ b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-relationship-field/dot-edit-content-relationship-field.constants.ts @@ -1,3 +1,5 @@ +import { RelationshipTypes } from './models/relationship.models'; + export const MANDATORY_FIELDS = { title: 'title', language: 'language', @@ -7,3 +9,10 @@ export const MANDATORY_FIELDS = { export const MANDATORY_FIRST_COLUMNS = [MANDATORY_FIELDS.title]; export const MANDATORY_LAST_COLUMNS = [MANDATORY_FIELDS.language, MANDATORY_FIELDS.modDate]; + +export const RELATIONSHIP_OPTIONS = { + 0: RelationshipTypes.ONE_TO_MANY, + 1: RelationshipTypes.MANY_TO_MANY, + 2: RelationshipTypes.ONE_TO_ONE, + 3: RelationshipTypes.MANY_TO_ONE +}; 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 index bac66d08b9a4..9f0756fb210e 100644 --- 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 @@ -6,3 +6,14 @@ export interface RelationshipFieldItem extends MandatoryFields { id: string; [key: string]: string; } + +export enum RelationshipTypes { + ONE_TO_ONE = '1-1', + ONE_TO_MANY = '1-n', + MANY_TO_ONE = 'n-1', + MANY_TO_MANY = 'n-n' +} + +export type RelationshipType = `${RelationshipTypes}`; + +export type SelectionMode = 'single' | 'multiple'; 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 647cbb659178..b77e660f8402 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 @@ -15,11 +15,17 @@ import { tap } from 'rxjs/operators'; import { ComponentStatus } from '@dotcms/dotcms-models'; -import { RelationshipFieldItem } from '../models/relationship.models'; +import { RELATIONSHIP_OPTIONS } from '../dot-edit-content-relationship-field.constants'; +import { + RelationshipFieldItem, + RelationshipTypes, + SelectionMode +} from '../models/relationship.models'; export interface RelationshipFieldState { data: RelationshipFieldItem[]; status: ComponentStatus; + selectionMode: SelectionMode | null; pagination: { offset: number; currentPage: number; @@ -30,6 +36,7 @@ export interface RelationshipFieldState { const initialState: RelationshipFieldState = { data: [], status: ComponentStatus.INIT, + selectionMode: null, pagination: { offset: 0, currentPage: 1, @@ -45,7 +52,17 @@ export const RelationshipFieldStore = signalStore( { providedIn: 'root' }, withState(initialState), withComputed((state) => ({ - totalPages: computed(() => Math.ceil(state.data().length / state.pagination().rowsPerPage)) + totalPages: computed(() => Math.ceil(state.data().length / state.pagination().rowsPerPage)), + isDisabledCreateNewContent: computed(() => { + const totalItems = state.data().length; + const selectionMode = state.selectionMode(); + + if (selectionMode === 'single') { + return totalItems >= 1; + } + + return false; + }) })), withMethods((store) => { return { @@ -58,6 +75,25 @@ export const RelationshipFieldStore = signalStore( data }); }, + /** + * Sets the cardinality of the relationship field. + * @param {number} cardinality - The cardinality of the relationship field. + */ + setCardinality(cardinality: number) { + + const relationshipType = RELATIONSHIP_OPTIONS[cardinality]; + + if (!relationshipType) { + throw new Error('Invalid relationship type'); + } + + const selectionMode: SelectionMode = + relationshipType === RelationshipTypes.ONE_TO_ONE ? 'single' : 'multiple'; + + patchState(store, { + selectionMode + }); + }, /** * Adds new data to the existing data in the state. * @param {RelationshipFieldItem[]} data - The new data to be added. From 89523f299dd32f2e1798f0a05a9485f778d7c31f Mon Sep 17 00:00:00 2001 From: Nicolas Molina Date: Tue, 24 Dec 2024 11:17:56 -0400 Subject: [PATCH 3/8] chore(edit-content): add unit tests --- ...dot-select-existing-content.component.html | 4 +- ...-select-existing-content.component.spec.ts | 126 +++++++++++++----- .../dot-select-existing-content.component.ts | 26 +--- .../store/existing-content.store.spec.ts | 84 ++++++------ .../store/existing-content.store.ts | 25 ++-- .../components/header/header.component.html | 6 + .../components/header/header.component.ts | 12 ++ ...-content-relationship-field.component.html | 14 +- ...-content-relationship-field.component.scss | 6 +- ...it-content-relationship-field.component.ts | 4 + .../store/relationship-field.store.ts | 1 - 11 files changed, 188 insertions(+), 120 deletions(-) create mode 100644 core-web/libs/edit-content/src/lib/fields/dot-edit-content-relationship-field/components/header/header.component.html create mode 100644 core-web/libs/edit-content/src/lib/fields/dot-edit-content-relationship-field/components/header/header.component.ts 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 e3caa50b0c0d..bb7886107de0 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 @@ -1,7 +1,7 @@ @let data = store.data(); @let columns = store.columns(); @let pagination = store.pagination(); -@let selectionMode = $selectionMode(); +@let selectionMode = store.selectionMode(); @let totalItems = $items().length;
@@ -118,4 +118,4 @@ [label]="$applyLabel()" />
-
\ No newline at end of file +
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 index 1db643357bbd..d8ec78e8ca21 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.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 @@ -1,9 +1,7 @@ import { Spectator, createComponentFactory, mockProvider } from '@ngneat/spectator/jest'; import { of } from 'rxjs'; -import { fakeAsync, tick } from '@angular/core/testing'; - -import { Dialog } from 'primeng/dialog'; +import { DynamicDialogConfig, DynamicDialogRef } from 'primeng/dynamicdialog'; import { DotMessageService } from '@dotcms/data-access'; import { RelationshipFieldItem } from '@dotcms/edit-content/fields/dot-edit-content-relationship-field/models/relationship.models'; @@ -17,6 +15,7 @@ import { RelationshipFieldService } from '../../services/relationship-field.serv describe('DotSelectExistingContentComponent', () => { let spectator: Spectator; let store: InstanceType; + let dialogRef: DynamicDialogRef; const mockRelationshipItem = (id: string): RelationshipFieldItem => ({ id, @@ -32,6 +31,13 @@ describe('DotSelectExistingContentComponent', () => { 'dot.file.relationship.dialog.apply.entries': 'Apply {0} entries' }); + const mockDialogConfig = { + data: { + contentTypeId: 'test-content-type-id', + selectionMode: 'multiple' + } + }; + const createComponent = createComponentFactory({ component: DotSelectExistingContentComponent, componentProviders: [ExistingContentStore], @@ -39,7 +45,9 @@ describe('DotSelectExistingContentComponent', () => { mockProvider(RelationshipFieldService, { getContent: jest.fn(() => of([])) }), - { provide: DotMessageService, useValue: messageServiceMock } + { provide: DotMessageService, useValue: messageServiceMock }, + { provide: DynamicDialogRef, useValue: { close: jest.fn() } }, + { provide: DynamicDialogConfig, useValue: mockDialogConfig } ], detectChanges: false }); @@ -47,6 +55,7 @@ describe('DotSelectExistingContentComponent', () => { beforeEach(() => { spectator = createComponent(); store = spectator.inject(ExistingContentStore, true); + dialogRef = spectator.inject(DynamicDialogRef); spectator.detectChanges(); }); @@ -55,24 +64,83 @@ describe('DotSelectExistingContentComponent', () => { expect(store).toBeTruthy(); }); - describe('Dialog Visibility', () => { - it('should set visibility to false when closeDialog is called', () => { - spectator.component.$visible.set(true); + describe('Initialization', () => { + it('should initialize with required configuration', () => { + const spy = jest.spyOn(store, 'initLoad'); + spectator.component.ngOnInit(); + expect(spy).toHaveBeenCalledWith({ + contentTypeId: 'test-content-type-id', + selectionMode: 'multiple' + }); + }); + + it('should throw error when selectionMode is missing', () => { + const invalidConfig = createComponentFactory({ + component: DotSelectExistingContentComponent, + componentProviders: [ExistingContentStore], + providers: [ + mockProvider(RelationshipFieldService, { + getContent: jest.fn(() => of([])) + }), + { provide: DotMessageService, useValue: messageServiceMock }, + { provide: DynamicDialogRef, useValue: { close: jest.fn() } }, + { + provide: DynamicDialogConfig, + useValue: { data: { contentTypeId: 'test-id' } } + } + ] + }); + + expect(() => { + const component = invalidConfig(); + component.detectChanges(); + }).toThrow('Selection mode is required'); + }); + }); + + describe('Dialog Behavior', () => { + it('should close dialog with selected items', () => { + const mockItems = [mockRelationshipItem('1'), mockRelationshipItem('2')]; + spectator.component.$selectedItems.set(mockItems); + spectator.component.closeDialog(); - expect(spectator.component.$visible()).toBeFalsy(); + + expect(dialogRef.close).toHaveBeenCalledWith(mockItems); + }); + + it('should close dialog with empty array when no items selected', () => { + spectator.component.$selectedItems.set([]); + + spectator.component.closeDialog(); + + expect(dialogRef.close).toHaveBeenCalledWith([]); }); }); describe('Selected Items State', () => { it('should disable apply button when no items are selected', () => { spectator.component.$selectedItems.set([]); - expect(spectator.component.$isApplyDisabled()).toBeTruthy(); + expect(spectator.component.$items().length).toBe(0); }); it('should enable apply button when items are selected', () => { const mockContent = [mockRelationshipItem('1')]; spectator.component.$selectedItems.set(mockContent); - expect(spectator.component.$isApplyDisabled()).toBeFalsy(); + expect(spectator.component.$items().length).toBe(1); + }); + + it('should handle single item selection', () => { + const singleItem = mockRelationshipItem('1'); + spectator.component.$selectedItems.set(singleItem); + expect(spectator.component.$items().length).toBe(1); + expect(spectator.component.$items()[0]).toEqual(singleItem); + }); + + it('should handle multiple items selection', () => { + const multipleItems = [mockRelationshipItem('1'), mockRelationshipItem('2')]; + spectator.component.$selectedItems.set(multipleItems); + expect(spectator.component.$items().length).toBe(2); + expect(spectator.component.$items()).toEqual(multipleItems); }); }); @@ -92,62 +160,50 @@ describe('DotSelectExistingContentComponent', () => { const label = spectator.component.$applyLabel(); expect(label).toBe('Apply 2 entries'); }); + + it('should handle empty selection', () => { + spectator.component.$selectedItems.set([]); + const label = spectator.component.$applyLabel(); + expect(label).toBe('Apply 0 entries'); + }); }); - describe('checkIfSelected', () => { + describe('Item Selection', () => { it('should return true when content is in selectedContent array', () => { - // Arrange const testContent = mockRelationshipItem('1'); spectator.component.$selectedItems.set([testContent]); - // Act const result = spectator.component.checkIfSelected(testContent); - // Assert expect(result).toBe(true); }); it('should return false when content is not in selectedContent array', () => { - // Arrange const testContent = mockRelationshipItem('123'); const differentContent = mockRelationshipItem('456'); spectator.component.$selectedItems.set([differentContent]); - // Act const result = spectator.component.checkIfSelected(testContent); - // Assert expect(result).toBe(false); }); it('should return false when selectedContent is empty', () => { - // Arrange const testContent = mockRelationshipItem('123'); spectator.component.$selectedItems.set([]); - // Act const result = spectator.component.checkIfSelected(testContent); - // Assert expect(result).toBe(false); }); - }); - describe('onShowDialog', () => { - it('should call onShowDialog when dialog is shown', fakeAsync(() => { - // Arrange - spectator.component.$visible.set(true); - - spectator.detectChanges(); - - tick(100); - const spy = jest.spyOn(spectator.component, 'onShowDialog'); + it('should handle null selectedContent', () => { + const testContent = mockRelationshipItem('123'); + spectator.component.$selectedItems.set(null); - // Act - spectator.triggerEventHandler(Dialog, 'onShow', null); + const result = spectator.component.checkIfSelected(testContent); - // Assert - expect(spy).toHaveBeenCalled(); - })); + expect(result).toBe(false); + }); }); }); 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 57a0b73d56a1..b4d7079e8d0d 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 @@ -3,11 +3,9 @@ import { Component, computed, inject, - input, model, output, - OnInit, - signal + OnInit } from '@angular/core'; import { ButtonModule } from 'primeng/button'; @@ -83,12 +81,6 @@ export class DotSelectExistingContentComponent implements OnInit { */ readonly #dialogConfig = inject(DynamicDialogConfig); - /** - * A required input that holds the selection mode. - * It is used to determine the selection mode. - */ - $selectionMode = signal(null); - /** * A signal that holds the selected items. * It is used to store the selected content items. @@ -144,9 +136,10 @@ export class DotSelectExistingContentComponent implements OnInit { throw new Error('Selection mode is required'); } - this.store.loadContent(data.contentTypeId); - // TODO: remove this once we have the cardinality set - this.$selectionMode.set(data.selectionMode); + this.store.initLoad({ + contentTypeId: data.contentTypeId, + selectionMode: data.selectionMode + }); } /** @@ -154,15 +147,6 @@ export class DotSelectExistingContentComponent implements OnInit { * It sets the visibility signal to false, hiding the dialog. */ closeDialog() { - this.#dialogRef.close(); - } - - /** - * 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.#dialogRef.close(this.$items()); } 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.spec.ts b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-relationship-field/components/dot-select-existing-content/store/existing-content.store.spec.ts index 490bb4d4fab1..8552d84b5701 100644 --- a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-relationship-field/components/dot-select-existing-content/store/existing-content.store.spec.ts +++ b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-relationship-field/components/dot-select-existing-content/store/existing-content.store.spec.ts @@ -39,9 +39,9 @@ describe('ExistingContentStore', () => { expect(store).toBeTruthy(); }); - describe('Method: loadContent', () => { - it('should set error state when contentId is empty', fakeAsync(() => { - store.loadContent(''); + describe('State Management', () => { + it('should handle empty contentTypeId', fakeAsync(() => { + store.initLoad({ contentTypeId: null, selectionMode: 'single' }); tick(); expect(store.status()).toBe(ComponentStatus.ERROR); @@ -52,7 +52,7 @@ describe('ExistingContentStore', () => { it('should load content successfully', fakeAsync(() => { service.getColumnsAndContent.mockReturnValue(of([mockColumns, mockData])); - store.loadContent('123'); + store.initLoad({ contentTypeId: '123', selectionMode: 'single' }); tick(); expect(store.status()).toBe(ComponentStatus.LOADED); @@ -66,7 +66,7 @@ describe('ExistingContentStore', () => { throwError(() => new Error('Server Error')) ); - store.loadContent('123'); + store.initLoad({ contentTypeId: '123', selectionMode: 'single' }); tick(); expect(store.status()).toBe(ComponentStatus.ERROR); @@ -77,15 +77,8 @@ describe('ExistingContentStore', () => { })); }); - describe('Method: applyInitialState', () => { - it('should reset store to initial state', () => { - // First set some data - service.getColumnsAndContent.mockReturnValue(of([mockColumns, mockData])); - store.loadContent('123'); - - // Then reset - store.applyInitialState(); - + describe('Initial State', () => { + it('should have correct initial state', () => { expect(store.data()).toEqual([]); expect(store.columns()).toEqual([]); expect(store.status()).toBe(ComponentStatus.INIT); @@ -95,51 +88,60 @@ describe('ExistingContentStore', () => { currentPage: 1, rowsPerPage: 50 }); + expect(store.selectionMode()).toBe(null); }); }); - describe('Pagination Methods', () => { - describe('Method: nextPage', () => { - it('should update pagination state for next page', () => { - store.nextPage(); + describe('Pagination', () => { + it('should handle next page', () => { + store.nextPage(); - expect(store.pagination()).toEqual({ - offset: 50, - currentPage: 2, - rowsPerPage: 50 - }); + expect(store.pagination()).toEqual({ + offset: 50, + currentPage: 2, + rowsPerPage: 50 + }); + }); + + it('should handle previous page', () => { + store.nextPage(); + store.previousPage(); + + expect(store.pagination()).toEqual({ + offset: 0, + currentPage: 1, + rowsPerPage: 50 }); }); - describe('Method: previousPage', () => { - it('should update pagination state for previous page', () => { - // First go to next page - store.nextPage(); - // Then go back - store.previousPage(); - - expect(store.pagination()).toEqual({ - offset: 0, - currentPage: 1, - rowsPerPage: 50 - }); + it('should not go to previous page when on first page', () => { + store.previousPage(); + + expect(store.pagination()).toEqual({ + offset: 0, + currentPage: 1, + rowsPerPage: 50 }); }); }); describe('Computed Properties', () => { - it('should compute isLoading correctly', () => { - store.loadContent('123'); + it('should compute loading state correctly', fakeAsync(() => { + service.getColumnsAndContent.mockReturnValue(of([mockColumns, mockData])); + + store.initLoad({ contentTypeId: '123', selectionMode: 'single' }); expect(store.isLoading()).toBe(true); - }); + + tick(); + expect(store.isLoading()).toBe(false); + })); - it('should compute totalPages correctly', fakeAsync(() => { + it('should compute total pages correctly', fakeAsync(() => { service.getColumnsAndContent.mockReturnValue(of([mockColumns, mockData])); - store.loadContent('123'); + store.initLoad({ contentTypeId: '123', selectionMode: 'single' }); tick(); - // With 3 items and 50 items per page, should be 1 page expect(store.totalPages()).toBe(1); })); }); 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 8005b35adb2b..f9b2bb032c66 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 @@ -9,12 +9,13 @@ import { tap, switchMap, filter } from 'rxjs/operators'; import { ComponentStatus } from '@dotcms/dotcms-models'; import { Column } from '@dotcms/edit-content/fields/dot-edit-content-relationship-field/models/column.model'; -import { RelationshipFieldItem } from '@dotcms/edit-content/fields/dot-edit-content-relationship-field/models/relationship.models'; +import { RelationshipFieldItem, SelectionMode } 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: RelationshipFieldItem[]; status: ComponentStatus; + selectionMode: SelectionMode | null; errorMessage: string | null; columns: Column[]; pagination: { @@ -28,6 +29,7 @@ const initialState: ExistingContentState = { data: [], columns: [], status: ComponentStatus.INIT, + selectionMode: null, errorMessage: null, pagination: { offset: 0, @@ -54,10 +56,15 @@ export const ExistingContentStore = signalStore( * 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( + initLoad: rxMethod<{ + contentTypeId: string; + selectionMode: SelectionMode; + }>( pipe( - tap(() => patchState(store, { status: ComponentStatus.LOADING })), - tap((contentTypeId) => { + tap(({ selectionMode }) => + patchState(store, { status: ComponentStatus.LOADING, selectionMode }) + ), + tap(({ contentTypeId }) => { if (!contentTypeId) { patchState(store, { status: ComponentStatus.ERROR, @@ -65,8 +72,8 @@ export const ExistingContentStore = signalStore( }); } }), - filter((contentTypeId) => !!contentTypeId), - switchMap((contentTypeId) => + filter(({ contentTypeId }) => !!contentTypeId), + switchMap(({ contentTypeId }) => relationshipFieldService.getColumnsAndContent(contentTypeId).pipe( tapResponse({ next: ([columns, data]) => { @@ -87,12 +94,6 @@ export const ExistingContentStore = signalStore( ) ) ), - /** - * Applies the initial state for the existing content. - */ - applyInitialState: () => { - patchState(store, initialState); - }, /** * Advances the pagination to the next page and updates the state accordingly. */ diff --git a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-relationship-field/components/header/header.component.html b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-relationship-field/components/header/header.component.html new file mode 100644 index 000000000000..13de25b21ced --- /dev/null +++ b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-relationship-field/components/header/header.component.html @@ -0,0 +1,6 @@ +
+ + + {{ 'dot.file.relationship.dialog.search' | dm }} + +
diff --git a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-relationship-field/components/header/header.component.ts b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-relationship-field/components/header/header.component.ts new file mode 100644 index 000000000000..5a8701a9043e --- /dev/null +++ b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-relationship-field/components/header/header.component.ts @@ -0,0 +1,12 @@ +import { ChangeDetectionStrategy, Component } from '@angular/core'; + +import { DotMessagePipe } from '@dotcms/ui'; + +@Component({ + selector: 'dot-relationship-header', + standalone: true, + imports: [DotMessagePipe], + templateUrl: './header.component.html', + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class HeaderComponent {} 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 df9925aef31c..c31a022b46cc 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 @@ -5,12 +5,12 @@ @let hitText = attributes.hitText; - @@ -91,4 +91,4 @@ } - \ No newline at end of file +
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 9e123b7ed805..c181a1ccc6fd 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,10 +2,14 @@ ::ng-deep { p-table { - .p-datatable-relationship { + .dotTable.p-datatable.p-datatable-relationship { .p-datatable-header { background-color: $color-palette-gray-100; } + .p-datatable-footer { + padding-left: 0; + padding-right: 0; + } .p-datatable-wrapper { border: 1px solid $color-palette-gray-300; border-radius: $border-radius-md; 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 141782f67bab..245ed3db76d8 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 @@ -25,6 +25,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 { HeaderComponent } from './components/header/header.component'; import { PaginationComponent } from './components/pagination/pagination.component'; import { RelationshipFieldItem } from './models/relationship.models'; import { RelationshipFieldStore } from './store/relationship-field.store'; @@ -227,6 +228,9 @@ export class DotEditContentRelationshipFieldComponent implements ControlValueAcc data: { contentTypeId: this.$attributes().contentTypeId, selectionMode: this.store.selectionMode() + }, + templates: { + header: HeaderComponent } }); 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 b77e660f8402..235a47ce602f 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 @@ -80,7 +80,6 @@ export const RelationshipFieldStore = signalStore( * @param {number} cardinality - The cardinality of the relationship field. */ setCardinality(cardinality: number) { - const relationshipType = RELATIONSHIP_OPTIONS[cardinality]; if (!relationshipType) { From ad569502fd2689d5cc6de77f71cd39ae3a918aae Mon Sep 17 00:00:00 2001 From: Nicolas Molina Date: Tue, 24 Dec 2024 11:18:19 -0400 Subject: [PATCH 4/8] chore(edit-content): apply format --- .../dot-select-existing-content.component.spec.ts | 8 ++++---- .../store/existing-content.store.spec.ts | 4 ++-- .../store/existing-content.store.ts | 5 ++++- 3 files changed, 10 insertions(+), 7 deletions(-) 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 index d8ec78e8ca21..4acf4257ae9c 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.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 @@ -102,17 +102,17 @@ describe('DotSelectExistingContentComponent', () => { it('should close dialog with selected items', () => { const mockItems = [mockRelationshipItem('1'), mockRelationshipItem('2')]; spectator.component.$selectedItems.set(mockItems); - + spectator.component.closeDialog(); - + expect(dialogRef.close).toHaveBeenCalledWith(mockItems); }); it('should close dialog with empty array when no items selected', () => { spectator.component.$selectedItems.set([]); - + spectator.component.closeDialog(); - + expect(dialogRef.close).toHaveBeenCalledWith([]); }); }); 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.spec.ts b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-relationship-field/components/dot-select-existing-content/store/existing-content.store.spec.ts index 8552d84b5701..92544fecdf5f 100644 --- a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-relationship-field/components/dot-select-existing-content/store/existing-content.store.spec.ts +++ b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-relationship-field/components/dot-select-existing-content/store/existing-content.store.spec.ts @@ -128,10 +128,10 @@ describe('ExistingContentStore', () => { describe('Computed Properties', () => { it('should compute loading state correctly', fakeAsync(() => { service.getColumnsAndContent.mockReturnValue(of([mockColumns, mockData])); - + store.initLoad({ contentTypeId: '123', selectionMode: 'single' }); expect(store.isLoading()).toBe(true); - + tick(); expect(store.isLoading()).toBe(false); })); 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 f9b2bb032c66..7290647e4e07 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 @@ -9,7 +9,10 @@ import { tap, switchMap, filter } from 'rxjs/operators'; import { ComponentStatus } from '@dotcms/dotcms-models'; import { Column } from '@dotcms/edit-content/fields/dot-edit-content-relationship-field/models/column.model'; -import { RelationshipFieldItem, SelectionMode } from '@dotcms/edit-content/fields/dot-edit-content-relationship-field/models/relationship.models'; +import { + RelationshipFieldItem, + SelectionMode +} 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 { From e94759cc943eb14ca02c0c33ce9f584a336deab9 Mon Sep 17 00:00:00 2001 From: Nicolas Molina Date: Tue, 24 Dec 2024 11:21:53 -0400 Subject: [PATCH 5/8] chore(edit-content): remove header --- .../dot-edit-content-relationship-field.component.ts | 1 - 1 file changed, 1 deletion(-) 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 245ed3db76d8..b5ecdde3235f 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 @@ -214,7 +214,6 @@ export class DotEditContentRelationshipFieldComponent implements ControlValueAcc */ showExistingContentDialog() { this.#dialogRef = this.#dialogService.open(DotSelectExistingContentComponent, { - header: 'sdsd', appendTo: 'body', closeOnEscape: false, draggable: false, From 4ff83cfd37687471448fd9f523a7519d8bc99343 Mon Sep 17 00:00:00 2001 From: Nicolas Molina Date: Thu, 26 Dec 2024 16:39:51 -0400 Subject: [PATCH 6/8] fix(edit-content): adjust modal height and update table column styles --- .../dot-select-existing-content.component.html | 4 ++-- .../dot-edit-content-relationship-field.component.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) 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 bb7886107de0..d77f97fd92f4 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 @@ -59,7 +59,7 @@ @@ -80,7 +80,7 @@
- + @if (selectionMode === 'multiple') { } @else { 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 b5ecdde3235f..8bdd7ca2129b 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 @@ -222,7 +222,7 @@ export class DotEditContentRelationshipFieldComponent implements ControlValueAcc resizable: false, position: 'center', width: '90%', - height: '90vh', + height: '90%', style: { 'max-width': '1040px', 'max-height': '800px' }, data: { contentTypeId: this.$attributes().contentTypeId, From 88da682f9108e70741a04c65c08360730eedbe6f Mon Sep 17 00:00:00 2001 From: Nicolas Molina Date: Fri, 27 Dec 2024 10:31:35 -0400 Subject: [PATCH 7/8] chore(edit-content): fix unit tests --- ...-select-existing-content.component.spec.ts | 115 +++++++++++++----- .../store/existing-content.store.spec.ts | 12 +- .../store/existing-content.store.ts | 10 +- 3 files changed, 102 insertions(+), 35 deletions(-) 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 index 4acf4257ae9c..41c060e8e055 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.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 @@ -10,8 +10,20 @@ import { MockDotMessageService } from '@dotcms/utils-testing'; import { DotSelectExistingContentComponent } from './dot-select-existing-content.component'; import { ExistingContentStore } from './store/existing-content.store'; +import { Column } from '../../models/column.model'; import { RelationshipFieldService } from '../../services/relationship-field.service'; +const mockColumns: Column[] = [ + { field: 'title', header: 'Title' }, + { field: 'modDate', header: 'Mod Date' } +]; + +const mockData: RelationshipFieldItem[] = [ + { id: '1', title: 'Content 1', language: '1', modDate: new Date().toISOString() }, + { id: '2', title: 'Content 2', language: '1', modDate: new Date().toISOString() }, + { id: '3', title: 'Content 3', language: '1', modDate: new Date().toISOString() } +]; + describe('DotSelectExistingContentComponent', () => { let spectator: Spectator; let store: InstanceType; @@ -43,7 +55,7 @@ describe('DotSelectExistingContentComponent', () => { componentProviders: [ExistingContentStore], providers: [ mockProvider(RelationshipFieldService, { - getContent: jest.fn(() => of([])) + getColumnsAndContent: jest.fn(() => of([mockColumns, mockData])) }), { provide: DotMessageService, useValue: messageServiceMock }, { provide: DynamicDialogRef, useValue: { close: jest.fn() } }, @@ -65,36 +77,15 @@ describe('DotSelectExistingContentComponent', () => { }); describe('Initialization', () => { - it('should initialize with required configuration', () => { - const spy = jest.spyOn(store, 'initLoad'); - spectator.component.ngOnInit(); - expect(spy).toHaveBeenCalledWith({ - contentTypeId: 'test-content-type-id', - selectionMode: 'multiple' - }); - }); - - it('should throw error when selectionMode is missing', () => { - const invalidConfig = createComponentFactory({ - component: DotSelectExistingContentComponent, - componentProviders: [ExistingContentStore], - providers: [ - mockProvider(RelationshipFieldService, { - getContent: jest.fn(() => of([])) - }), - { provide: DotMessageService, useValue: messageServiceMock }, - { provide: DynamicDialogRef, useValue: { close: jest.fn() } }, - { - provide: DynamicDialogConfig, - useValue: { data: { contentTypeId: 'test-id' } } - } - ] + describe('with valid configuration', () => { + it('should initialize with required configuration', () => { + const spy = jest.spyOn(store, 'initLoad'); + spectator.component.ngOnInit(); + expect(spy).toHaveBeenCalledWith({ + contentTypeId: 'test-content-type-id', + selectionMode: 'multiple' + }); }); - - expect(() => { - const component = invalidConfig(); - component.detectChanges(); - }).toThrow('Selection mode is required'); }); }); @@ -207,3 +198,67 @@ describe('DotSelectExistingContentComponent', () => { }); }); }); + +describe('DotSelectExistingContentComponent when selectionMode is missing', () => { + let spectator: Spectator; + + const messageServiceMock = new MockDotMessageService({ + 'dot.file.relationship.dialog.apply.one.entry': 'Apply 1 entry', + 'dot.file.relationship.dialog.apply.entries': 'Apply {0} entries' + }); + + const createComponentWithInvalidConfig = createComponentFactory({ + component: DotSelectExistingContentComponent, + componentProviders: [ExistingContentStore], + providers: [ + mockProvider(RelationshipFieldService, { + getColumnsAndContent: jest.fn(() => of([mockColumns, mockData])) + }), + { provide: DotMessageService, useValue: messageServiceMock }, + { provide: DynamicDialogRef, useValue: { close: jest.fn() } }, + { + provide: DynamicDialogConfig, + useValue: { data: { contentTypeId: 'test-id' } } + } + ] + }); + + it('should throw error when selectionMode is missing', () => { + expect(() => { + spectator = createComponentWithInvalidConfig(); + spectator.detectChanges(); + }).toThrow('Selection mode is required'); + }); +}); + +describe('DotSelectExistingContentComponent when contentTypeId is missing', () => { + let spectator: Spectator; + + const messageServiceMock = new MockDotMessageService({ + 'dot.file.relationship.dialog.apply.one.entry': 'Apply 1 entry', + 'dot.file.relationship.dialog.apply.entries': 'Apply {0} entries' + }); + + const createComponentWithInvalidConfig = createComponentFactory({ + component: DotSelectExistingContentComponent, + componentProviders: [ExistingContentStore], + providers: [ + mockProvider(RelationshipFieldService, { + getColumnsAndContent: jest.fn(() => of([mockColumns, mockData])) + }), + { provide: DotMessageService, useValue: messageServiceMock }, + { provide: DynamicDialogRef, useValue: { close: jest.fn() } }, + { + provide: DynamicDialogConfig, + useValue: { data: { selectionMode: 'multiple' } } + } + ] + }); + + it('should throw error when contentTypeId is missing', () => { + expect(() => { + spectator = createComponentWithInvalidConfig(); + spectator.detectChanges(); + }).toThrow('Content type id is required'); + }); +}); 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.spec.ts b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-relationship-field/components/dot-select-existing-content/store/existing-content.store.spec.ts index 92544fecdf5f..45b755d9edd7 100644 --- a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-relationship-field/components/dot-select-existing-content/store/existing-content.store.spec.ts +++ b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-relationship-field/components/dot-select-existing-content/store/existing-content.store.spec.ts @@ -1,8 +1,10 @@ import { SpyObject, mockProvider } from '@ngneat/spectator/jest'; -import { of, throwError } from 'rxjs'; +import { Observable, of, throwError } from 'rxjs'; import { TestBed, fakeAsync, tick } from '@angular/core/testing'; +import { delay } from 'rxjs/operators'; + import { ComponentStatus } from '@dotcms/dotcms-models'; import { RelationshipFieldService } from '@dotcms/edit-content/fields/dot-edit-content-relationship-field/services/relationship-field.service'; @@ -127,12 +129,16 @@ describe('ExistingContentStore', () => { describe('Computed Properties', () => { it('should compute loading state correctly', fakeAsync(() => { - service.getColumnsAndContent.mockReturnValue(of([mockColumns, mockData])); + const mockObservable = of([mockColumns, mockData]).pipe(delay(100)) as Observable< + [Column[], RelationshipFieldItem[]] + >; + + service.getColumnsAndContent.mockReturnValue(mockObservable); store.initLoad({ contentTypeId: '123', selectionMode: 'single' }); expect(store.isLoading()).toBe(true); - tick(); + tick(100); expect(store.isLoading()).toBe(false); })); 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 7290647e4e07..2c27e88d1fd9 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 @@ -113,11 +113,17 @@ export const ExistingContentStore = signalStore( * Moves the pagination to the previous page and updates the state accordingly. */ previousPage: () => { + const { currentPage, offset, rowsPerPage } = store.pagination(); + + if (currentPage === 1) { + return; + } + patchState(store, { pagination: { ...store.pagination(), - offset: store.pagination().offset - store.pagination().rowsPerPage, - currentPage: store.pagination().currentPage - 1 + offset: offset - rowsPerPage, + currentPage: currentPage - 1 } }); } From f818cd978318ba21c7b2ed20a8c807c8e33313b0 Mon Sep 17 00:00:00 2001 From: Nicolas Molina Date: Fri, 27 Dec 2024 11:07:51 -0400 Subject: [PATCH 8/8] chore(edit-content): Fix UI issues --- .../dot-select-existing-content.component.html | 6 ------ .../dot-edit-content-relationship-field.component.ts | 1 + 2 files changed, 1 insertion(+), 6 deletions(-) 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 d77f97fd92f4..1c185109de44 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 @@ -73,9 +73,6 @@ } - - {{ 'dot.file.relationship.dialog.menu.column' | dm }} - @@ -92,9 +89,6 @@

{{ item[column.field] }}

} - - -
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 8bdd7ca2129b..34cb07e40867 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 @@ -223,6 +223,7 @@ export class DotEditContentRelationshipFieldComponent implements ControlValueAcc position: 'center', width: '90%', height: '90%', + maskStyleClass: 'p-dialog-mask-dynamic', style: { 'max-width': '1040px', 'max-height': '800px' }, data: { contentTypeId: this.$attributes().contentTypeId,