Skip to content

Commit

Permalink
chore(edit-content): enhance dialog and table functionality (#30901)
Browse files Browse the repository at this point in the history
### Parent Issue

#30523

### Proposed Changes

This pull request includes changes to the
`dot-select-existing-content.component.html` and
`dot-select-existing-content.component.ts` files to improve the user
interface and functionality of the content selection dialog. The most
important changes include adding a header template to the dialog, making
the dialog non-draggable, and introducing a method to check if an item
is selected.

Improvements to user interface:

*
[`dot-select-existing-content.component.html`](diffhunk://#diff-ba794904cc95635d9efca07b7e2bfeb948b5b0bb0c8e1e97c777c615398ba011R4-R22):
Added a header template to the dialog with a search icon and title.
*
[`dot-select-existing-content.component.html`](diffhunk://#diff-ba794904cc95635d9efca07b7e2bfeb948b5b0bb0c8e1e97c777c615398ba011R4-R22):
Made the dialog non-draggable and updated the style to include a height
property.

Functionality enhancements:

*
[`dot-select-existing-content.component.html`](diffhunk://#diff-ba794904cc95635d9efca07b7e2bfeb948b5b0bb0c8e1e97c777c615398ba011L79-R89):
Added a conditional class to highlight selected rows in the table.
*
[`dot-select-existing-content.component.ts`](diffhunk://#diff-14d3d666b0a074fe26436013ebf4abe96e518e28e9799b85b393d14b82ac528bR115-R123):
Introduced the `checkIfSelected` method to determine if an item is
selected.

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

### Screenshots

- The title should be "Search" and not "Select existing content".

<img width="1098" alt="Screenshot 2024-12-09 at 8 45 05 PM"
src="https://github.com/user-attachments/assets/1bacf286-8f9f-488d-b838-db292794663b">

- If the user make a search, close the dialog and open it again the
search input and the entries button are not clean, keeps the state.


https://github.com/user-attachments/assets/b92a8307-c43c-4f4c-b14f-fb3210c91b6a

- Following patterns, Dialog should keep the same height. when the user
perform a search with a couple of rows the height change


https://github.com/user-attachments/assets/286c9e15-29d3-4de4-ac7a-b9972928a973


- Form figma: The selected items will have an active state background.
- Dialog should not be draggable.


https://github.com/user-attachments/assets/767a049a-a713-4365-87b1-44c2c8f648ba
  • Loading branch information
nicobytes authored Dec 11, 2024
1 parent 8af2d78 commit cadf653
Show file tree
Hide file tree
Showing 9 changed files with 221 additions and 117 deletions.
2 changes: 1 addition & 1 deletion core-web/apps/dotcms-binary-field-builder/project.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
{
"type": "initial",
"maximumWarning": "500kb",
"maximumError": "2mb"
"maximumError": "2.5mb"
},
{
"type": "anyComponentStyle",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,121 +1,137 @@
@let data = store.data();
@let pagination = store.pagination();

<p-dialog
[header]="'dot.file.relationship.dialog.select.existing.content' | dm"
[modal]="true"
[(visible)]="$visible"
(onHide)="emitSelectedItems()"
dataKey="id"
appendTo="body"
width="90%"
[style]="{ width: '90%', 'max-width': '1040px' }">
<p-table
#datatable
[value]="data"
selectionMode="multiple"
[(selection)]="$selectedItems"
[loading]="store.isLoading()"
[paginator]="true"
[first]="pagination.offset"
[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')" />
@if ($visible()) {
@defer {
<p-dialog
[modal]="true"
[(visible)]="$visible"
(onShow)="onShowDialog()"
(onHide)="emitSelectedItems()"
[draggable]="false"
dataKey="id"
appendTo="body"
width="90%"
[style]="{ width: '90%', 'max-width': '1040px', height: '90vh' }">
<ng-template pTemplate="header">
<div class="flex items-center justify-center gap-2">
<i class="pi pi-search"></i>
<span class="p-dialog-title">
{{ 'dot.file.relationship.dialog.search' | dm }}
</span>
</div>
</ng-template>
<p-table
#datatable
[value]="data"
selectionMode="multiple"
[(selection)]="$selectedItems"
[loading]="store.isLoading()"
[paginator]="true"
[first]="pagination.offset"
[rows]="pagination.rowsPerPage"
[globalFilterFields]="['title', 'step', 'description']"
styleClass="dotTable 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>
<dot-pagination
[currentPageReportLayout]="'left'"
(nextPage)="store.nextPage()"
(previousPage)="store.previousPage()"
[totalPages]="store.totalPages()"
[currentPage]="pagination.currentPage" />
</div>
<div class="flex-grow-1">
</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">
{{ 'dot.file.relationship.dialog.search.empty.content' | dm }}
</p>
</div>
</td>
</tr>
</ng-template>
<ng-template pTemplate="body" let-item>
<tr [class.p-highlight]="checkIfSelected(item)">
<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.per.page'
| dm: [pagination.rowsPerPage.toString()]
'dot.file.relationship.dialog.selected.items'
| dm: [$selectedItems().length.toString()]
}}
</p>
</div>
</div>
<dot-pagination
[currentPageReportLayout]="'left'"
(nextPage)="store.nextPage()"
(previousPage)="store.previousPage()"
[totalPages]="store.totalPages()"
[currentPage]="pagination.currentPage" />
</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>
<p-button
[label]="'Cancel' | dm"
[outlined]="true"
(onClick)="closeDialog()"
[text]="true"
severity="primary" />
<p-button
[disabled]="$isApplyDisabled()"
(onClick)="closeDialog()"
[label]="$applyLabel()" />
</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()"
(onClick)="closeDialog()"
[label]="$applyLabel()" />
</div>
</div>
</ng-template>
</p-dialog>
</div>
</ng-template>
</p-dialog>
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,10 @@

::ng-deep {
p-table {
.p-datatable-sm.p-datatable-existing-content {
.p-datatable-existing-content {
.p-datatable-header {
background-color: $white;
border: 0px;
padding-left: 0px;
padding-right: 0px;
}
.p-datatable-table {
border: 1px solid $color-palette-gray-300;
Expand All @@ -24,5 +22,10 @@
height: auto;
}
}
.dotTable.p-datatable {
.p-datatable-tbody > tr > td:first-child:has(p-tableCheckbox) + td {
padding: 0 $spacing-1;
}
}
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { Spectator, createComponentFactory } from '@ngneat/spectator/jest';

import { fakeAsync, tick } from '@angular/core/testing';

import { Dialog } from 'primeng/dialog';

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';
Expand Down Expand Up @@ -85,4 +89,61 @@ describe('DotSelectExistingContentComponent', () => {
expect(label).toBe('Apply 2 entries');
});
});

describe('checkIfSelected', () => {
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');

// Act
spectator.triggerEventHandler(Dialog, 'onShow', null);

// Assert
expect(spy).toHaveBeenCalled();
}));
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -112,4 +112,21 @@ export class DotSelectExistingContentComponent {
emitSelectedItems() {
this.onSelectItems.emit(this.$selectedItems());
}

/**
* Checks if an item is selected.
* @param item - The item to check.
* @returns True if the item is selected, false otherwise.
*/
checkIfSelected(item: RelationshipFieldItem) {
return this.$selectedItems().some((selectedItem) => selectedItem.id === item.id);
}

/**
* Shows the existing content dialog and loads the content.
*/
onShowDialog() {
this.store.applyInitialState();
this.store.loadContent();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,12 @@ 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.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

<p-table
[value]="store.data()"
styleClass="p-datatable-sm p-datatable-relationship"
styleClass="dotTable p-datatable-relationship"
[paginator]="true"
[first]="pagination.offset"
[rows]="pagination.rowsPerPage"
Expand Down
Loading

0 comments on commit cadf653

Please sign in to comment.