Skip to content

Commit

Permalink
chore(edit-content): Basic Relationship field Layout (#30794)
Browse files Browse the repository at this point in the history
### Parent Issue

#30515

### Proposed Changes
This pull request introduces a new `RELATIONSHIP` field type in the
dotCMS content editing module. The most important changes include the
addition of the `DotEditContentRelationshipFieldComponent`, updates to
the `FIELD_TYPES` enum, and the creation of supporting components and
stores for managing relationship fields.

### Introduction of `RELATIONSHIP` Field Type:

*
[`core-web/libs/edit-content/src/lib/models/dot-edit-content-field.enum.ts`](diffhunk://#diff-aa72329fa1c18891b5d44042ac6ae09ff3d915621f16a084982f87d068c2453aL33-R34):
Added `RELATIONSHIP` to the `FIELD_TYPES` enum.

### New Components for Relationship Fields:

*
[`core-web/libs/edit-content/src/lib/fields/dot-edit-content-relationship-field/dot-edit-content-relationship-field.component.ts`](diffhunk://#diff-5cb2225620beb3baaf219cbfa2241aa934812de8ece62e1d4f7e7747f5f15416R1-R129):
Created `DotEditContentRelationshipFieldComponent` to handle
relationship fields.
*
[`core-web/libs/edit-content/src/lib/fields/dot-edit-content-relationship-field/components/dot-select-existing-content/dot-select-existing-content.component.ts`](diffhunk://#diff-14d3d666b0a074fe26436013ebf4abe96e518e28e9799b85b393d14b82ac528bR1-R49):
Added `DotSelectExistingContentComponent` for selecting existing
content.

### Supporting Stores and Utilities:

*
[`core-web/libs/edit-content/src/lib/fields/dot-edit-content-relationship-field/store/relationship-field.store.ts`](diffhunk://#diff-a2c980da63c75c9dc2cd899475beab89cb79492b1dc0819574fc380e856e7ed1R1-R32):
Created `RelationshipFieldStore` to manage state for relationship
fields.
*
[`core-web/libs/edit-content/src/lib/fields/dot-edit-content-relationship-field/components/dot-select-existing-content/store/existing-content.store.ts`](diffhunk://#diff-1c1758cf3c19df1a6e75e613761290b504a1d26e8521af1492593e1825969b63R1-R57):
Added `ExistingContentStore` to manage state for selecting existing
content.

### Template and Style Updates:

*
[`core-web/libs/edit-content/src/lib/components/dot-edit-content-field/dot-edit-content-field.component.html`](diffhunk://#diff-685da2f87189fff825b7aec2666cfbabdb33c40f8f9d8c82df77ebb8652ccd4aR145-R152):
Updated template to include `RELATIONSHIP` field type.
*
[`core-web/libs/edit-content/src/lib/fields/dot-edit-content-relationship-field/components/dot-select-existing-content/dot-select-existing-content.component.html`](diffhunk://#diff-ba794904cc95635d9efca07b7e2bfeb948b5b0bb0c8e1e97c777c615398ba011R1-R88):
Added template for selecting existing content.
*
[`core-web/libs/edit-content/src/lib/fields/dot-edit-content-relationship-field/components/dot-select-existing-content/dot-select-existing-content.component.scss`](diffhunk://#diff-778cca685214b49b5445efc5613609b68214a24052f4ed77408caeb0c9e6ffb2R1-R20):
Added styles for selecting existing content.

### Checklist
- [x] Tests
- [x] Translations
- [x] Security Implications Contemplated (add notes if applicable)



https://github.com/user-attachments/assets/0a0f9f23-1949-44c2-ac06-e4d62f8dbaf3
  • Loading branch information
nicobytes authored Dec 3, 2024
1 parent 83de9e0 commit 7a64a00
Show file tree
Hide file tree
Showing 21 changed files with 819 additions and 22 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ export interface DotCMSContentTypeField {
variable: string;
forceIncludeInApi?: boolean;
fieldContentTypeProperties?: string[];
skipRelationshipCreation?: boolean;
metadata?: { [key: string]: string | number | boolean };
}

Expand Down
12 changes: 11 additions & 1 deletion core-web/libs/dotcms-scss/shared/_colors.scss
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,17 @@ $success: $color-accessible-text-green;
--color-palette-primary-800: hsl(var(--color-primary-h) var(--color-primary-s) 27%);
--color-palette-primary-900: hsl(var(--color-primary-h) var(--color-primary-s) 21%);

--color-palette-primary-op-10: hsla(var(--color-primary-h), var(--color-primary-s), 60%, 0.1);
--primary-100: var(--color-palette-primary-100);
--primary-200: var(--color-palette-primary-200);
--primary-300: var(--color-palette-primary-300);
--primary-400: var(--color-palette-primary-400);
--primary-500: var(--color-palette-primary-500);
--primary-600: var(--color-palette-primary-600);
--primary-700: var(--color-palette-primary-700);
--primary-800: var(--color-palette-primary-800);
--primary-900: var(--color-palette-primary-900);

--color-palette-primary-op-10: var(--color-palette-primary-op-10);
--color-palette-primary-op-20: hsla(var(--color-primary-h), var(--color-primary-s), 60%, 0.2);
--color-palette-primary-op-30: hsla(var(--color-primary-h), var(--color-primary-s), 60%, 0.3);
--color-palette-primary-op-40: hsla(var(--color-primary-h), var(--color-primary-s), 60%, 0.4);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,14 @@
[field]="field" />
}
}
@case (fieldTypes.RELATIONSHIP) {
@defer (on immediate) {
<dot-edit-content-relationship-field
[formControlName]="field.variable"
[attr.data-testId]="'field-' + field.variable"
[field]="field" />
}
}
}
@if (field.hint) {
<small [attr.data-testId]="'hint-' + field.variable">{{ field.hint }}</small>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import { DotEditContentJsonFieldComponent } from '../../fields/dot-edit-content-
import { DotEditContentKeyValueComponent } from '../../fields/dot-edit-content-key-value/dot-edit-content-key-value.component';
import { DotEditContentMultiSelectFieldComponent } from '../../fields/dot-edit-content-multi-select-field/dot-edit-content-multi-select-field.component';
import { DotEditContentRadioFieldComponent } from '../../fields/dot-edit-content-radio-field/dot-edit-content-radio-field.component';
import { DotEditContentRelationshipFieldComponent } from '../../fields/dot-edit-content-relationship-field/dot-edit-content-relationship-field.component';
import { DotEditContentSelectFieldComponent } from '../../fields/dot-edit-content-select-field/dot-edit-content-select-field.component';
import { DotEditContentTagFieldComponent } from '../../fields/dot-edit-content-tag-field/dot-edit-content-tag-field.component';
import { DotEditContentTextAreaComponent } from '../../fields/dot-edit-content-text-area/dot-edit-content-text-area.component';
Expand Down Expand Up @@ -73,6 +74,9 @@ declare module '@tiptap/core' {
const FIELD_TYPES_COMPONENTS: Record<FIELD_TYPES, Type<unknown> | DotEditFieldTestBed> = {
// We had to use unknown because components have different types.
[FIELD_TYPES.TEXT]: DotEditContentTextFieldComponent,
[FIELD_TYPES.RELATIONSHIP]: {
component: DotEditContentRelationshipFieldComponent
},
[FIELD_TYPES.FILE]: {
component: DotEditContentFileFieldComponent,
providers: [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,25 @@ import { ControlContainer, ReactiveFormsModule } from '@angular/forms';

import { BlockEditorModule } from '@dotcms/block-editor';
import { DotCMSContentlet, DotCMSContentTypeField } from '@dotcms/dotcms-models';
import { DotEditContentBinaryFieldComponent } from '@dotcms/edit-content/fields/dot-edit-content-binary-field/dot-edit-content-binary-field.component';
import { DotEditContentCalendarFieldComponent } from '@dotcms/edit-content/fields/dot-edit-content-calendar-field/dot-edit-content-calendar-field.component';
import { DotEditContentCategoryFieldComponent } from '@dotcms/edit-content/fields/dot-edit-content-category-field/dot-edit-content-category-field.component';
import { DotEditContentCheckboxFieldComponent } from '@dotcms/edit-content/fields/dot-edit-content-checkbox-field/dot-edit-content-checkbox-field.component';
import { DotEditContentCustomFieldComponent } from '@dotcms/edit-content/fields/dot-edit-content-custom-field/dot-edit-content-custom-field.component';
import { DotEditContentFileFieldComponent } from '@dotcms/edit-content/fields/dot-edit-content-file-field/dot-edit-content-file-field.component';
import { DotEditContentHostFolderFieldComponent } from '@dotcms/edit-content/fields/dot-edit-content-host-folder-field/dot-edit-content-host-folder-field.component';
import { DotEditContentJsonFieldComponent } from '@dotcms/edit-content/fields/dot-edit-content-json-field/dot-edit-content-json-field.component';
import { DotEditContentKeyValueComponent } from '@dotcms/edit-content/fields/dot-edit-content-key-value/dot-edit-content-key-value.component';
import { DotEditContentMultiSelectFieldComponent } from '@dotcms/edit-content/fields/dot-edit-content-multi-select-field/dot-edit-content-multi-select-field.component';
import { DotEditContentRadioFieldComponent } from '@dotcms/edit-content/fields/dot-edit-content-radio-field/dot-edit-content-radio-field.component';
import { DotEditContentRelationshipFieldComponent } from '@dotcms/edit-content/fields/dot-edit-content-relationship-field/dot-edit-content-relationship-field.component';
import { DotEditContentSelectFieldComponent } from '@dotcms/edit-content/fields/dot-edit-content-select-field/dot-edit-content-select-field.component';
import { DotEditContentTagFieldComponent } from '@dotcms/edit-content/fields/dot-edit-content-tag-field/dot-edit-content-tag-field.component';
import { DotEditContentTextAreaComponent } from '@dotcms/edit-content/fields/dot-edit-content-text-area/dot-edit-content-text-area.component';
import { DotEditContentTextFieldComponent } from '@dotcms/edit-content/fields/dot-edit-content-text-field/dot-edit-content-text-field.component';
import { DotEditContentWYSIWYGFieldComponent } from '@dotcms/edit-content/fields/dot-edit-content-wysiwyg-field/dot-edit-content-wysiwyg-field.component';
import { DotFieldRequiredDirective } from '@dotcms/ui';

import { DotEditContentBinaryFieldComponent } from '../../fields/dot-edit-content-binary-field/dot-edit-content-binary-field.component';
import { DotEditContentCalendarFieldComponent } from '../../fields/dot-edit-content-calendar-field/dot-edit-content-calendar-field.component';
import { DotEditContentCategoryFieldComponent } from '../../fields/dot-edit-content-category-field/dot-edit-content-category-field.component';
import { DotEditContentCheckboxFieldComponent } from '../../fields/dot-edit-content-checkbox-field/dot-edit-content-checkbox-field.component';
import { DotEditContentCustomFieldComponent } from '../../fields/dot-edit-content-custom-field/dot-edit-content-custom-field.component';
import { DotEditContentFileFieldComponent } from '../../fields/dot-edit-content-file-field/dot-edit-content-file-field.component';
import { DotEditContentHostFolderFieldComponent } from '../../fields/dot-edit-content-host-folder-field/dot-edit-content-host-folder-field.component';
import { DotEditContentJsonFieldComponent } from '../../fields/dot-edit-content-json-field/dot-edit-content-json-field.component';
import { DotEditContentKeyValueComponent } from '../../fields/dot-edit-content-key-value/dot-edit-content-key-value.component';
import { DotEditContentMultiSelectFieldComponent } from '../../fields/dot-edit-content-multi-select-field/dot-edit-content-multi-select-field.component';
import { DotEditContentRadioFieldComponent } from '../../fields/dot-edit-content-radio-field/dot-edit-content-radio-field.component';
import { DotEditContentSelectFieldComponent } from '../../fields/dot-edit-content-select-field/dot-edit-content-select-field.component';
import { DotEditContentTagFieldComponent } from '../../fields/dot-edit-content-tag-field/dot-edit-content-tag-field.component';
import { DotEditContentTextAreaComponent } from '../../fields/dot-edit-content-text-area/dot-edit-content-text-area.component';
import { DotEditContentTextFieldComponent } from '../../fields/dot-edit-content-text-field/dot-edit-content-text-field.component';
import { DotEditContentWYSIWYGFieldComponent } from '../../fields/dot-edit-content-wysiwyg-field/dot-edit-content-wysiwyg-field.component';
import { CALENDAR_FIELD_TYPES } from '../../models/dot-edit-content-field.constant';
import { FIELD_TYPES } from '../../models/dot-edit-content-field.enum';

Expand Down Expand Up @@ -54,10 +55,10 @@ import { FIELD_TYPES } from '../../models/dot-edit-content-field.enum';
DotEditContentCategoryFieldComponent,
DotFieldRequiredDirective,
BlockEditorModule,
DotEditContentBinaryFieldComponent,
DotEditContentKeyValueComponent,
DotEditContentWYSIWYGFieldComponent,
DotEditContentFileFieldComponent
DotEditContentFileFieldComponent,
DotEditContentRelationshipFieldComponent
]
})
export class DotEditContentFieldComponent {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,5 +62,6 @@ export const resolutionValue: Record<FIELD_TYPES, FnResolutionValue> = {
}

return field.defaultValue ?? [];
}
},
[FIELD_TYPES.RELATIONSHIP]: defaultResolutionFn
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
@let currentPage = $currentPage();
@let totalPages = $totalPages();

<div class="flex justify-content-center gap-3">
<span class="text-primary-500 flex align-items-center">
{{ currentPage }} of {{ totalPages }}
</span>
<div class="flex justify-content-center gap-1">
<p-button
type="button"
(onClick)="previousPage.emit()"
icon="pi pi-angle-left"
[rounded]="true"
[text]="true"
[disabled]="currentPage === 1"
size="small" />
<p-button
type="button"
(onClick)="nextPage.emit()"
icon="pi pi-angle-right"
[rounded]="true"
[text]="true"
[disabled]="currentPage === totalPages"
size="small" />
</div>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { Component, input, output } from '@angular/core';

import { ButtonModule } from 'primeng/button';

@Component({
selector: 'dot-pagination',
standalone: true,
imports: [ButtonModule],
templateUrl: './pagination.component.html'
})
export class PaginationComponent {
/**
* A signal that holds the total number of pages.
* It is used to display the total number of pages in the pagination component.
*/
$totalPages = input.required<number>({ alias: 'totalPages' });

/**
* A signal that holds the current page number.
* It is used to display the current page number in the pagination component.
*/
$currentPage = input.required<number>({ alias: 'currentPage' });

/**
* An output signal that emits when the previous page button is clicked.
*/
previousPage = output<void>();

/**
* An output signal that emits when the next page button is clicked.
*/
nextPage = output<void>();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<p-inputGroup size="small">
<input
#searchInput
pInputText
type="search"
[placeholder]="'dot.file.relationship.dialog.search' | dm"
(input)="onSearch.emit(searchInput.value)" />
<p-button
(onClick)="op.toggle($event)"
size="small"
type="button"
icon="pi pi-sliders-h"
[text]="true" />
</p-inputGroup>

<p-overlayPanel #op>
<form class="w-11rem" [formGroup]="form">
<div class="flex flex-column gap-2">
<div class="flex flex-column gap-2">
<p-dropdown
styleClass="w-full"
[options]="[]"
formControlName="language"
optionLabel="language" />

<p-dropdown
styleClass="w-full"
[options]="[]"
formControlName="site"
optionLabel="Site or Host" />
</div>

<div class="flex justify-content-end gap-2">
<p-button
(onClick)="op.toggle($event)"
label="Clear all"
type="button"
text="true"
severity="primary"
outline="true" />

<p-button
(onClick)="op.toggle($event)"
label="Search"
type="button"
severity="primary" />
</div>
</div>
</form>
</p-overlayPanel>
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { Component, inject, output } from '@angular/core';
import { FormBuilder, ReactiveFormsModule } from '@angular/forms';

import { ButtonModule } from 'primeng/button';
import { DropdownModule } from 'primeng/dropdown';
import { InputGroupModule } from 'primeng/inputgroup';
import { InputTextModule } from 'primeng/inputtext';
import { OverlayPanelModule } from 'primeng/overlaypanel';

import { DotMessagePipe } from '@dotcms/ui';

@Component({
selector: 'dot-search',
standalone: true,
imports: [
InputTextModule,
ButtonModule,
InputGroupModule,
OverlayPanelModule,
DotMessagePipe,
DropdownModule,
ReactiveFormsModule
],
templateUrl: './search.compoment.html'
})
export class SearchComponent {
/**
* An output signal that emits when the search input is changed.
*/
onSearch = output<string>();

/**
* Injects FormBuilder to create form control groups.
*/
readonly #formBuilder = inject(FormBuilder);

/**
* Initializes the form group with default values for language and site.
*/
readonly form = this.#formBuilder.group({
language: [''],
site: ['']
});
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
@let data = store.data();
@let pagination = store.pagination();

<p-dialog
[header]="'dot.file.relationship.dialog.select.existing.content' | dm"
[modal]="true"
[(visible)]="$visible"
dataKey="id"
appendTo="body"
width="90%"
[style]="{ width: '90%', 'max-width': '1040px' }">
<p-table
#datatable
[value]="data"
selectionMode="multiple"
[(selection)]="$selectedItems"
[first]="pagination.offset"
[loading]="store.isLoading()"
[paginator]="true"
[rows]="pagination.rowsPerPage"
[globalFilterFields]="['title', 'step', 'description']"
styleClass="p-datatable-sm p-datatable-existing-content">
<ng-template pTemplate="caption">
<div class="flex justify-content-between align-items-center w-full">
<div class="flex align-items-center gap-2">
<div class="flex-none">
<dot-search (onSearch)="datatable.filterGlobal($event, 'contains')" />
</div>
<div class="flex-grow-1">
<p class="text-primary-500">
{{
'dot.file.relationship.dialog.per.page'
| dm: [pagination.rowsPerPage.toString()]
}}
</p>
</div>
</div>
<div>
<dot-pagination
(nextPage)="store.nextPage()"
(previousPage)="store.previousPage()"
[totalPages]="store.totalPages()"
[currentPage]="pagination.currentPage" />
</div>
</div>
</ng-template>
<ng-template pTemplate="header" styleClass="relative">
<tr>
<th scope="col" style="width: 4rem"><p-tableHeaderCheckbox /></th>
<th scope="col" pSortableColumn="title">
Title
<p-sortIcon field="title" />
</th>
<th scope="col" pSortableColumn="step">
Step
<p-sortIcon field="step" />
</th>
<th scope="col" pSortableColumn="description">
Description
<p-sortIcon field="description" />
</th>
<th scope="col" pSortableColumn="lastUpdate">
Last Update
<p-sortIcon field="lastUpdate" />
</th>
<th scope="col">Menu</th>
</tr>
</ng-template>
<ng-template pTemplate="emptymessage">
<tr>
<td colspan="6">
<div class="flex p-2 gap-2 justify-content-center">
<p class="text-500">Relate content by clicking on the Plus Button</p>
</div>
</td>
</tr>
</ng-template>
<ng-template pTemplate="body" let-item>
<tr>
<td>
<p-tableCheckbox [value]="item" />
</td>
<td class="max-w-12rem">
<p class="truncate-text">{{ item.title }}</p>
</td>
<td>{{ item.step }}</td>
<td class="max-w-12rem">
<p class="truncate-text">{{ item.description }}</p>
</td>
<td>{{ item.lastUpdate | date }}</td>
<td>
<i class="pi pi-pencil"></i>
</td>
</tr>
</ng-template>
</p-table>
<ng-template pTemplate="footer">
<div class="flex justify-content-between">
<div>
<p class="text-primary-500">
{{
'dot.file.relationship.dialog.selected.items'
| dm: [$selectedItems().length.toString()]
}}
</p>
</div>
<div>
<p-button
[label]="'Cancel' | dm"
[outlined]="true"
(onClick)="closeDialog()"
[text]="true"
severity="primary" />
<p-button [disabled]="$isApplyDisabled()" [label]="$applyLabel()" />
</div>
</div>
</ng-template>
</p-dialog>
Loading

0 comments on commit 7a64a00

Please sign in to comment.