From 14430e369283bb3c4c96a8bbb0de6d1a42b522cb Mon Sep 17 00:00:00 2001 From: Arcadio Quintero Date: Fri, 25 Oct 2024 16:40:14 -0400 Subject: [PATCH] feat(edit-content) improve the ui of the autocomplete (#30452) #30222 This pull request includes changes to the `dot-edit-content-field` and `dot-edit-content-wysiwyg-field` components in the `core-web` library. The updates focus on adding new providers, improving the autocomplete functionality, and enhancing the styling for better UI consistency. ### Improvements to `dot-edit-content-field` component: * Added `DotEditContentStore` provider and used the `signal` function to manage the sidebar state in the `dot-edit-content-field.component.spec.ts` file. [[1]](diffhunk://#diff-23d2e2fd9b71e1819e10ae720cb79ecf5e4d75e51fee641b020017fe8186640fR24) [[2]](diffhunk://#diff-23d2e2fd9b71e1819e10ae720cb79ecf5e4d75e51fee641b020017fe8186640fR172-R177) ### Enhancements to `dot-edit-content-wysiwyg-field` component: * Updated the HTML template to include new attributes and event handlers for the autocomplete component, improving its functionality. * Introduced new CSS classes and styles to enhance the appearance and usability of the autocomplete component. [[1]](diffhunk://#diff-3925f7b09d560f017223afe50a71fddad903e83ad78c7506862eff63102c67baL23-R23) [[2]](diffhunk://#diff-3925f7b09d560f017223afe50a71fddad903e83ad78c7506862eff63102c67baR35-R53) [[3]](diffhunk://#diff-3925f7b09d560f017223afe50a71fddad903e83ad78c7506862eff63102c67baL55-R85) * Added new providers and services in the `dot-edit-content-wysiwyg-field.component.spec.ts` file to support the updated functionality. [[1]](diffhunk://#diff-b2d7e080973018db72ba464358ae5bc33312adf4b1c174df7e45f1aa76e7d364R15-R29) [[2]](diffhunk://#diff-b2d7e080973018db72ba464358ae5bc33312adf4b1c174df7e45f1aa76e7d364R49-R50) [[3]](diffhunk://#diff-b2d7e080973018db72ba464358ae5bc33312adf4b1c174df7e45f1aa76e7d364R134-R143) [[4]](diffhunk://#diff-b2d7e080973018db72ba464358ae5bc33312adf4b1c174df7e45f1aa76e7d364R217-R231) * Refactored the TypeScript code to use signals for managing the state and updated methods to improve the autocomplete functionality. [[1]](diffhunk://#diff-bb38b1bd1641f522aed15dd1bafe8bc834070a87783fa502604bb4a42a10cdb0L18-R18) [[2]](diffhunk://#diff-bb38b1bd1641f522aed15dd1bafe8bc834070a87783fa502604bb4a42a10cdb0R41-R50) [[3]](diffhunk://#diff-bb38b1bd1641f522aed15dd1bafe8bc834070a87783fa502604bb4a42a10cdb0R79-R113) [[4]](diffhunk://#diff-bb38b1bd1641f522aed15dd1bafe8bc834070a87783fa502604bb4a42a10cdb0L162-R208) [[5]](diffhunk://#diff-bb38b1bd1641f522aed15dd1bafe8bc834070a87783fa502604bb4a42a10cdb0L222-R274) [[6]](diffhunk://#diff-bb38b1bd1641f522aed15dd1bafe8bc834070a87783fa502604bb4a42a10cdb0R283-R314) ### Checklist - [x] Tests - [ ] Translations - [ ] Security Implications Contemplated (add notes if applicable) ### Additional Info https://github.com/user-attachments/assets/ccf69e27-cb75-4683-90aa-3951a7d8ee81 --- .../dot-edit-content-field.component.spec.ts | 17 ++- ...-edit-content-wysiwyg-field.component.html | 13 ++- ...-edit-content-wysiwyg-field.component.scss | 30 ++++- ...it-content-wysiwyg-field.component.spec.ts | 33 +++++- ...ot-edit-content-wysiwyg-field.component.ts | 109 ++++++++++++------ 5 files changed, 154 insertions(+), 48 deletions(-) diff --git a/core-web/libs/edit-content/src/lib/components/dot-edit-content-field/dot-edit-content-field.component.spec.ts b/core-web/libs/edit-content/src/lib/components/dot-edit-content-field/dot-edit-content-field.component.spec.ts index 4dbc05c58831..9b8ab39beb1d 100644 --- a/core-web/libs/edit-content/src/lib/components/dot-edit-content-field/dot-edit-content-field.component.spec.ts +++ b/core-web/libs/edit-content/src/lib/components/dot-edit-content-field/dot-edit-content-field.component.spec.ts @@ -5,7 +5,7 @@ import { MockComponent } from 'ng-mocks'; import { of } from 'rxjs'; import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { Provider, Type } from '@angular/core'; +import { Provider, signal, Type } from '@angular/core'; import { ControlContainer, FormGroupDirective } from '@angular/forms'; import { By } from '@angular/platform-browser'; @@ -21,6 +21,7 @@ import { DotKeyValueComponent } from '@dotcms/ui'; import { DotEditContentFieldComponent } from './dot-edit-content-field.component'; +import { DotEditContentStore } from '../../feature/edit-content/store/edit-content.store'; 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'; @@ -168,6 +169,12 @@ const FIELD_TYPES_COMPONENTS: Record | DotEditFieldTe { provide: DotWorkflowActionsFireService, useValue: {} + }, + { + provide: DotEditContentStore, + useValue: { + showSidebar: signal(false) + } } ], declarations: [MockComponent(EditorComponent)] @@ -239,11 +246,11 @@ describe.each([...FIELDS_TO_BE_RENDER])('DotEditContentFieldComponent all fields it('should render the correct field type', () => { spectator.detectChanges(); - const field = spectator.debugElement.query( - By.css(`[data-testId="field-${fieldMock.variable}"]`) - ); const FIELD_TYPE = fieldTestBed.component ? fieldTestBed.component : fieldTestBed; - expect(field.componentInstance instanceof FIELD_TYPE).toBeTruthy(); + const component = spectator.query(FIELD_TYPE); + + expect(component).toBeTruthy(); + expect(component instanceof FIELD_TYPE).toBeTruthy(); }); if (fieldTestBed.outsideFormControl) { diff --git a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-wysiwyg-field/dot-edit-content-wysiwyg-field.component.html b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-wysiwyg-field/dot-edit-content-wysiwyg-field.component.html index 96d22c077ade..f1b9c20589ff 100644 --- a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-wysiwyg-field/dot-edit-content-wysiwyg-field.component.html +++ b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-wysiwyg-field/dot-edit-content-wysiwyg-field.component.html @@ -8,20 +8,25 @@
- {{ variable.key }} - {{ variable.value }} + {{ variable.key }} + {{ variable.value }} diff --git a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-wysiwyg-field/dot-edit-content-wysiwyg-field.component.scss b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-wysiwyg-field/dot-edit-content-wysiwyg-field.component.scss index 4cc0d6d16af6..45e836ab6b05 100644 --- a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-wysiwyg-field/dot-edit-content-wysiwyg-field.component.scss +++ b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-wysiwyg-field/dot-edit-content-wysiwyg-field.component.scss @@ -20,7 +20,7 @@ .dot-wysiwyg__language-selector { position: relative; - min-width: 12rem; + min-width: 16rem; } .dot-wysiwyg__search-icon { @@ -32,6 +32,25 @@ color: $color-palette-primary-500; } + .dot-wysiwyg__language-item { + display: flex; + gap: $spacing-1; + max-width: 100%; + + .dot-wysiwyg__language-key { + flex: 0 1 auto; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + direction: rtl; + } + + .dot-wysiwyg__language-value { + flex: 0 0 auto; + font-weight: $font-weight-medium-bold; + } + } + ::ng-deep { // Hide the promotion button // This button redirect to the tinyMCE premium page @@ -52,10 +71,17 @@ } p-dropdown { - min-width: 12rem; + min-width: 10rem; } + p-autocomplete .p-inputwrapper { padding-right: 35px; } + + .p-autocomplete-panel { + z-index: 1000; + position: fixed; + max-width: 16rem; + } } } diff --git a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-wysiwyg-field/dot-edit-content-wysiwyg-field.component.spec.ts b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-wysiwyg-field/dot-edit-content-wysiwyg-field.component.spec.ts index 043d3cd9a6a3..8542d26679d5 100644 --- a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-wysiwyg-field/dot-edit-content-wysiwyg-field.component.spec.ts +++ b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-wysiwyg-field/dot-edit-content-wysiwyg-field.component.spec.ts @@ -12,16 +12,21 @@ import { provideHttpClient } from '@angular/common/http'; import { provideHttpClientTesting } from '@angular/common/http/testing'; import { ControlContainer, FormsModule } from '@angular/forms'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { ActivatedRoute } from '@angular/router'; import { ConfirmationService } from 'primeng/api'; import { ConfirmDialogModule } from 'primeng/confirmdialog'; import { DropdownModule } from 'primeng/dropdown'; import { + DotContentTypeService, + DotHttpErrorManagerService, DotLanguagesService, DotLanguageVariableEntry, DotPropertiesService, - DotUploadFileService + DotUploadFileService, + DotWorkflowActionsFireService, + DotWorkflowsActionsService } from '@dotcms/data-access'; import { DotMessagePipe, mockMatchMedia, monacoMock } from '@dotcms/utils-testing'; @@ -41,6 +46,8 @@ import { WYSIWYG_MOCK } from './mocks/dot-edit-content-wysiwyg-field.mock'; +import { DotEditContentStore } from '../../feature/edit-content/store/edit-content.store'; +import { DotEditContentService } from '../../services/dot-edit-content.service'; import { createFormGroupDirectiveMock } from '../../utils/mocks'; const mockLanguageVariables: Record = { @@ -124,9 +131,16 @@ describe('DotEditContentWYSIWYGFieldComponent', () => { providers: [ mockProvider(DotLanguagesService), mockProvider(DotUploadFileService), + mockProvider(DotWorkflowsActionsService), + mockProvider(DotWorkflowActionsFireService), + mockProvider(DotContentTypeService), + mockProvider(DotEditContentService), + mockProvider(DotHttpErrorManagerService), + mockProvider(ActivatedRoute), provideHttpClient(), provideHttpClientTesting(), - ConfirmationService + ConfirmationService, + DotEditContentStore ] }); @@ -200,4 +214,19 @@ describe('DotEditContentWYSIWYGFieldComponent', () => { expect(spectator.query(byTestId('language-variable-selector'))).toBeTruthy(); }); }); + + describe('sidebar closed state', () => { + it('should add sidebar-closed class when sidebar is closed', () => { + const store = spectator.inject(DotEditContentStore); + + spectator.detectChanges(); + const element = spectator.query(byTestId('language-variable-selector')); + expect(element.classList).not.toContain('dot-wysiwyg__sidebar-closed'); + + store.toggleSidebar(); + spectator.detectChanges(); + + expect(element.classList).toContain('dot-wysiwyg__sidebar-closed'); + }); + }); }); diff --git a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-wysiwyg-field/dot-edit-content-wysiwyg-field.component.ts b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-wysiwyg-field/dot-edit-content-wysiwyg-field.component.ts index bc76e9127399..f8a04d520cf1 100644 --- a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-wysiwyg-field/dot-edit-content-wysiwyg-field.component.ts +++ b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-wysiwyg-field/dot-edit-content-wysiwyg-field.component.ts @@ -15,11 +15,7 @@ import { import { FormsModule } from '@angular/forms'; import { ConfirmationService } from 'primeng/api'; -import { - AutoCompleteCompleteEvent, - AutoCompleteModule, - AutoCompleteSelectEvent -} from 'primeng/autocomplete'; +import { AutoComplete, AutoCompleteModule, AutoCompleteSelectEvent } from 'primeng/autocomplete'; import { ConfirmDialogModule } from 'primeng/confirmdialog'; import { DropdownModule } from 'primeng/dropdown'; import { InputGroupModule } from 'primeng/inputgroup'; @@ -42,11 +38,16 @@ import { } from './dot-edit-content-wysiwyg-field.constant'; import { shouldUseDefaultEditor } from './dot-edit-content-wysiwyg-field.utils'; +import { DotEditContentStore } from '../../feature/edit-content/store/edit-content.store'; + interface LanguageVariable { key: string; value: string; } +// Quantity of language variables to show in the autocomplete +const MAX_LANGUAGES_SUGGESTIONS = 20; + /** * Component representing a WYSIWYG (What You See Is What You Get) editor field for editing content in DotCMS. * Allows users to edit content using either the TinyMCE or Monaco editor, based on the content type and properties. @@ -75,15 +76,41 @@ interface LanguageVariable { changeDetection: ChangeDetectionStrategy.OnPush }) export class DotEditContentWYSIWYGFieldComponent implements AfterViewInit { + /** + * Clear the autocomplete when the overlay is hidden. + */ + onHideOverlay() { + this.$autoComplete()?.clear(); + } + + /** + * Signal to get the TinyMCE component. + */ $tinyMCEComponent: Signal = viewChild( DotWysiwygTinymceComponent ); + + /** + * Signal to get the Monaco component. + */ $monacoComponent: Signal = viewChild(DotWysiwygMonacoComponent); + /** + * Signal to get the autocomplete component. + */ + $autoComplete = viewChild(AutoComplete); + #confirmationService = inject(ConfirmationService); #dotMessageService = inject(DotMessageService); #dotLanguagesService = inject(DotLanguagesService); + #store = inject(DotEditContentStore); + + /** + * This variable represents if the sidebar is closed. + */ + $sidebarClosed = computed(() => !this.#store.showSidebar()); + /** * This variable represents a required content type field in DotCMS. */ @@ -159,21 +186,26 @@ export class DotEditContentWYSIWYGFieldComponent implements AfterViewInit { */ $languageVariables = signal([]); - /** - * Signal to track if the user has interacted with the autocomplete. - * This is used to determine if the language variables should be loaded, and to avoid unnecessary loading. - */ - $hasInteracted = signal(false); - /** * Signal to store the selected item from the autocomplete. */ - $selectedItem = signal(null); + $selectedItem = model(''); /** - * Signal to store the search query from the autocomplete. + * Computed property to filter the language variables based on the search query. */ - $searchQuery = signal(''); + $filteredSuggestions = computed(() => { + const term = this.$selectedItem()?.toLowerCase(); + const languageVariables = this.$languageVariables(); + + if (!term) { + return []; + } + + return languageVariables + .filter((variable) => variable.key.toLowerCase().includes(term)) + .slice(0, MAX_LANGUAGES_SUGGESTIONS); + }); ngAfterViewInit(): void { // Assign the selected editor value @@ -219,30 +251,28 @@ export class DotEditContentWYSIWYGFieldComponent implements AfterViewInit { /** * Search for language variables. * - * @param {AutoCompleteCompleteEvent} event - The event object containing the search query. * @return {void} */ - search(event: AutoCompleteCompleteEvent) { - if (event.query) { - this.$searchQuery.set(event.query); - } - + loadSuggestions() { if (this.$languageVariables().length === 0) { this.getLanguageVariables(); + } else { + /** + * Set the autocomplete loading to false and show the overlay if there are suggestions. + * this for handle the bug in the autocomplete and show the loading icon when there are suggestions + */ + const autocomplete = this.$autoComplete(); + if (autocomplete) { + autocomplete.loading = false; + if (this.$filteredSuggestions().length > 0) { + autocomplete.overlayVisible = true; + } + + autocomplete.cd.markForCheck(); + } } } - /** - * Computed property to filter the language variables based on the search query. - */ - $filteredSuggestions = computed(() => { - const term = this.$searchQuery().toLowerCase(); - - return this.$languageVariables() - .filter((variable) => variable.key.toLowerCase().includes(term)) - .slice(0, 10); - }); - /** * Handles the selection of a language variable from the autocomplete. * @@ -250,29 +280,38 @@ export class DotEditContentWYSIWYGFieldComponent implements AfterViewInit { * @return {void} */ onSelectLanguageVariable($event: AutoCompleteSelectEvent) { + const { value } = $event; + if (this.$displayedEditor() === AvailableEditor.TinyMCE) { const tinyMCE = this.$tinyMCEComponent(); if (tinyMCE) { - tinyMCE.insertContent(`$text.get('${$event.value.key}')`); + tinyMCE.insertContent(`$text.get('${value.key}')`); + this.resetAutocomplete(); } else { console.warn('TinyMCE component is not available'); } } else if (this.$displayedEditor() === AvailableEditor.Monaco) { const monaco = this.$monacoComponent(); if (monaco) { - monaco.insertContent(`$text.get('${$event.value.key}')`); + monaco.insertContent(`$text.get('${value.key}')`); + this.resetAutocomplete(); } else { console.warn('Monaco component is not available'); } } + } - this.$selectedItem.set(null); + /** + * Resets the autocomplete state. + */ + private resetAutocomplete() { + this.$autoComplete()?.clear(); } /** * Fetches language variables from the DotCMS Languages API and formats them for use in the autocomplete. */ - private getLanguageVariables() { + getLanguageVariables() { // TODO: This is a temporary solution to get the language variables from the DotCMS Languages API. // We need a way to get the current language from the contentlet. this.#dotLanguagesService