Skip to content

Commit

Permalink
feat(edit-content) improve the ui of the autocomplete (#30452)
Browse files Browse the repository at this point in the history
#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
  • Loading branch information
oidacra authored Oct 25, 2024
1 parent f0cde82 commit 14430e3
Show file tree
Hide file tree
Showing 5 changed files with 154 additions and 48 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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';
Expand Down Expand Up @@ -168,6 +169,12 @@ const FIELD_TYPES_COMPONENTS: Record<FIELD_TYPES, Type<unknown> | DotEditFieldTe
{
provide: DotWorkflowActionsFireService,
useValue: {}
},
{
provide: DotEditContentStore,
useValue: {
showSidebar: signal(false)
}
}
],
declarations: [MockComponent(EditorComponent)]
Expand Down Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,20 +8,25 @@

<div class="dot-wysiwyg__language-selector">
<p-autoComplete
#autoComplete
class="dot-wysiwyg__language-autocomplete"
[pTooltip]="'edit.content.wysiwyg-field.language-variable-tooltip' | dm"
tooltipPosition="bottom"
[class.dot-wysiwyg__sidebar-closed]="$sidebarClosed()"
data-testId="language-variable-selector"
[placeholder]="'edit.content.wysiwyg-field.language-variable-placeholder' | dm"
[(ngModel)]="$selectedItem"
[ngModelOptions]="{ standalone: true }"
[suggestions]="$filteredSuggestions()"
[minLength]="1"
debounce="200"
(onHide)="onHideOverlay()"
tooltipEvent="focus"
(onSelect)="onSelectLanguageVariable($event)"
(completeMethod)="search($event)"
(completeMethod)="loadSuggestions()"
[styleClass]="'dot-wysiwyg__language-autocomplete--with-icon'">
<ng-template let-variable pTemplate="item">
<span class="dot-wysiwyg__language-item">
{{ variable.key }} - {{ variable.value }}
<span class="dot-wysiwyg__language-key">{{ variable.key }}</span>
<span class="dot-wysiwyg__language-value">{{ variable.value }}</span>
</span>
</ng-template>
</p-autoComplete>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@

.dot-wysiwyg__language-selector {
position: relative;
min-width: 12rem;
min-width: 16rem;
}

.dot-wysiwyg__search-icon {
Expand All @@ -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
Expand All @@ -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;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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<string, DotLanguageVariableEntry> = {
Expand Down Expand Up @@ -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
]
});

Expand Down Expand Up @@ -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');
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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.
Expand Down Expand Up @@ -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<DotWysiwygTinymceComponent | undefined> = viewChild(
DotWysiwygTinymceComponent
);

/**
* Signal to get the Monaco component.
*/
$monacoComponent: Signal<DotWysiwygMonacoComponent | undefined> =
viewChild(DotWysiwygMonacoComponent);

/**
* Signal to get the autocomplete component.
*/
$autoComplete = viewChild<AutoComplete>(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.
*/
Expand Down Expand Up @@ -159,21 +186,26 @@ export class DotEditContentWYSIWYGFieldComponent implements AfterViewInit {
*/
$languageVariables = signal<LanguageVariable[]>([]);

/**
* 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<LanguageVariable | null>(null);
$selectedItem = model<string>('');

/**
* 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
Expand Down Expand Up @@ -219,60 +251,67 @@ 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.
*
* @param {AutoCompleteSelectEvent} $event - The event object containing the selected value.
* @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
Expand Down

0 comments on commit 14430e3

Please sign in to comment.