diff --git a/core-web/apps/dotcms-block-editor/src/app/app.module.ts b/core-web/apps/dotcms-block-editor/src/app/app.module.ts index 3fff454dea1f..6a95d4e8b407 100644 --- a/core-web/apps/dotcms-block-editor/src/app/app.module.ts +++ b/core-web/apps/dotcms-block-editor/src/app/app.module.ts @@ -10,7 +10,12 @@ import { ListboxModule } from 'primeng/listbox'; import { OrderListModule } from 'primeng/orderlist'; import { BlockEditorModule, DotBlockEditorComponent } from '@dotcms/block-editor'; -import { DotPropertiesService } from '@dotcms/data-access'; +import { + DotPropertiesService, + DotContentSearchService, + DotLanguagesService +} from '@dotcms/data-access'; +import { DotAssetSearchComponent } from '@dotcms/ui'; import { AppComponent } from './app.component'; @@ -24,9 +29,10 @@ import { AppComponent } from './app.component'; BlockEditorModule, OrderListModule, ListboxModule, - HttpClientModule + HttpClientModule, + DotAssetSearchComponent ], - providers: [DotPropertiesService] + providers: [DotPropertiesService, DotContentSearchService, DotLanguagesService] }) export class AppModule implements DoBootstrap { constructor(private injector: Injector) {} diff --git a/core-web/libs/block-editor/src/lib/block-editor.module.ts b/core-web/libs/block-editor/src/lib/block-editor.module.ts index 35449c869303..39be1706b76d 100644 --- a/core-web/libs/block-editor/src/lib/block-editor.module.ts +++ b/core-web/libs/block-editor/src/lib/block-editor.module.ts @@ -6,9 +6,15 @@ import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { ConfirmationService } from 'primeng/api'; import { ConfirmDialogModule } from 'primeng/confirmdialog'; -import { DotMessageService } from '@dotcms/data-access'; +import { + DotContentSearchService, + DotLanguagesService, + DotMessageService, + DotPropertiesService, + DotUploadFileService +} from '@dotcms/data-access'; import { LoggerService, StringUtils } from '@dotcms/dotcms-js'; -import { DotFieldRequiredDirective, DotMessagePipe } from '@dotcms/ui'; +import { DotAssetSearchComponent, DotFieldRequiredDirective, DotMessagePipe } from '@dotcms/ui'; //Editor import { DotBlockEditorComponent } from './components/dot-block-editor/dot-block-editor.component'; @@ -29,7 +35,7 @@ import { } from './extensions'; import { AssetFormModule } from './extensions/asset-form/asset-form.module'; import { ContentletBlockComponent } from './nodes'; -import { DotAiService, DotUploadFileService, EditorDirective } from './shared'; +import { DotAiService, EditorDirective } from './shared'; import { PrimengModule } from './shared/primeng.module'; import { SharedModule } from './shared/shared.module'; @@ -49,7 +55,8 @@ const initTranslations = (dotMessageService: DotMessageService) => { UploadPlaceholderComponent, DotMessagePipe, ConfirmDialogModule, - AIImagePromptComponent + AIImagePromptComponent, + DotAssetSearchComponent ], declarations: [ EditorDirective, @@ -73,6 +80,9 @@ const initTranslations = (dotMessageService: DotMessageService) => { StringUtils, DotAiService, ConfirmationService, + DotPropertiesService, + DotContentSearchService, + DotLanguagesService, { provide: APP_INITIALIZER, useFactory: initTranslations, diff --git a/core-web/libs/block-editor/src/lib/components/dot-block-editor/dot-block-editor.component.stories.ts b/core-web/libs/block-editor/src/lib/components/dot-block-editor/dot-block-editor.component.stories.ts index 07597f122cb1..6d11145aee9a 100644 --- a/core-web/libs/block-editor/src/lib/components/dot-block-editor/dot-block-editor.component.stories.ts +++ b/core-web/libs/block-editor/src/lib/components/dot-block-editor/dot-block-editor.component.stories.ts @@ -10,7 +10,8 @@ import { OrderListModule } from 'primeng/orderlist'; import { debounceTime, delay, tap } from 'rxjs/operators'; -import { DotMessageService, DotPropertiesService } from '@dotcms/data-access'; +import { DotMessageService, DotPropertiesService, DotUploadFileService } from '@dotcms/data-access'; +import { DotContentSearchService, DotLanguageService } from '@dotcms/ui'; import { DotBlockEditorComponent } from './dot-block-editor.component'; @@ -26,10 +27,7 @@ import { ASSET_MOCK, CONTENTLETS_MOCK, DotAiService, - DotLanguageService, - DotUploadFileService, FileStatus, - SearchService, SuggestionsComponent, SuggestionsService } from '../../shared'; @@ -170,7 +168,7 @@ export const Primary = () => ({ } }, { - provide: SearchService, + provide: DotContentSearchService, useValue: { get(params) { const query = params.query.match(new RegExp(/(?<=:)(.*?)(?=\*)/))[0]; diff --git a/core-web/libs/block-editor/src/lib/extensions/asset-form/asset-form.component.scss b/core-web/libs/block-editor/src/lib/extensions/asset-form/asset-form.component.scss index d26ff251441e..90d774beef7e 100644 --- a/core-web/libs/block-editor/src/lib/extensions/asset-form/asset-form.component.scss +++ b/core-web/libs/block-editor/src/lib/extensions/asset-form/asset-form.component.scss @@ -26,4 +26,8 @@ .p-tabview-panel { height: 25rem; } + + .p-tabview-panels { + padding: 0; + } } diff --git a/core-web/libs/block-editor/src/lib/extensions/asset-form/asset-form.module.ts b/core-web/libs/block-editor/src/lib/extensions/asset-form/asset-form.module.ts index b9e250980478..2b345fa035e3 100644 --- a/core-web/libs/block-editor/src/lib/extensions/asset-form/asset-form.module.ts +++ b/core-web/libs/block-editor/src/lib/extensions/asset-form/asset-form.module.ts @@ -2,33 +2,32 @@ import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; -import { DotSpinnerModule } from '@dotcms/ui'; +import { DotUploadFileService } from '@dotcms/data-access'; +import { DotAssetSearchComponent, DotSpinnerModule } from '@dotcms/ui'; import { AssetFormComponent } from './asset-form.component'; -import { DotAssetCardComponent } from './components/dot-asset-search/components/dot-asset-card/dot-asset-card.component'; -import { DotAssetCardListComponent } from './components/dot-asset-search/components/dot-asset-card-list/dot-asset-card-list.component'; -import { DotAssetCardSkeletonComponent } from './components/dot-asset-search/components/dot-asset-card-skeleton/dot-asset-card-skeleton.component'; -import { DotAssetSearchComponent } from './components/dot-asset-search/dot-asset-search.component'; import { DotExternalAssetComponent } from './components/dot-external-asset/dot-external-asset.component'; import { DotAssetPreviewComponent } from './components/dot-upload-asset/components/dot-asset-preview/dot-asset-preview.component'; import { DotUploadAssetComponent } from './components/dot-upload-asset/dot-upload-asset.component'; -import { DotUploadFileService } from '../../shared'; import { PrimengModule } from '../../shared/primeng.module'; @NgModule({ - imports: [CommonModule, FormsModule, ReactiveFormsModule, DotSpinnerModule, PrimengModule], + imports: [ + CommonModule, + FormsModule, + ReactiveFormsModule, + DotSpinnerModule, + PrimengModule, + DotAssetSearchComponent + ], declarations: [ AssetFormComponent, - DotAssetCardListComponent, - DotAssetCardComponent, - DotAssetCardSkeletonComponent, DotExternalAssetComponent, - DotAssetSearchComponent, DotUploadAssetComponent, DotAssetPreviewComponent ], providers: [DotUploadFileService], - exports: [AssetFormComponent, DotAssetSearchComponent] + exports: [AssetFormComponent] }) export class AssetFormModule {} diff --git a/core-web/libs/block-editor/src/lib/extensions/asset-form/components/dot-asset-search/components/dot-asset-card-list/dot-asset-card-list.component.spec.ts b/core-web/libs/block-editor/src/lib/extensions/asset-form/components/dot-asset-search/components/dot-asset-card-list/dot-asset-card-list.component.spec.ts deleted file mode 100644 index e93e60c36066..000000000000 --- a/core-web/libs/block-editor/src/lib/extensions/asset-form/components/dot-asset-search/components/dot-asset-card-list/dot-asset-card-list.component.spec.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; - -import { DotAssetCardListComponent } from './dot-asset-card-list.component'; - -describe('DotAssetCardListComponent', () => { - let component: DotAssetCardListComponent; - let fixture: ComponentFixture; - - beforeEach(async () => { - await TestBed.configureTestingModule({ - declarations: [DotAssetCardListComponent] - }).compileComponents(); - - fixture = TestBed.createComponent(DotAssetCardListComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/core-web/libs/block-editor/src/lib/extensions/asset-form/components/dot-asset-search/components/dot-asset-card-list/dot-asset-card-list.component.ts b/core-web/libs/block-editor/src/lib/extensions/asset-form/components/dot-asset-search/components/dot-asset-card-list/dot-asset-card-list.component.ts deleted file mode 100644 index c7acbea8e64f..000000000000 --- a/core-web/libs/block-editor/src/lib/extensions/asset-form/components/dot-asset-search/components/dot-asset-card-list/dot-asset-card-list.component.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core'; - -import { DotCMSContentlet } from '@dotcms/dotcms-models'; - -import { sanitizeUrl, squarePlus } from '../../../../../../shared'; - -@Component({ - selector: 'dot-asset-card-list', - templateUrl: './dot-asset-card-list.component.html', - styleUrls: ['./dot-asset-card-list.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush -}) -export class DotAssetCardListComponent { - @Output() nextBatch: EventEmitter = new EventEmitter(); - @Output() selectedItem: EventEmitter = new EventEmitter(); - - @Input() done = false; - @Input() loading = true; - @Input() set contentlets(value: DotCMSContentlet[]) { - this._offset = value?.length || 0; - this._itemRows = this.createRowItem(value); - } - - public loadingItems = [null, null, null]; - public icon = sanitizeUrl(squarePlus); - private _itemRows: DotCMSContentlet[][] = []; - private _offset = 0; - - get rows() { - // Force Scroll to Update by breaking the object Reference - return [...this._itemRows]; - } - - onScrollIndexChange(e: { first: number; last: number }) { - if (this.done) { - return; - } - - const end = e.last; - const total = this.rows.length; - - if (end === total) { - // We multiply by 2 because of the way we should de images. - // Two images per row. - this.nextBatch.emit(this._offset); - } - } - - /** - * - * Create an array of type: [[DotCMSContentlet, DotCMSContentlet], ...] - * Due PrimeNg virtual scroll allows only displaying one element at a time [https://primefaces.org/primeng/virtualscroller], - * and figma's layout requires displaying two columns of contentlets [https://github.com/dotCMS/core/issues/23235] - * - * @private - * @param {DotCMSContentlet[][]} prev - * @param {DotCMSContentlet[]} contentlets - * @return {*} - * @memberof DotAssetSearchStore - */ - private createRowItem(contentlets: DotCMSContentlet[] = []) { - const rows = []; - contentlets.forEach((contentlet) => { - const i = rows.length - 1; - rows[i]?.length < 2 ? rows[i].push(contentlet) : rows.push([contentlet]); - }); - - return rows; - } -} diff --git a/core-web/libs/block-editor/src/lib/extensions/asset-form/components/dot-asset-search/components/dot-asset-card-skeleton/dot-asset-card-skeleton.component.html b/core-web/libs/block-editor/src/lib/extensions/asset-form/components/dot-asset-search/components/dot-asset-card-skeleton/dot-asset-card-skeleton.component.html deleted file mode 100644 index 3e02dc251bbb..000000000000 --- a/core-web/libs/block-editor/src/lib/extensions/asset-form/components/dot-asset-search/components/dot-asset-card-skeleton/dot-asset-card-skeleton.component.html +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - -
- - -
-
-
diff --git a/core-web/libs/block-editor/src/lib/extensions/asset-form/components/dot-asset-search/components/dot-asset-card-skeleton/dot-asset-card-skeleton.component.spec.ts b/core-web/libs/block-editor/src/lib/extensions/asset-form/components/dot-asset-search/components/dot-asset-card-skeleton/dot-asset-card-skeleton.component.spec.ts deleted file mode 100644 index 2901addcc777..000000000000 --- a/core-web/libs/block-editor/src/lib/extensions/asset-form/components/dot-asset-search/components/dot-asset-card-skeleton/dot-asset-card-skeleton.component.spec.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; - -import { DotAssetSkeletonComponent } from './dot-asset-card-skeleton.component'; - -describe('DotAssetSkeletonComponent', () => { - let component: DotAssetSkeletonComponent; - let fixture: ComponentFixture; - - beforeEach(async () => { - await TestBed.configureTestingModule({ - declarations: [DotAssetSkeletonComponent] - }).compileComponents(); - - fixture = TestBed.createComponent(DotAssetSkeletonComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/core-web/libs/block-editor/src/lib/extensions/asset-form/components/dot-asset-search/components/dot-asset-card/dot-asset-card.component.spec.ts b/core-web/libs/block-editor/src/lib/extensions/asset-form/components/dot-asset-search/components/dot-asset-card/dot-asset-card.component.spec.ts deleted file mode 100644 index 8fb82514f617..000000000000 --- a/core-web/libs/block-editor/src/lib/extensions/asset-form/components/dot-asset-search/components/dot-asset-card/dot-asset-card.component.spec.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; - -import { DotAssetCardComponent } from './dot-asset-card.component'; - -describe('DotAssetCardComponent', () => { - let component: DotAssetCardComponent; - let fixture: ComponentFixture; - - beforeEach(async () => { - await TestBed.configureTestingModule({ - declarations: [DotAssetCardComponent] - }).compileComponents(); - - fixture = TestBed.createComponent(DotAssetCardComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/core-web/libs/block-editor/src/lib/extensions/asset-form/components/dot-asset-search/components/dot-asset-card/dot-asset-card.component.ts b/core-web/libs/block-editor/src/lib/extensions/asset-form/components/dot-asset-search/components/dot-asset-card/dot-asset-card.component.ts deleted file mode 100644 index 5aa775502d23..000000000000 --- a/core-web/libs/block-editor/src/lib/extensions/asset-form/components/dot-asset-search/components/dot-asset-card/dot-asset-card.component.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core'; - -import { DotCMSContentlet, EDITOR_MARKETING_KEYS } from '@dotcms/dotcms-models'; - -import { DotMarketingConfigService } from '../../../../../../shared'; - -@Component({ - selector: 'dot-asset-card', - templateUrl: './dot-asset-card.component.html', - styleUrls: ['./dot-asset-card.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush -}) -export class DotAssetCardComponent implements OnInit { - showVideoThumbnail = true; - - @Input() contentlet: DotCMSContentlet; - - constructor(private dotMarketingConfigService: DotMarketingConfigService) {} - - ngOnInit() { - this.showVideoThumbnail = this.dotMarketingConfigService.getProperty( - EDITOR_MARKETING_KEYS.SHOW_VIDEO_THUMBNAIL - ); - } - - /** - * Return the contentlet Thumbanil based in the inode - * - * @param {string} inode - * @return {*} {string} - * @memberof DotAssetCardComponent - */ - getImage(inode: string): string { - return `/dA/${inode}/500w/50q`; - } - - /** - * Return the contentlet icon - * - * @return {*} {string} - * @memberof DotAssetCardComponent - */ - getContentletIcon(): string { - return this.contentlet?.baseType !== 'FILEASSET' - ? this.contentlet?.contentTypeIcon - : this.contentlet?.__icon__; - } -} diff --git a/core-web/libs/block-editor/src/lib/extensions/asset-form/components/dot-asset-search/dot-asset-search.component.spec.ts b/core-web/libs/block-editor/src/lib/extensions/asset-form/components/dot-asset-search/dot-asset-search.component.spec.ts deleted file mode 100644 index 1a185cd7d31a..000000000000 --- a/core-web/libs/block-editor/src/lib/extensions/asset-form/components/dot-asset-search/dot-asset-search.component.spec.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; - -import { DotAssetSearchComponent } from './dot-asset-search.component'; - -describe('DotAssetSearchComponent', () => { - let component: DotAssetSearchComponent; - let fixture: ComponentFixture; - - beforeEach(async () => { - await TestBed.configureTestingModule({ - declarations: [DotAssetSearchComponent] - }).compileComponents(); - - fixture = TestBed.createComponent(DotAssetSearchComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/core-web/libs/block-editor/src/lib/extensions/asset-form/components/dot-asset-search/dot-asset-search.component.ts b/core-web/libs/block-editor/src/lib/extensions/asset-form/components/dot-asset-search/dot-asset-search.component.ts deleted file mode 100644 index 0b57629d4d1f..000000000000 --- a/core-web/libs/block-editor/src/lib/extensions/asset-form/components/dot-asset-search/dot-asset-search.component.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { BehaviorSubject, fromEvent, Subject } from 'rxjs'; - -import { - AfterViewInit, - ChangeDetectionStrategy, - Component, - ElementRef, - EventEmitter, - Input, - OnDestroy, - OnInit, - Output, - ViewChild -} from '@angular/core'; - -import { debounceTime, skip, takeUntil, throttleTime } from 'rxjs/operators'; - -import { DotCMSContentlet, EditorAssetTypes } from '@dotcms/dotcms-models'; - -// services -import { DotAssetSearchStore } from './store/dot-asset-search.store'; - -@Component({ - selector: 'dot-asset-search', - templateUrl: './dot-asset-search.component.html', - styleUrls: ['./dot-asset-search.component.scss'], - providers: [DotAssetSearchStore], - changeDetection: ChangeDetectionStrategy.OnPush -}) -export class DotAssetSearchComponent implements OnInit, OnDestroy, AfterViewInit { - @ViewChild('input') input!: ElementRef; - @Output() addAsset = new EventEmitter(); - - @Input() set languageId(id) { - this.store.updatelanguageId(id); - } - - @Input() set type(type: EditorAssetTypes) { - this.store.updateAssetType(type); - } - - vm$ = this.store.vm$; - offset$ = new BehaviorSubject(0); - - private destroy$: Subject = new Subject(); - - constructor(private store: DotAssetSearchStore) {} - - ngOnInit(): void { - this.store.searchContentlet(''); - - this.offset$ - .pipe(takeUntil(this.destroy$), skip(1), throttleTime(450)) - .subscribe(this.store.nextBatch); - - requestAnimationFrame(() => this.input.nativeElement.focus()); - } - - ngAfterViewInit() { - fromEvent(this.input.nativeElement, 'input') - .pipe(takeUntil(this.destroy$), debounceTime(450)) - .subscribe(({ target }) => { - this.store.searchContentlet(target.value); - }); - } - - ngOnDestroy(): void { - this.destroy$.next(true); - } -} diff --git a/core-web/libs/block-editor/src/lib/extensions/asset-form/components/dot-asset-search/store/dot-asset-search.store.spec.ts b/core-web/libs/block-editor/src/lib/extensions/asset-form/components/dot-asset-search/store/dot-asset-search.store.spec.ts deleted file mode 100644 index 51acaf7e8f24..000000000000 --- a/core-web/libs/block-editor/src/lib/extensions/asset-form/components/dot-asset-search/store/dot-asset-search.store.spec.ts +++ /dev/null @@ -1,268 +0,0 @@ -import { of } from 'rxjs'; - -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { TestBed } from '@angular/core/testing'; - -import { DotAssetSearchStore } from './dot-asset-search.store'; - -import { - DotLanguageService, - ESOrderDirection, - IMAGE_CONTENTLETS_MOCK, - SearchService -} from '../../../../../shared'; - -const INITIAL_STATE = { contentlets: [], loading: true, preventScroll: false }; -const LanguageMock = { - 1: { - country: 'United States', - countryCode: 'US', - defaultLanguage: true, - id: 1, - language: 'English', - languageCode: 'en' - }, - 2: { - country: 'Espana', - countryCode: 'ES', - defaultLanguage: false, - id: 2, - language: 'Espanol', - languageCode: 'es' - } -}; - -const CONTENTLETS_MOCK_WITH_LANG = IMAGE_CONTENTLETS_MOCK.splice(0, 4).map((contentlet) => ({ - ...contentlet, - language: 'en-US' -})); - -describe('DotAssetSearchStore', () => { - let service: DotAssetSearchStore; - let searchService: SearchService; - - beforeEach(() => { - TestBed.configureTestingModule({ - imports: [HttpClientTestingModule], - providers: [ - DotAssetSearchStore, - { - provide: SearchService, - useValue: { - get: () => of() - } - }, - { - provide: DotLanguageService, - useValue: { - getLanguages: jest.fn().mockReturnValue(of(LanguageMock)) - } - } - ] - }); - - service = TestBed.inject(DotAssetSearchStore); - }); - - test('should have inital state', (done) => { - service.vm$.subscribe((res) => { - expect(res).toEqual(INITIAL_STATE); - done(); - }); - }); - - describe('Updaters', () => { - test('should update contentlets', (done) => { - const contentlet = [IMAGE_CONTENTLETS_MOCK[0], IMAGE_CONTENTLETS_MOCK[1]]; - service.updateContentlets(contentlet); - - service.vm$.subscribe((res) => { - expect(res).toEqual({ - ...INITIAL_STATE, - contentlets: contentlet - }); - done(); - }); - }); - - test('should update LanguageId', (done) => { - service.updatelanguageId(2); - - service.state$.subscribe((res) => { - expect(res.languageId).toEqual(2); - done(); - }); - }); - - test('should update Loading', (done) => { - service.updateLoading(false); - - service.vm$.subscribe((res) => { - expect(res).toEqual({ - ...INITIAL_STATE, - loading: false - }); - done(); - }); - }); - - test('should prevent Scroll', (done) => { - service.updatePreventScroll(true); - - service.vm$.subscribe((res) => { - expect(res).toEqual({ - ...INITIAL_STATE, - preventScroll: true - }); - done(); - }); - }); - - test('should Search', (done) => { - const search = 'Image'; - service.updateSearch(search); - - service.state$.subscribe((res) => { - expect(res.search).toEqual(search); - done(); - }); - }); - }); - - describe('Effects', () => { - beforeEach(() => { - searchService = TestBed.inject(SearchService); - }); - - test('should search contentlets based on a search query', (done) => { - const contentlets = CONTENTLETS_MOCK_WITH_LANG.splice(0, 2); - // Spies - const loadingMock = jest.spyOn(service, 'updateLoading'); - jest.spyOn(service, 'updateSearch'); - jest.spyOn(service, 'updateContentlets'); - jest.spyOn(searchService, 'get').mockReturnValue( - of({ jsonObjectView: { contentlets } }) - ); - - const query = 'image'; - - service.searchContentlet(query); - - service.vm$.subscribe((res) => { - expect(res).toEqual({ - preventScroll: false, - loading: false, - contentlets - }); - - done(); - }); - - expect(searchService.get).toHaveBeenCalledWith({ - query: `+catchall:${query}* +title:'${query}'^15 +languageId:1 +baseType:(4 OR 9) +metadata.contenttype:image/* +deleted:false +working:true`, - sortOrder: ESOrderDirection.ASC, - limit: 20, - offset: 0 - }); - - // First -> the value is true because we start the search - // Second -> the value is false because we finish the search - expect(loadingMock.mock.calls).toEqual([[true], [false]]); - expect(service.updateContentlets).toHaveBeenCalledWith(contentlets); - expect(service.updateSearch).toHaveBeenCalledWith(query); - }); - - test('should not add "*" when the search has a "-" ', (done) => { - const contentlets = CONTENTLETS_MOCK_WITH_LANG; - jest.spyOn(searchService, 'get').mockReturnValue( - of({ jsonObjectView: { contentlets } }) - ); - - const query = 'hola-'; - - service.searchContentlet(query); - - service.vm$.subscribe((res) => { - expect(res).toEqual({ - preventScroll: false, - loading: false, - contentlets - }); - - done(); - }); - - expect(searchService.get).toHaveBeenCalledWith({ - query: `+catchall:${query} +title:'${query}'^15 +languageId:1 +baseType:(4 OR 9) +metadata.contenttype:image/* +deleted:false +working:true`, - sortOrder: ESOrderDirection.ASC, - limit: 20, - offset: 0 - }); - }); - - test('should load the next batch of contentlets based on the offset', (done) => { - const contentlets_p1 = [...CONTENTLETS_MOCK_WITH_LANG].splice(0, 2); - const contentlets_p2 = [...CONTENTLETS_MOCK_WITH_LANG].splice(2, 2); - - // Spies - jest.spyOn(service, 'updateSearch'); - jest.spyOn(service, 'updateContentlets'); - jest.spyOn(searchService, 'get').mockReturnValue( - of({ jsonObjectView: { contentlets: contentlets_p2 } }) - ); - - const query = 'image'; - const offset = 2; - - service.updateContentlets(contentlets_p1); - service.updateSearch(query); - service.nextBatch(offset); - - service.vm$.subscribe((res) => { - expect(res).toEqual({ - preventScroll: false, - loading: false, - contentlets: [...contentlets_p1, ...contentlets_p2] - }); - - done(); - }); - - expect(searchService.get).toHaveBeenCalledWith({ - query: ` +catchall:${query}* title:'${query}'^15 +languageId:1 +baseType:(4 OR 9) +metadata.contenttype:image/* +deleted:false +working:true`, - sortOrder: ESOrderDirection.ASC, - limit: 20, - offset - }); - expect(service.updateContentlets).toHaveBeenCalledWith(contentlets_p1); - }); - - test('should set preventScroll to true when backend date is coming empty', (done) => { - const contentlets = [...CONTENTLETS_MOCK_WITH_LANG].splice(0, 2); - - // Spies - jest.spyOn(service, 'updateSearch'); - jest.spyOn(service, 'updateContentlets'); - jest.spyOn(searchService, 'get').mockReturnValue( - of({ jsonObjectView: { contentlets: [] } }) - ); - - const query = 'image'; - const offset = 2; - - service.updateContentlets(contentlets); - service.updateSearch(query); - service.nextBatch(offset); - - service.vm$.subscribe((res) => { - expect(res).toEqual({ - preventScroll: true, - loading: false, - contentlets - }); - - done(); - }); - }); - }); -}); diff --git a/core-web/libs/block-editor/src/lib/extensions/asset-form/components/dot-asset-search/store/dot-asset-search.store.ts b/core-web/libs/block-editor/src/lib/extensions/asset-form/components/dot-asset-search/store/dot-asset-search.store.ts deleted file mode 100644 index 7474f26e9a4a..000000000000 --- a/core-web/libs/block-editor/src/lib/extensions/asset-form/components/dot-asset-search/store/dot-asset-search.store.ts +++ /dev/null @@ -1,177 +0,0 @@ -import { ComponentStore } from '@ngrx/component-store'; - -import { Injectable } from '@angular/core'; - -import { Observable } from 'rxjs/internal/Observable'; -import { map, mergeMap, tap, withLatestFrom } from 'rxjs/operators'; - -import { DotCMSContentlet, EditorAssetTypes } from '@dotcms/dotcms-models'; - -import { - SearchService, - Languages, - DotLanguageService, - EsQueryParams, - ESOrderDirection -} from '../../../../../shared'; - -const DEFAULT_LANG_ID = 1; - -export interface DotImageSearchState { - loading: boolean; - preventScroll: boolean; - contentlets: DotCMSContentlet[]; - languageId: number; - search: string; - assetType: EditorAssetTypes; -} - -const defaultState: DotImageSearchState = { - loading: true, - preventScroll: false, - contentlets: [], - languageId: DEFAULT_LANG_ID, - search: '', - assetType: null -}; - -@Injectable() -export class DotAssetSearchStore extends ComponentStore { - // Selectors - readonly vm$ = this.select(({ contentlets, loading, preventScroll }) => ({ - contentlets, - loading, - preventScroll - })); - - // Setters - readonly updateContentlets = this.updater((state, contentlets) => { - return { - ...state, - contentlets - }; - }); - - readonly updateAssetType = this.updater((state, assetType) => { - return { - ...state, - assetType - }; - }); - - readonly updatelanguageId = this.updater((state, languageId) => { - return { - ...state, - languageId - }; - }); - - readonly updateLoading = this.updater((state, loading) => { - return { - ...state, - loading - }; - }); - - readonly updatePreventScroll = this.updater((state, preventScroll) => { - return { - ...state, - preventScroll - }; - }); - - readonly updateSearch = this.updater((state, search) => { - return { - ...state, - search - }; - }); - - // Effects - readonly searchContentlet = this.effect((origin$: Observable) => { - return origin$.pipe( - tap((search) => { - this.updateLoading(true); - this.updateSearch(search); - }), - withLatestFrom(this.state$), - map(([search, state]) => ({ ...state, search })), - mergeMap((data) => this.searchContentletsRequest(this.params({ ...data }), [])) - ); - }); - - readonly nextBatch = this.effect((origin$: Observable) => { - return origin$.pipe( - withLatestFrom(this.state$), - map(([offset, state]) => ({ ...state, offset })), - mergeMap(({ contentlets, ...data }) => - this.searchContentletsRequest(this.params(data), contentlets) - ) - ); - }); - - private languages: Languages; - - constructor( - private searchService: SearchService, - private dotLanguageService: DotLanguageService - ) { - super(defaultState); - - this.dotLanguageService.getLanguages().subscribe((languages) => { - this.languages = languages; - }); - } - - private searchContentletsRequest(params, prev: DotCMSContentlet[]) { - return this.searchService.get(params).pipe( - map(({ jsonObjectView: { contentlets } }) => { - const items = this.setContentletLanguage(contentlets); - this.updateLoading(false); - this.updatePreventScroll(!contentlets?.length); - - return this.updateContentlets([...prev, ...items]); - }) - ); - } - - private params({ search, assetType, offset = 0, languageId }): EsQueryParams { - const filter = search.includes('-') ? search : `${search}*`; - - return { - query: `+catchall:${filter} title:'${search}'^15 +languageId:${languageId} +baseType:(4 OR 9) +metadata.contenttype:${ - assetType || '' - }/* +deleted:false +working:true`, - sortOrder: ESOrderDirection.ASC, - limit: 20, - offset - }; - } - - /** - * This method add the Language to the contentets based on their languageId - * - * @private - * @param {DotCMSContentlet[]} contentlets - * @return {*} - * @memberof ImageTabviewFormComponent - */ - private setContentletLanguage(contentlets: DotCMSContentlet[]) { - return contentlets.map((contentlet) => { - return { - ...contentlet, - language: this.getLanguage(contentlet.languageId) - }; - }); - } - - private getLanguage(languageId: number): string { - const { languageCode, countryCode } = this.languages[languageId]; - - if (!languageCode || !countryCode) { - return ''; - } - - return `${languageCode}-${countryCode}`; - } -} diff --git a/core-web/libs/block-editor/src/lib/extensions/asset-form/components/dot-upload-asset/dot-upload-asset.component.ts b/core-web/libs/block-editor/src/lib/extensions/asset-form/components/dot-upload-asset/dot-upload-asset.component.ts index 4a1ed57d79f1..799edec6be2f 100644 --- a/core-web/libs/block-editor/src/lib/extensions/asset-form/components/dot-upload-asset/dot-upload-asset.component.ts +++ b/core-web/libs/block-editor/src/lib/extensions/asset-form/components/dot-upload-asset/dot-upload-asset.component.ts @@ -16,12 +16,11 @@ import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser'; import { catchError, take } from 'rxjs/operators'; +import { DotUploadFileService } from '@dotcms/data-access'; import { DotCMSContentlet, EditorAssetTypes } from '@dotcms/dotcms-models'; import { shakeAnimation } from './animations'; -import { DotUploadFileService } from '../../../../shared'; - export enum STATUS { SELECT = 'SELECT', PREVIEW = 'PREVIEW', diff --git a/core-web/libs/block-editor/src/lib/extensions/asset-uploader/asset-uploader.extension.ts b/core-web/libs/block-editor/src/lib/extensions/asset-uploader/asset-uploader.extension.ts index dd899a9a2e1f..8dc9ace59fc8 100644 --- a/core-web/libs/block-editor/src/lib/extensions/asset-uploader/asset-uploader.extension.ts +++ b/core-web/libs/block-editor/src/lib/extensions/asset-uploader/asset-uploader.extension.ts @@ -8,18 +8,14 @@ import { take } from 'rxjs/operators'; import { Extension } from '@tiptap/core'; +import { DotUploadFileService } from '@dotcms/data-access'; import { DotCMSContentlet, EditorAssetTypes } from '@dotcms/dotcms-models'; import { UploadPlaceholderComponent } from './components/upload-placeholder/upload-placeholder.component'; import { PlaceholderPlugin } from './plugins/placeholder.plugin'; import { ImageNode } from '../../nodes'; -import { - deselectCurrentNode, - DotUploadFileService, - getCursorPosition, - isImageURL -} from '../../shared'; +import { deselectCurrentNode, getCursorPosition, isImageURL } from '../../shared'; interface UploadNode { view: EditorView; diff --git a/core-web/libs/block-editor/src/lib/extensions/bubble-link-form/bubble-link-form.component.ts b/core-web/libs/block-editor/src/lib/extensions/bubble-link-form/bubble-link-form.component.ts index 665b4db9dc42..4f6c1ffdd714 100644 --- a/core-web/libs/block-editor/src/lib/extensions/bubble-link-form/bubble-link-form.component.ts +++ b/core-web/libs/block-editor/src/lib/extensions/bubble-link-form/bubble-link-form.component.ts @@ -12,12 +12,13 @@ import { FormBuilder, FormGroup } from '@angular/forms'; import { debounceTime, take } from 'rxjs/operators'; -import { DotCMSContentlet } from '@dotcms/dotcms-models'; +import { DotLanguagesService } from '@dotcms/data-access'; +import { DotCMSContentlet, DotLanguage } from '@dotcms/dotcms-models'; import { SuggestionPageComponent } from './components/suggestion-page/suggestion-page.component'; -import { Languages, SuggestionsCommandProps } from '../../shared'; -import { DotLanguageService, SuggestionsService } from '../../shared/services'; +import { SuggestionsCommandProps } from '../../shared'; +import { SuggestionsService } from '../../shared/services'; import { DEFAULT_LANG_ID } from '../bubble-menu/models'; import { isValidURL } from '../bubble-menu/utils'; @@ -48,7 +49,7 @@ export class BubbleLinkFormComponent implements OnInit { }; private minChars = 3; - private dotLangs: Languages; + private dotLangs: { [key: string]: DotLanguage } = {}; loading = false; form: FormGroup; @@ -87,7 +88,7 @@ export class BubbleLinkFormComponent implements OnInit { constructor( private fb: FormBuilder, private suggestionsService: SuggestionsService, - private dotLanguageService: DotLanguageService + private dotLanguagesService: DotLanguagesService ) {} ngOnInit() { @@ -110,10 +111,12 @@ export class BubbleLinkFormComponent implements OnInit { this.setNodeProps.emit({ link: this.currentLink, blank }) ); - this.dotLanguageService - .getLanguages() + this.dotLanguagesService + .get() .pipe(take(1)) - .subscribe((dotLang) => (this.dotLangs = dotLang)); + .subscribe((dotLang) => { + dotLang.forEach((lang) => (this.dotLangs[lang.id] = lang)); + }); } /** diff --git a/core-web/libs/block-editor/src/lib/extensions/floating-button/floating-button.component.ts b/core-web/libs/block-editor/src/lib/extensions/floating-button/floating-button.component.ts index 1fef00a9dc37..5cf86ffa8336 100644 --- a/core-web/libs/block-editor/src/lib/extensions/floating-button/floating-button.component.ts +++ b/core-web/libs/block-editor/src/lib/extensions/floating-button/floating-button.component.ts @@ -1,6 +1,6 @@ import { Component, EventEmitter, Input, Output } from '@angular/core'; -import { FileStatus } from '../../shared/services/dot-upload-file/dot-upload-file.service'; +import { FileStatus } from '@dotcms/data-access'; @Component({ selector: 'dot-floating-button', diff --git a/core-web/libs/block-editor/src/lib/extensions/floating-button/floating-button.extension.ts b/core-web/libs/block-editor/src/lib/extensions/floating-button/floating-button.extension.ts index 140791a1e400..9a0fca748d32 100644 --- a/core-web/libs/block-editor/src/lib/extensions/floating-button/floating-button.extension.ts +++ b/core-web/libs/block-editor/src/lib/extensions/floating-button/floating-button.extension.ts @@ -4,11 +4,11 @@ import { Injector, ViewContainerRef } from '@angular/core'; import { Extension } from '@tiptap/core'; +import { DotUploadFileService } from '@dotcms/data-access'; + import { FloatingButtonComponent } from './floating-button.component'; import { DotFloatingButtonPlugin } from './plugin/floating-button.plugin'; -import { DotUploadFileService } from '../../shared'; - export const FLOATING_BUTTON_PLUGIN_KEY = new PluginKey('floating-button'); export function DotFloatingButton(injector: Injector, viewContainerRef: ViewContainerRef) { diff --git a/core-web/libs/block-editor/src/lib/extensions/floating-button/plugin/floating-button.plugin.ts b/core-web/libs/block-editor/src/lib/extensions/floating-button/plugin/floating-button.plugin.ts index 20f1b08f2ade..f847f62cea7d 100644 --- a/core-web/libs/block-editor/src/lib/extensions/floating-button/plugin/floating-button.plugin.ts +++ b/core-web/libs/block-editor/src/lib/extensions/floating-button/plugin/floating-button.plugin.ts @@ -9,10 +9,10 @@ import { take, takeUntil, tap } from 'rxjs/operators'; import { Editor } from '@tiptap/core'; +import { DotUploadFileService, FileStatus } from '@dotcms/data-access'; import { DotCMSContentlet } from '@dotcms/dotcms-models'; import { ImageNode } from '../../../nodes'; -import { DotUploadFileService, FileStatus } from '../../../shared'; import { getNodeCoords } from '../../bubble-menu/utils'; import { FloatingButtonComponent } from '../floating-button.component'; diff --git a/core-web/libs/block-editor/src/lib/shared/components/suggestions/suggestions.component.ts b/core-web/libs/block-editor/src/lib/shared/components/suggestions/suggestions.component.ts index 101286b359da..6b05defa793a 100644 --- a/core-web/libs/block-editor/src/lib/shared/components/suggestions/suggestions.component.ts +++ b/core-web/libs/block-editor/src/lib/shared/components/suggestions/suggestions.component.ts @@ -13,11 +13,13 @@ import { MenuItem } from 'primeng/api'; import { map, take } from 'rxjs/operators'; -import { DotCMSContentlet, DotCMSContentType } from '@dotcms/dotcms-models'; +import { DotLanguagesService } from '@dotcms/data-access'; +import { DotCMSContentlet, DotCMSContentType, DotLanguage } from '@dotcms/dotcms-models'; import { DEFAULT_LANG_ID } from '../../../extensions'; -import { DotLanguageService, Languages, SuggestionsService } from '../../services'; +import { SuggestionsService } from '../../services'; import { SuggestionListComponent } from '../suggestion-list/suggestion-list.component'; + export interface SuggestionsCommandProps { payload?: DotCMSContentlet; type: { name: string; level?: number }; @@ -56,7 +58,7 @@ export class SuggestionsComponent implements OnInit { private itemsLoaded: ItemsType; private selectedContentType: DotCMSContentType; - private dotLangs: Languages; + private dotLangs: { [key: string]: DotLanguage } = {}; private initialItems: DotMenuItem[]; isFilterActive = false; @@ -74,17 +76,19 @@ export class SuggestionsComponent implements OnInit { constructor( private suggestionsService: SuggestionsService, - private dotLanguageService: DotLanguageService, + private dotLanguagesService: DotLanguagesService, private cd: ChangeDetectorRef ) {} ngOnInit(): void { this.initialItems = this.items; this.itemsLoaded = ItemsType.BLOCK; - this.dotLanguageService - .getLanguages() + this.dotLanguagesService + .get() .pipe(take(1)) - .subscribe((dotLang) => (this.dotLangs = dotLang)); + .subscribe((dotLang) => { + dotLang.forEach((lang) => (this.dotLangs[lang.id] = lang)); + }); } /** diff --git a/core-web/libs/block-editor/src/lib/shared/services/dot-language/dot-language.service.spec.ts b/core-web/libs/block-editor/src/lib/shared/services/dot-language/dot-language.service.spec.ts deleted file mode 100644 index e585858c5689..000000000000 --- a/core-web/libs/block-editor/src/lib/shared/services/dot-language/dot-language.service.spec.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { TestBed } from '@angular/core/testing'; - -import { DotLanguageService } from './dot-language.service'; - -describe('DotLanguageService', () => { - let service: DotLanguageService; - - beforeEach(() => { - TestBed.configureTestingModule({}); - service = TestBed.inject(DotLanguageService); - }); - - it('should be created', () => { - expect(service).toBeTruthy(); - }); -}); diff --git a/core-web/libs/block-editor/src/lib/shared/services/dot-language/dot-language.service.ts b/core-web/libs/block-editor/src/lib/shared/services/dot-language/dot-language.service.ts deleted file mode 100644 index c4653637072a..000000000000 --- a/core-web/libs/block-editor/src/lib/shared/services/dot-language/dot-language.service.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { Observable, of } from 'rxjs'; - -import { HttpClient, HttpHeaders } from '@angular/common/http'; -import { Injectable } from '@angular/core'; - -import { map, pluck } from 'rxjs/operators'; - -interface DotLanguage { - country: string; - countryCode: string; - defaultLanguage: boolean; - id: number; - language: string; - languageCode: string; -} - -export interface Languages { - [key: string]: DotLanguage; -} - -@Injectable({ - providedIn: 'root' -}) -export class DotLanguageService { - private languages: Languages; - - constructor(private http: HttpClient) {} - - get defaultHeaders() { - const headers = new HttpHeaders(); - headers.set('Accept', '*/*').set('Content-Type', 'application/json'); - - return headers; - } - - /** - * Get an object of DotLanguage whose keys are the languageId. - * - * @return {*} {Observable} - * @memberof DotLanguageService - */ - getLanguages(): Observable { - if (this.languages) { - return of(this.languages); - } - - return this.http - .get(`/api/v2/languages`, { - headers: this.defaultHeaders - }) - .pipe( - pluck('entity'), - map((lang: DotLanguage[]) => { - const dotLang: Languages = this.getDotLanguageObject(lang); - - this.languages = dotLang; - - return dotLang; - }) - ); - } - - /** - * Transform an array of languages into an object - * using the language id as an object key. - * @private - * @param {DotLanguage[]} lang - * @return {*} {Languages} - * @memberof DotLanguageService - */ - private getDotLanguageObject(lang: DotLanguage[]): Languages { - return lang.reduce((obj, lang) => Object.assign(obj, { [lang.id]: lang }), {}); - } -} diff --git a/core-web/libs/block-editor/src/lib/shared/services/dot-upload-file/dot-upload-file.service.spec.ts b/core-web/libs/block-editor/src/lib/shared/services/dot-upload-file/dot-upload-file.service.spec.ts deleted file mode 100644 index 19f9a0fe3913..000000000000 --- a/core-web/libs/block-editor/src/lib/shared/services/dot-upload-file/dot-upload-file.service.spec.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { TestBed } from '@angular/core/testing'; - -import { DotUploadFileService } from './dot-upload-file.service'; - -describe('DotUploadFileService', () => { - let service: DotUploadFileService; - - beforeEach(() => { - TestBed.configureTestingModule({ teardown: { destroyAfterEach: false } }); - service = TestBed.inject(DotUploadFileService); - }); - - it('should be created', () => { - expect(service).toBeTruthy(); - }); -}); diff --git a/core-web/libs/block-editor/src/lib/shared/services/index.ts b/core-web/libs/block-editor/src/lib/shared/services/index.ts index 57be502f8713..13bcfd4afc9a 100644 --- a/core-web/libs/block-editor/src/lib/shared/services/index.ts +++ b/core-web/libs/block-editor/src/lib/shared/services/index.ts @@ -1,6 +1,3 @@ -export * from './dot-language/dot-language.service'; export * from './dot-marketing-config/dot-marketing-config.service'; -export * from './dot-upload-file/dot-upload-file.service'; -export * from './search/search.service'; export * from './suggestions/suggestions.service'; export * from './dot-ai/dot-ai.service'; diff --git a/core-web/libs/data-access/src/index.ts b/core-web/libs/data-access/src/index.ts index 3489fae94057..e92ee7844e21 100644 --- a/core-web/libs/data-access/src/index.ts +++ b/core-web/libs/data-access/src/index.ts @@ -11,6 +11,7 @@ export * from './lib/dot-current-user/dot-current-user.service'; export * from './lib/dot-devices/dot-devices.service'; export * from './lib/dot-edit-page/dot-edit-page.service'; export * from './lib/dot-es-content/dot-es-content.service'; +export * from './lib/dot-content-search/dot-content-search.service'; export * from './lib/dot-events/dot-events.service'; export * from './lib/dot-format-date/dot-format-date.service'; export * from './lib/dot-generate-secure-password/dot-generate-secure-password.service'; @@ -34,6 +35,7 @@ export * from './lib/dot-site-browser/dot-site-browser.service'; export * from './lib/dot-tags/dot-tags.service'; export * from './lib/dot-themes/dot-themes.service'; export * from './lib/dot-upload/dot-upload.service'; +export * from './lib/dot-upload-file/dot-upload-file.service'; export * from './lib/dot-versionable/dot-versionable.service'; export * from './lib/dot-wizard/dot-wizard.service'; export * from './lib/dot-workflow-actions-fire/dot-workflow-actions-fire.service'; diff --git a/core-web/libs/data-access/src/lib/dot-content-search/dot-content-search.service.spec.ts b/core-web/libs/data-access/src/lib/dot-content-search/dot-content-search.service.spec.ts new file mode 100644 index 000000000000..b6ffb41e7c96 --- /dev/null +++ b/core-web/libs/data-access/src/lib/dot-content-search/dot-content-search.service.spec.ts @@ -0,0 +1,36 @@ +import { createHttpFactory, HttpMethod, SpectatorHttp } from '@ngneat/spectator/jest'; + +import { DotContentSearchService, EsQueryParamsSearch } from './dot-content-search.service'; + +describe('DotContentSearchService', () => { + let spectator: SpectatorHttp; + const createHttp = createHttpFactory(DotContentSearchService); + + beforeEach(() => (spectator = createHttp())); + + it('should call the search method with the right EsQueryParamsSearch', (done) => { + const params: EsQueryParamsSearch = { + query: 'test', + limit: 10, + offset: 0 + }; + + spectator.service.get(params).subscribe((resp) => { + expect(resp).toEqual({ contentlets: [] }); + done(); + }); + + const req = spectator.expectOne('/api/content/_search', HttpMethod.POST); + expect(req.request.body).toEqual({ + query: 'test', + sort: 'score,modDate desc', + limit: 10, + offset: 0 + }); + req.flush({ + entity: { + contentlets: [] + } + }); + }); +}); diff --git a/core-web/libs/block-editor/src/lib/shared/services/search/search.service.ts b/core-web/libs/data-access/src/lib/dot-content-search/dot-content-search.service.ts similarity index 84% rename from core-web/libs/block-editor/src/lib/shared/services/search/search.service.ts rename to core-web/libs/data-access/src/lib/dot-content-search/dot-content-search.service.ts index 4ddece725ba4..cf9fb8c05846 100644 --- a/core-web/libs/block-editor/src/lib/shared/services/search/search.service.ts +++ b/core-web/libs/data-access/src/lib/dot-content-search/dot-content-search.service.ts @@ -5,12 +5,12 @@ import { Injectable } from '@angular/core'; import { pluck } from 'rxjs/operators'; -export enum ESOrderDirection { +export enum ESOrderDirectionSearch { ASC = 'ASC', DESC = 'DESC' } -export interface EsQueryParams { +export interface EsQueryParamsSearch { itemsPerPage?: number; filter?: string; lang?: string; @@ -18,13 +18,13 @@ export interface EsQueryParams { query: string; sortField?: string; limit?: number; - sortOrder?: ESOrderDirection; + sortOrder?: ESOrderDirectionSearch; } @Injectable({ providedIn: 'root' }) -export class SearchService { +export class DotContentSearchService { constructor(private http: HttpClient) {} /** @@ -33,7 +33,7 @@ export class SearchService { * @returns Observable * @memberof DotESContentService */ - public get({ query, limit = 0, offset = 0 }: EsQueryParams): Observable { + public get({ query, limit = 0, offset = 0 }: EsQueryParamsSearch): Observable { return this.http .post('/api/content/_search', { query, diff --git a/core-web/libs/data-access/src/lib/dot-upload-file/dot-upload-file.service.spec.ts b/core-web/libs/data-access/src/lib/dot-upload-file/dot-upload-file.service.spec.ts new file mode 100644 index 000000000000..e49dbf3f3ae4 --- /dev/null +++ b/core-web/libs/data-access/src/lib/dot-upload-file/dot-upload-file.service.spec.ts @@ -0,0 +1,27 @@ +import { createServiceFactory, SpectatorService } from '@ngneat/spectator'; + +import { HttpClientTestingModule } from '@angular/common/http/testing'; + +import { DotUploadFileService } from './dot-upload-file.service'; + +import { DotUploadService } from '../dot-upload/dot-upload.service'; + +describe('DotUploadFileService', () => { + let spectator: SpectatorService; + let service: DotUploadFileService; + + const createService = createServiceFactory({ + service: DotUploadFileService, + imports: [HttpClientTestingModule], + providers: [DotUploadService] + }); + + beforeEach(() => { + spectator = createService(); + service = spectator.service; + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/core-web/libs/block-editor/src/lib/shared/services/dot-upload-file/dot-upload-file.service.ts b/core-web/libs/data-access/src/lib/dot-upload-file/dot-upload-file.service.ts similarity index 97% rename from core-web/libs/block-editor/src/lib/shared/services/dot-upload-file/dot-upload-file.service.ts rename to core-web/libs/data-access/src/lib/dot-upload-file/dot-upload-file.service.ts index cc9a8c5904c2..856438adf157 100644 --- a/core-web/libs/block-editor/src/lib/shared/services/dot-upload-file/dot-upload-file.service.ts +++ b/core-web/libs/data-access/src/lib/dot-upload-file/dot-upload-file.service.ts @@ -44,7 +44,7 @@ export class DotUploadFileService { return this.setTempResource({ data, maxSize, signal }).pipe( switchMap((response: DotCMSTempFile | DotCMSTempFile[]) => { const files = Array.isArray(response) ? response : [response]; - const contentlets = []; + const contentlets: Record[] = []; files.forEach((file: DotCMSTempFile) => { contentlets.push({ baseType: 'dotAsset', 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 333f08b83358..2b43a30a0cb3 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 @@ -31,7 +31,7 @@ import { DotEditContentSelectFieldComponent } from '../../fields/dot-edit-conten 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 { DotWYSIWYGFieldComponent } from '../../fields/dot-wysiwyg-field/dot-wysiwyg-field.component'; +import { DotEditContentWYSIWYGFieldComponent } from '../../fields/dot-edit-content-wysiwyg-field/dot-edit-content-wysiwyg-field.component'; import { FIELD_TYPES } from '../../models/dot-edit-content-field.enum'; import { DotEditContentService } from '../../services/dot-edit-content.service'; import { @@ -126,7 +126,7 @@ const FIELD_TYPES_COMPONENTS: Record | DotEditFieldTe providers: [mockProvider(DotMessageDisplayService)] }, [FIELD_TYPES.WYSIWYG]: { - component: DotWYSIWYGFieldComponent, + component: DotEditContentWYSIWYGFieldComponent, declarations: [MockComponent(EditorComponent)] } }; diff --git a/core-web/libs/edit-content/src/lib/components/dot-edit-content-field/dot-edit-content-field.component.ts b/core-web/libs/edit-content/src/lib/components/dot-edit-content-field/dot-edit-content-field.component.ts index 34ebe3afe029..58cd844fc6cb 100644 --- a/core-web/libs/edit-content/src/lib/components/dot-edit-content-field/dot-edit-content-field.component.ts +++ b/core-web/libs/edit-content/src/lib/components/dot-edit-content-field/dot-edit-content-field.component.ts @@ -9,7 +9,7 @@ import { DotFieldRequiredDirective } from '@dotcms/ui'; import { DotEditContentBinaryFieldComponent } from '../../fields/dot-edit-content-binary-field/dot-edit-content-binary-field.component'; import { DotEditContentFieldsModule } from '../../fields/dot-edit-content-fields.module'; import { DotEditContentKeyValueComponent } from '../../fields/dot-edit-content-key-value/dot-edit-content-key-value.component'; -import { DotWYSIWYGFieldComponent } from '../../fields/dot-wysiwyg-field/dot-wysiwyg-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'; @@ -35,7 +35,7 @@ import { FIELD_TYPES } from '../../models/dot-edit-content-field.enum'; BlockEditorModule, DotEditContentBinaryFieldComponent, DotEditContentKeyValueComponent, - DotWYSIWYGFieldComponent + DotEditContentWYSIWYGFieldComponent ] }) export class DotEditContentFieldComponent { diff --git a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-fields.module.ts b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-fields.module.ts index d764475ea7f7..42f186a479d5 100644 --- a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-fields.module.ts +++ b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-fields.module.ts @@ -11,6 +11,7 @@ import { DotEditContentSelectFieldComponent } from './dot-edit-content-select-fi import { DotEditContentTagFieldComponent } from './dot-edit-content-tag-field/dot-edit-content-tag-field.component'; import { DotEditContentTextAreaComponent } from './dot-edit-content-text-area/dot-edit-content-text-area.component'; import { DotEditContentTextFieldComponent } from './dot-edit-content-text-field/dot-edit-content-text-field.component'; +import { DotEditContentWYSIWYGFieldComponent } from './dot-edit-content-wysiwyg-field/dot-edit-content-wysiwyg-field.component'; @NgModule({ declarations: [], @@ -25,7 +26,8 @@ import { DotEditContentTextFieldComponent } from './dot-edit-content-text-field/ DotEditContentMultiSelectFieldComponent, DotEditContentBinaryFieldComponent, DotEditContentJsonFieldComponent, - DotEditContentCustomFieldComponent + DotEditContentCustomFieldComponent, + DotEditContentWYSIWYGFieldComponent ], exports: [ DotEditContentTextAreaComponent, @@ -38,7 +40,8 @@ import { DotEditContentTextFieldComponent } from './dot-edit-content-text-field/ DotEditContentMultiSelectFieldComponent, DotEditContentBinaryFieldComponent, DotEditContentJsonFieldComponent, - DotEditContentCustomFieldComponent + DotEditContentCustomFieldComponent, + DotEditContentWYSIWYGFieldComponent ] }) export class DotEditContentFieldsModule {} diff --git a/core-web/libs/edit-content/src/lib/fields/dot-wysiwyg-field/dot-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 similarity index 77% rename from core-web/libs/edit-content/src/lib/fields/dot-wysiwyg-field/dot-wysiwyg-field.component.html rename to core-web/libs/edit-content/src/lib/fields/dot-edit-content-wysiwyg-field/dot-edit-content-wysiwyg-field.component.html index 4b35e94d954b..cae5d952ede0 100644 --- a/core-web/libs/edit-content/src/lib/fields/dot-wysiwyg-field/dot-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 @@ -2,4 +2,4 @@ [formControlName]="field.variable" [plugins]="plugins()" [toolbar]="toolbar()" - [init]="init"> + [init]="init" /> diff --git a/core-web/libs/edit-content/src/lib/fields/dot-wysiwyg-field/dot-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 similarity index 100% rename from core-web/libs/edit-content/src/lib/fields/dot-wysiwyg-field/dot-wysiwyg-field.component.scss rename to core-web/libs/edit-content/src/lib/fields/dot-edit-content-wysiwyg-field/dot-edit-content-wysiwyg-field.component.scss 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 new file mode 100644 index 000000000000..fdc7836247bc --- /dev/null +++ b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-wysiwyg-field/dot-edit-content-wysiwyg-field.component.spec.ts @@ -0,0 +1,89 @@ +import { expect } from '@jest/globals'; +import { Spectator, createComponentFactory } from '@ngneat/spectator'; +import { EditorComponent, EditorModule } from '@tinymce/tinymce-angular'; +import { MockComponent, MockService } from 'ng-mocks'; +import { Editor } from 'tinymce'; + +import { + ControlContainer, + FormGroupDirective, + FormsModule, + ReactiveFormsModule +} from '@angular/forms'; + +import { DialogService } from 'primeng/dynamicdialog'; + +import { DotUploadFileService } from '@dotcms/data-access'; + +import { DotEditContentWYSIWYGFieldComponent } from './dot-edit-content-wysiwyg-field.component'; +import { DotWysiwygPluginService } from './dot-wysiwyg-plugin/dot-wysiwyg-plugin.service'; + +import { WYSIWYG_MOCK, createFormGroupDirectiveMock } from '../../utils/mocks'; + +const ALL_PLUGINS = + 'advlist autolink lists link image charmap preview anchor pagebreak searchreplace wordcount visualblocks visualchars code fullscreen insertdatetime media nonbreaking save table directionality emoticons template'; +const ALL_TOOLBAR_ITEMS = + 'undo redo | bold italic | alignleft aligncenter alignright alignjustify | bullist numlist outdent indent dotAddImage hr'; + +describe('DotEditContentWYSIWYGFieldComponent', () => { + let spectator: Spectator; + let dotWysiwygPluginService: DotWysiwygPluginService; + + const createComponent = createComponentFactory({ + component: DotEditContentWYSIWYGFieldComponent, + imports: [EditorModule, FormsModule, ReactiveFormsModule], + declarations: [MockComponent(EditorComponent)], + componentViewProviders: [ + { + provide: DotWysiwygPluginService, + useValue: { + initializePlugins: jest.fn() + } + }, + { + provide: ControlContainer, + useValue: createFormGroupDirectiveMock() + } + ], + providers: [ + FormGroupDirective, + DialogService, + { + provide: DotUploadFileService, + useValue: MockService(DotUploadFileService) + } + ] + }); + + beforeEach(() => { + spectator = createComponent({ + props: { + field: WYSIWYG_MOCK + } + }); + + dotWysiwygPluginService = spectator.inject(DotWysiwygPluginService, true); + }); + + it('should instance WYSIWYG editor and set the correct plugins and toolbar items', () => { + const editor = spectator.query(EditorComponent); + expect(editor).toBeTruthy(); + expect(editor.plugins).toEqual(ALL_PLUGINS); + expect(editor.toolbar).toEqual(ALL_TOOLBAR_ITEMS); + expect(editor.init).toEqual({ + menubar: false, + image_caption: true, + image_advtab: true, + contextmenu: 'align link image', + setup: expect.any(Function) + }); + }); + + it('should initialize Plugins when the setup method is called', () => { + const spy = jest.spyOn(dotWysiwygPluginService, 'initializePlugins'); + const editor = spectator.query(EditorComponent); + const mockEditor = {} as Editor; + editor.init.setup(mockEditor); + expect(spy).toHaveBeenCalledWith(mockEditor); + }); +}); diff --git a/core-web/libs/edit-content/src/lib/fields/dot-wysiwyg-field/dot-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 similarity index 55% rename from core-web/libs/edit-content/src/lib/fields/dot-wysiwyg-field/dot-wysiwyg-field.component.ts rename to core-web/libs/edit-content/src/lib/fields/dot-edit-content-wysiwyg-field/dot-edit-content-wysiwyg-field.component.ts index 9ad9612e0e91..64de5360bde2 100644 --- a/core-web/libs/edit-content/src/lib/fields/dot-wysiwyg-field/dot-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 @@ -4,16 +4,24 @@ import { RawEditorOptions } from 'tinymce'; import { ChangeDetectionStrategy, Component, Input, inject, signal } from '@angular/core'; import { ControlContainer, FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { DialogService } from 'primeng/dynamicdialog'; + import { DotCMSContentTypeField } from '@dotcms/dotcms-models'; +import { DotWysiwygPluginService } from './dot-wysiwyg-plugin/dot-wysiwyg-plugin.service'; + @Component({ selector: 'dot-wysiwyg-field', standalone: true, imports: [EditorModule, FormsModule, ReactiveFormsModule], - templateUrl: './dot-wysiwyg-field.component.html', - styleUrl: './dot-wysiwyg-field.component.scss', + templateUrl: './dot-edit-content-wysiwyg-field.component.html', + styleUrl: './dot-edit-content-wysiwyg-field.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, - providers: [{ provide: TINYMCE_SCRIPT_SRC, useValue: 'tinymce/tinymce.min.js' }], + providers: [ + DialogService, + DotWysiwygPluginService, + { provide: TINYMCE_SCRIPT_SRC, useValue: 'tinymce/tinymce.min.js' } + ], viewProviders: [ { provide: ControlContainer, @@ -21,17 +29,24 @@ import { DotCMSContentTypeField } from '@dotcms/dotcms-models'; } ] }) -export class DotWYSIWYGFieldComponent { +export class DotEditContentWYSIWYGFieldComponent { @Input() field!: DotCMSContentTypeField; + private readonly dotWysiwygPluginService = inject(DotWysiwygPluginService); + protected readonly init: RawEditorOptions = { - menubar: false + menubar: false, + image_caption: true, + image_advtab: true, + contextmenu: 'align link image', + setup: (editor) => this.dotWysiwygPluginService.initializePlugins(editor) }; protected readonly plugins = signal( 'advlist autolink lists link image charmap preview anchor pagebreak searchreplace wordcount visualblocks visualchars code fullscreen insertdatetime media nonbreaking save table directionality emoticons template' ); + protected readonly toolbar = signal( - 'insertfile undo redo | styleselect | bold italic | alignleft aligncenter alignright alignjustify | bullist numlist outdent indent hr' + 'undo redo | bold italic | alignleft aligncenter alignright alignjustify | bullist numlist outdent indent dotAddImage hr' ); } diff --git a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-wysiwyg-field/dot-wysiwyg-plugin/dot-wysiwyg-plugin.service.spec.ts b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-wysiwyg-field/dot-wysiwyg-plugin/dot-wysiwyg-plugin.service.spec.ts new file mode 100644 index 000000000000..fef6071ebfd0 --- /dev/null +++ b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-wysiwyg-field/dot-wysiwyg-plugin/dot-wysiwyg-plugin.service.spec.ts @@ -0,0 +1,199 @@ +import { expect } from '@jest/globals'; +import { createServiceFactory, SpectatorService } from '@ngneat/spectator'; +import { MockComponent } from 'ng-mocks'; +import { of } from 'rxjs'; + +import { DialogService, DynamicDialogRef } from 'primeng/dynamicdialog'; + +import { DotUploadFileService } from '@dotcms/data-access'; +import { DotCMSContentlet } from '@dotcms/dotcms-models'; +import { DotAssetSearchDialogComponent } from '@dotcms/ui'; +import { EMPTY_CONTENTLET } from '@dotcms/utils-testing'; + +import { DotWysiwygPluginService } from './dot-wysiwyg-plugin.service'; +import { formatDotImageNode } from './utils/editor.utils'; + +/** + * This Mock is used to check we are sending the correct configuration to the editor + * No need to mock all the methods and properties of the Editor + * Some methods are customized to check the configuration + */ +class MockEditor { + private customButtons = {}; + private events = {}; + + ui = { + registry: { + getAll: () => { + return { + buttons: this.customButtons + }; + }, + addButton: (name, config) => { + this.customButtons[name] = config; + } + } + }; + + on = (name, fn) => { + if (!this.events[name]) { + this.events[name] = [fn]; + + return; + } + + this.events[name].push(fn); + }; + + fakeOnCall = (name, event) => { + this.events[name].forEach((fn) => fn(event)); + }; + + insertContent = jest.fn(); +} + +describe('DotWysiwygPluginService', () => { + let spectator: SpectatorService; + let dialogService: DialogService; + let dotUploadFileService: DotUploadFileService; + /** + * `any` is used here because the Editor is a complex object that we don't need to mock all the methods and properties + * This mock also contains some custom methods to check the configuration + * We are using this mock to check the configuration of the editor + */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + let editor: any; + + const createService = createServiceFactory({ + service: DotWysiwygPluginService, + declarations: [MockComponent(DotAssetSearchDialogComponent)], + providers: [ + DialogService, + { + provide: DotUploadFileService, + useValue: { + publishContent: jest.fn() + } + } + ] + }); + + beforeEach(() => { + spectator = createService(); + dialogService = spectator.inject(DialogService); + dotUploadFileService = spectator.inject(DotUploadFileService); + editor = new MockEditor(); + }); + + describe('dotImagePlugin', () => { + it('should configure the dotAddImage button', () => { + const spyButton = jest.spyOn(editor.ui.registry, 'addButton'); + const spyOn = jest.spyOn(editor, 'on'); + + spectator.service.initializePlugins(editor); + + expect(spyOn).toHaveBeenCalledWith('drop', expect.any(Function)); + expect(spyButton).toHaveBeenCalledWith('dotAddImage', { + icon: 'image', + onAction: expect.any(Function) + }); + }); + + it('should open the dialog when the button is clicked', () => { + const spyDialog = jest.spyOn(dialogService, 'open').mockReturnValue({ + onClose: of(EMPTY_CONTENTLET) + } as DynamicDialogRef); + + const spyEditorInserContent = jest.spyOn(editor, 'insertContent'); + + spectator.service.initializePlugins(editor); + + const button = editor.ui.registry.getAll().buttons['dotAddImage']; + const dialogConfig = { + header: 'Insert Image', + width: '800px', + height: '500px', + contentStyle: { padding: 0 }, + data: { + assetType: 'image' + } + }; + + // Simulate the button click + button.onAction(); + + expect(spyDialog).toHaveBeenCalledWith(DotAssetSearchDialogComponent, dialogConfig); + expect(spyEditorInserContent).toHaveBeenCalledWith( + formatDotImageNode(EMPTY_CONTENTLET) + ); + }); + + it('should upload the image when dropped', () => { + const uploadRespMock: unknown = [{ '1234': EMPTY_CONTENTLET }]; + const spyUpload = jest + .spyOn(dotUploadFileService, 'publishContent') + .mockReturnValue(of(uploadRespMock as DotCMSContentlet[])); + const spyEditorInserContent = jest.spyOn(editor, 'insertContent'); + + spectator.service.initializePlugins(editor); + + const dropEvent = { + dataTransfer: { + files: [ + { + type: 'image/png' + } + ] + }, + preventDefault: jest.fn(), + stopImmediatePropagation: jest.fn(), + stopPropagation: jest.fn() + }; + + editor.fakeOnCall('drop', dropEvent); + + expect(spyUpload).toHaveBeenCalledWith({ + data: dropEvent.dataTransfer.files[0] + }); + expect(spyEditorInserContent).toHaveBeenCalledWith( + formatDotImageNode(EMPTY_CONTENTLET) + ); + + expect(dropEvent.preventDefault).toHaveBeenCalled(); + expect(dropEvent.stopImmediatePropagation).toHaveBeenCalled(); + expect(dropEvent.stopPropagation).toHaveBeenCalled(); + }); + + it('should not upload the image when dropped', () => { + const uploadRespMock: unknown = [{ '1234': EMPTY_CONTENTLET }]; + const spyUpload = jest + .spyOn(dotUploadFileService, 'publishContent') + .mockReturnValue(of(uploadRespMock as DotCMSContentlet[])); + const spyEditorInserContent = jest.spyOn(editor, 'insertContent'); + + spectator.service.initializePlugins(editor); + + const dropEvent = { + dataTransfer: { + files: [ + { + type: 'video/mp4' + } + ] + }, + preventDefault: jest.fn(), + stopImmediatePropagation: jest.fn(), + stopPropagation: jest.fn() + }; + + editor.fakeOnCall('drop', dropEvent); + + expect(spyUpload).not.toHaveBeenCalledWith({ + data: dropEvent.dataTransfer.files[0] + }); + expect(spyEditorInserContent).not.toHaveBeenCalledWith( + formatDotImageNode(EMPTY_CONTENTLET) + ); + }); + }); +}); diff --git a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-wysiwyg-field/dot-wysiwyg-plugin/dot-wysiwyg-plugin.service.ts b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-wysiwyg-field/dot-wysiwyg-plugin/dot-wysiwyg-plugin.service.ts new file mode 100644 index 000000000000..78aaed215f4e --- /dev/null +++ b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-wysiwyg-field/dot-wysiwyg-plugin/dot-wysiwyg-plugin.service.ts @@ -0,0 +1,76 @@ +import { Editor } from 'tinymce'; + +import { Injectable, NgZone, inject } from '@angular/core'; + +import { DialogService } from 'primeng/dynamicdialog'; + +import { filter } from 'rxjs/operators'; + +import { DotUploadFileService } from '@dotcms/data-access'; +import { DotCMSContentlet } from '@dotcms/dotcms-models'; +import { DotAssetSearchDialogComponent } from '@dotcms/ui'; + +import { formatDotImageNode } from './utils/editor.utils'; + +@Injectable() +export class DotWysiwygPluginService { + private readonly dialogService: DialogService = inject(DialogService); + private readonly dotUploadFileService: DotUploadFileService = inject(DotUploadFileService); + private readonly ngZone: NgZone = inject(NgZone); + + initializePlugins(editor: Editor): void { + this.dotImagePlugin(editor); + } + + private dotImagePlugin(editor: Editor): void { + editor.ui.registry.addButton('dotAddImage', { + icon: 'image', + onAction: () => { + this.ngZone.run(() => { + const ref = this.dialogService.open(DotAssetSearchDialogComponent, { + header: 'Insert Image', + width: '800px', + height: '500px', + contentStyle: { padding: 0 }, + data: { + assetType: 'image' + } + }); + + ref.onClose + .pipe(filter((asset) => !!asset)) + .subscribe((asset: DotCMSContentlet) => + editor.insertContent(formatDotImageNode(asset)) + ); + }); + } + }); + + this.dotFilePlugin(editor); + } + + private dotFilePlugin(editor: Editor) { + editor.on('drop', async (event) => { + const file = event.dataTransfer.files[0]; + + // Check if the file is an image + if (!file.type.includes('image')) { + return; + } + + event.preventDefault(); + event.stopImmediatePropagation(); + event.stopPropagation(); + + this.dotUploadFileService + .publishContent({ + data: file + }) + .subscribe((contentlets) => { + const data = contentlets[0]; + const asset = data[Object.keys(data)[0]]; + editor.insertContent(formatDotImageNode(asset)); + }); + }); + } +} diff --git a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-wysiwyg-field/dot-wysiwyg-plugin/utils/editor.utils.spec.ts b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-wysiwyg-field/dot-wysiwyg-plugin/utils/editor.utils.spec.ts new file mode 100644 index 000000000000..96c102f2227d --- /dev/null +++ b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-wysiwyg-field/dot-wysiwyg-plugin/utils/editor.utils.spec.ts @@ -0,0 +1,29 @@ +import { DotCMSContentlet } from '@dotcms/dotcms-models'; +import { EMPTY_CONTENTLET } from '@dotcms/utils-testing'; + +import { formatDotImageNode } from './editor.utils'; + +describe('formatDotImageNode', () => { + it('should return formatted image node', () => { + const asset: DotCMSContentlet = { + ...EMPTY_CONTENTLET, + assetVersion: 'version', + asset: 'asset', + title: 'title', + titleImage: 'titleImage', + inode: 'inode', + identifier: 'identifier' + }; + + const result = formatDotImageNode(asset); + + expect(result).toBe( + `${asset.title}` + ); + }); +}); diff --git a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-wysiwyg-field/dot-wysiwyg-plugin/utils/editor.utils.ts b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-wysiwyg-field/dot-wysiwyg-plugin/utils/editor.utils.ts new file mode 100644 index 000000000000..7945c8c26b82 --- /dev/null +++ b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-wysiwyg-field/dot-wysiwyg-plugin/utils/editor.utils.ts @@ -0,0 +1,10 @@ +import { DotCMSContentlet } from '@dotcms/dotcms-models'; + +export const formatDotImageNode = (asset: DotCMSContentlet) => { + return `${asset.title}`; +}; diff --git a/core-web/libs/edit-content/src/lib/fields/dot-wysiwyg-field/dot-wysiwyg-field.component.spec.ts b/core-web/libs/edit-content/src/lib/fields/dot-wysiwyg-field/dot-wysiwyg-field.component.spec.ts deleted file mode 100644 index dc40bd601a7d..000000000000 --- a/core-web/libs/edit-content/src/lib/fields/dot-wysiwyg-field/dot-wysiwyg-field.component.spec.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { Spectator, createComponentFactory } from '@ngneat/spectator'; -import { EditorComponent, EditorModule } from '@tinymce/tinymce-angular'; -import { MockComponent } from 'ng-mocks'; - -import { - ControlContainer, - FormGroupDirective, - FormsModule, - ReactiveFormsModule -} from '@angular/forms'; - -import { DotWYSIWYGFieldComponent } from './dot-wysiwyg-field.component'; - -import { WYSIWYG_MOCK, createFormGroupDirectiveMock } from '../../utils/mocks'; - -const ALL_PLUGINS = - 'advlist autolink lists link image charmap preview anchor pagebreak searchreplace wordcount visualblocks visualchars code fullscreen insertdatetime media nonbreaking save table directionality emoticons template'; -const ALL_TOOLBAR_ITEMS = - 'insertfile undo redo | styleselect | bold italic | alignleft aligncenter alignright alignjustify | bullist numlist outdent indent hr'; - -describe('DotWYSIWYGFieldComponent', () => { - let spectator: Spectator; - const createComponent = createComponentFactory({ - component: DotWYSIWYGFieldComponent, - imports: [EditorModule, FormsModule, ReactiveFormsModule], - declarations: [MockComponent(EditorComponent)], - componentViewProviders: [ - { - provide: ControlContainer, - useValue: createFormGroupDirectiveMock() - } - ], - providers: [FormGroupDirective] - }); - - beforeEach(() => { - spectator = createComponent({ - props: { - field: WYSIWYG_MOCK - } - }); - }); - - it('should instance WYSIWYG editor and set the correct plugins and toolbar items', () => { - const editor = spectator.query(EditorComponent); - expect(editor).toBeTruthy(); - expect(editor.plugins).toEqual(ALL_PLUGINS); - expect(editor.toolbar).toEqual(ALL_TOOLBAR_ITEMS); - }); -}); diff --git a/core-web/libs/ui/src/index.ts b/core-web/libs/ui/src/index.ts index 7d3ff7ca9731..37b3c4f808ec 100644 --- a/core-web/libs/ui/src/index.ts +++ b/core-web/libs/ui/src/index.ts @@ -4,6 +4,8 @@ export * from './lib/dot-spinner/dot-spinner.module'; export * from './lib/modules/dot-dialog/dot-dialog.module'; // Components +export * from './lib/components/dot-asset-search/dot-asset-search.component'; +export * from './lib/components/dot-asset-search/components/dot-asset-search-dialog/dot-asset-search-dialog.component'; export * from './lib/components/dot-binary-option-selector/dot-binary-option-selector.component'; export * from './lib/dot-spinner/dot-spinner.component'; export * from './lib/components/dot-drop-zone/dot-drop-zone.component'; diff --git a/core-web/libs/block-editor/src/lib/extensions/asset-form/components/dot-asset-search/components/dot-asset-card-list/dot-asset-card-list.component.html b/core-web/libs/ui/src/lib/components/dot-asset-search/components/dot-asset-card-list/dot-asset-card-list.component.html similarity index 83% rename from core-web/libs/block-editor/src/lib/extensions/asset-form/components/dot-asset-search/components/dot-asset-card-list/dot-asset-card-list.component.html rename to core-web/libs/ui/src/lib/components/dot-asset-search/components/dot-asset-card-list/dot-asset-card-list.component.html index 7bccac6cce94..df5314dbf7f2 100644 --- a/core-web/libs/block-editor/src/lib/extensions/asset-form/components/dot-asset-search/components/dot-asset-card-list/dot-asset-card-list.component.html +++ b/core-web/libs/ui/src/lib/components/dot-asset-search/components/dot-asset-card-list/dot-asset-card-list.component.html @@ -4,20 +4,17 @@ [items]="rows" [lazy]="true" (onScrollIndexChange)="onScrollIndexChange($event)" - scrollHeight="20rem" -> + scrollHeight="20rem">
+ (click)="selectedItem.emit(contentlet[0])"> + (click)="selectedItem.emit(contentlet[1])">
diff --git a/core-web/libs/block-editor/src/lib/extensions/asset-form/components/dot-asset-search/components/dot-asset-card-list/dot-asset-card-list.component.scss b/core-web/libs/ui/src/lib/components/dot-asset-search/components/dot-asset-card-list/dot-asset-card-list.component.scss similarity index 100% rename from core-web/libs/block-editor/src/lib/extensions/asset-form/components/dot-asset-search/components/dot-asset-card-list/dot-asset-card-list.component.scss rename to core-web/libs/ui/src/lib/components/dot-asset-search/components/dot-asset-card-list/dot-asset-card-list.component.scss diff --git a/core-web/libs/ui/src/lib/components/dot-asset-search/components/dot-asset-card-list/dot-asset-card-list.component.spec.ts b/core-web/libs/ui/src/lib/components/dot-asset-search/components/dot-asset-card-list/dot-asset-card-list.component.spec.ts new file mode 100644 index 000000000000..329d9eaa9697 --- /dev/null +++ b/core-web/libs/ui/src/lib/components/dot-asset-search/components/dot-asset-card-list/dot-asset-card-list.component.spec.ts @@ -0,0 +1,34 @@ +import { createComponentFactory, Spectator } from '@ngneat/spectator'; + +import { CommonModule } from '@angular/common'; + +import { ScrollerModule } from 'primeng/scroller'; + +import { DotAssetCardListComponent } from './dot-asset-card-list.component'; + +import { DotAssetCardComponent } from '../dot-asset-card/dot-asset-card.component'; +import { DotAssetCardSkeletonComponent } from '../dot-asset-card-skeleton/dot-asset-card-skeleton.component'; + +describe('DotAssetCardListComponent', () => { + let spectator: Spectator; + let component: DotAssetCardListComponent; + + const createComponent = createComponentFactory({ + component: DotAssetCardListComponent, + imports: [ + CommonModule, + ScrollerModule, + DotAssetCardComponent, + DotAssetCardSkeletonComponent + ] + }); + + beforeEach(() => { + spectator = createComponent(); + component = spectator.component; + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/core-web/libs/ui/src/lib/components/dot-asset-search/components/dot-asset-card-list/dot-asset-card-list.component.ts b/core-web/libs/ui/src/lib/components/dot-asset-search/components/dot-asset-card-list/dot-asset-card-list.component.ts new file mode 100644 index 000000000000..4a51c189c4ed --- /dev/null +++ b/core-web/libs/ui/src/lib/components/dot-asset-search/components/dot-asset-card-list/dot-asset-card-list.component.ts @@ -0,0 +1,94 @@ +import { CommonModule } from '@angular/common'; +import { + ChangeDetectionStrategy, + Component, + EventEmitter, + Input, + OnChanges, + Output, + SimpleChanges, + inject +} from '@angular/core'; +import { DomSanitizer } from '@angular/platform-browser'; + +import { ScrollerModule } from 'primeng/scroller'; + +import { DotCMSContentlet } from '@dotcms/dotcms-models'; + +import { DotAssetCardComponent } from '../dot-asset-card/dot-asset-card.component'; +import { DotAssetCardSkeletonComponent } from '../dot-asset-card-skeleton/dot-asset-card-skeleton.component'; + +const squarePlus = + 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDAiIGhlaWdodD0iNDEiIHZpZXdCb3g9IjAgMCA0MCA0MSIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTQuMDc2MTcgOC41NTcxMkgwLjA5NTIxNDhWMzYuNDIzOEMwLjA5NTIxNDggMzguNjEzMyAxLjg4NjY0IDQwLjQwNDcgNC4wNzYxNyA0MC40MDQ3SDMxLjk0MjhWMzYuNDIzOEg0LjA3NjE3VjguNTU3MTJaTTM1LjkyMzggMC41OTUyMTVIMTIuMDM4MUM5Ljg0ODU1IDAuNTk1MjE1IDguMDU3MTIgMi4zODY2NCA4LjA1NzEyIDQuNTc2MTdWMjguNDYxOUM4LjA1NzEyIDMwLjY1MTQgOS44NDg1NSAzMi40NDI4IDEyLjAzODEgMzIuNDQyOEgzNS45MjM4QzM4LjExMzMgMzIuNDQyOCAzOS45MDQ3IDMwLjY1MTQgMzkuOTA0NyAyOC40NjE5VjQuNTc2MTdDMzkuOTA0NyAyLjM4NjY0IDM4LjExMzMgMC41OTUyMTUgMzUuOTIzOCAwLjU5NTIxNVpNMzUuOTIzOCAyOC40NjE5SDEyLjAzODFWNC41NzYxN0gzNS45MjM4VjI4LjQ2MTlaTTIxLjk5MDUgMjQuNDgwOUgyNS45NzE0VjE4LjUwOTVIMzEuOTQyOFYxNC41Mjg1SDI1Ljk3MTRWOC41NTcxMkgyMS45OTA1VjE0LjUyODVIMTYuMDE5VjE4LjUwOTVIMjEuOTkwNVYyNC40ODA5WiIgZmlsbD0iIzU3NkJFOCIvPgo8L3N2Zz4K'; + +@Component({ + selector: 'dot-asset-card-list', + templateUrl: './dot-asset-card-list.component.html', + styleUrls: ['./dot-asset-card-list.component.scss'], + standalone: true, + imports: [CommonModule, ScrollerModule, DotAssetCardComponent, DotAssetCardSkeletonComponent], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class DotAssetCardListComponent implements OnChanges { + @Output() nextBatch: EventEmitter = new EventEmitter(); + @Output() selectedItem: EventEmitter = new EventEmitter(); + + @Input() done = false; + @Input() loading = true; + @Input() contentlets: DotCMSContentlet[] = []; + + private domSanitizer: DomSanitizer = inject(DomSanitizer); + public loadingItems = [null, null, null]; + public icon = this.domSanitizer.bypassSecurityTrustResourceUrl(squarePlus); + private _itemRows: DotCMSContentlet[][] = []; + private _offset = 0; + + get rows() { + // Force Scroll to Update by breaking the object Reference + return [...this._itemRows]; + } + + ngOnChanges(changes: SimpleChanges) { + if (changes.contentlets) { + this._offset = this.contentlets?.length || 0; + this._itemRows = this.createRowItem(this.contentlets); + } + } + + onScrollIndexChange(e: { first: number; last: number }) { + if (this.done) { + return; + } + + const end = e.last; + const total = this.rows.length; + + if (end === total) { + // We multiply by 2 because of the way we should de images. + // Two images per row. + this.nextBatch.emit(this._offset); + } + } + + /** + * + * Create an array of type: [[DotCMSContentlet, DotCMSContentlet], ...] + * Due PrimeNg virtual scroll allows only displaying one element at a time [https://primefaces.org/primeng/virtualscroller], + * and figma's layout requires displaying two columns of contentlets [https://github.com/dotCMS/core/issues/23235] + * + * @private + * @param {DotCMSContentlet[][]} prev + * @param {DotCMSContentlet[]} contentlets + * @return {*} + * @memberof DotAssetSearchStore + */ + private createRowItem(contentlets: DotCMSContentlet[] = []) { + const rows = []; + contentlets.forEach((contentlet) => { + const i = rows.length - 1; + rows[i]?.length < 2 ? rows[i].push(contentlet) : rows.push([contentlet]); + }); + + return rows; + } +} diff --git a/core-web/libs/ui/src/lib/components/dot-asset-search/components/dot-asset-card-skeleton/dot-asset-card-skeleton.component.html b/core-web/libs/ui/src/lib/components/dot-asset-search/components/dot-asset-card-skeleton/dot-asset-card-skeleton.component.html new file mode 100644 index 000000000000..38ed289b7b79 --- /dev/null +++ b/core-web/libs/ui/src/lib/components/dot-asset-search/components/dot-asset-card-skeleton/dot-asset-card-skeleton.component.html @@ -0,0 +1,12 @@ + + + + + + +
+ + +
+
+
diff --git a/core-web/libs/block-editor/src/lib/extensions/asset-form/components/dot-asset-search/components/dot-asset-card-skeleton/dot-asset-card-skeleton.component.scss b/core-web/libs/ui/src/lib/components/dot-asset-search/components/dot-asset-card-skeleton/dot-asset-card-skeleton.component.scss similarity index 100% rename from core-web/libs/block-editor/src/lib/extensions/asset-form/components/dot-asset-search/components/dot-asset-card-skeleton/dot-asset-card-skeleton.component.scss rename to core-web/libs/ui/src/lib/components/dot-asset-search/components/dot-asset-card-skeleton/dot-asset-card-skeleton.component.scss diff --git a/core-web/libs/ui/src/lib/components/dot-asset-search/components/dot-asset-card-skeleton/dot-asset-card-skeleton.component.spec.ts b/core-web/libs/ui/src/lib/components/dot-asset-search/components/dot-asset-card-skeleton/dot-asset-card-skeleton.component.spec.ts new file mode 100644 index 000000000000..126460fa44c2 --- /dev/null +++ b/core-web/libs/ui/src/lib/components/dot-asset-search/components/dot-asset-card-skeleton/dot-asset-card-skeleton.component.spec.ts @@ -0,0 +1,42 @@ +import { createComponentFactory, Spectator } from '@ngneat/spectator'; + +import { CardModule } from 'primeng/card'; +import { Skeleton, SkeletonModule } from 'primeng/skeleton'; + +import { DotAssetCardSkeletonComponent } from './dot-asset-card-skeleton.component'; + +describe('DotAssetCardSkeletonComponent', () => { + let spectator: Spectator; + + const createComponent = createComponentFactory({ + component: DotAssetCardSkeletonComponent, + imports: [CardModule, SkeletonModule], + declarations: [Skeleton] + }); + + beforeEach(() => { + spectator = createComponent(); + }); + + it('should have the four skelestons components', () => { + const skeletons = spectator.queryAll(Skeleton); + expect(skeletons).toHaveLength(4); + }); + + it('should have the right inputs for each p-skeleton', () => { + const headerSkeleton = spectator.query('[data-testId="p-skeleton-header"]'); + expect(headerSkeleton.getAttribute('shape')).toEqual('square'); + expect(headerSkeleton.getAttribute('size')).toEqual('94px'); + + const bodySkeleton = spectator.query('[data-testId="p-skeleton-body"]'); + expect(bodySkeleton.getAttribute('height')).toEqual('1rem'); + + const state1Skeleton = spectator.query('[data-testId="p-skeleton-state-1"]'); + expect(state1Skeleton.getAttribute('width')).toEqual('2rem'); + expect(state1Skeleton.getAttribute('height')).toEqual('1rem'); + + const state2Skeleton = spectator.query('[data-testId="p-skeleton-state-2"]'); + expect(state2Skeleton.getAttribute('shape')).toEqual('circle'); + expect(state2Skeleton.getAttribute('size')).toEqual('16px'); + }); +}); diff --git a/core-web/libs/block-editor/src/lib/extensions/asset-form/components/dot-asset-search/components/dot-asset-card-skeleton/dot-asset-card-skeleton.component.ts b/core-web/libs/ui/src/lib/components/dot-asset-search/components/dot-asset-card-skeleton/dot-asset-card-skeleton.component.ts similarity index 68% rename from core-web/libs/block-editor/src/lib/extensions/asset-form/components/dot-asset-search/components/dot-asset-card-skeleton/dot-asset-card-skeleton.component.ts rename to core-web/libs/ui/src/lib/components/dot-asset-search/components/dot-asset-card-skeleton/dot-asset-card-skeleton.component.ts index 051c5047cd6a..3799d0dbdf28 100644 --- a/core-web/libs/block-editor/src/lib/extensions/asset-form/components/dot-asset-search/components/dot-asset-card-skeleton/dot-asset-card-skeleton.component.ts +++ b/core-web/libs/ui/src/lib/components/dot-asset-search/components/dot-asset-card-skeleton/dot-asset-card-skeleton.component.ts @@ -1,9 +1,14 @@ import { ChangeDetectionStrategy, Component } from '@angular/core'; +import { CardModule } from 'primeng/card'; +import { SkeletonModule } from 'primeng/skeleton'; + @Component({ selector: 'dot-asset-card-skeleton', templateUrl: './dot-asset-card-skeleton.component.html', styleUrls: ['./dot-asset-card-skeleton.component.scss'], + standalone: true, + imports: [CardModule, SkeletonModule], changeDetection: ChangeDetectionStrategy.OnPush }) export class DotAssetCardSkeletonComponent {} diff --git a/core-web/libs/block-editor/src/lib/extensions/asset-form/components/dot-asset-search/components/dot-asset-card/dot-asset-card.component.html b/core-web/libs/ui/src/lib/components/dot-asset-search/components/dot-asset-card/dot-asset-card.component.html similarity index 53% rename from core-web/libs/block-editor/src/lib/extensions/asset-form/components/dot-asset-search/components/dot-asset-card/dot-asset-card.component.html rename to core-web/libs/ui/src/lib/components/dot-asset-search/components/dot-asset-card/dot-asset-card.component.html index d95cf2514072..0eb9ccfeb543 100644 --- a/core-web/libs/block-editor/src/lib/extensions/asset-form/components/dot-asset-search/components/dot-asset-card/dot-asset-card.component.html +++ b/core-web/libs/ui/src/lib/components/dot-asset-search/components/dot-asset-card/dot-asset-card.component.html @@ -4,13 +4,16 @@ + [showVideoThumbnail]="true" + data-testId="dot-contentlet-thumbnail"> -

{{ contentlet?.fileName || contentlet?.title }}

+

+ {{ contentlet?.fileName || contentlet?.title }} +

- {{ contentlet.language }} + {{ contentlet?.language }}
diff --git a/core-web/libs/block-editor/src/lib/extensions/asset-form/components/dot-asset-search/components/dot-asset-card/dot-asset-card.component.scss b/core-web/libs/ui/src/lib/components/dot-asset-search/components/dot-asset-card/dot-asset-card.component.scss similarity index 100% rename from core-web/libs/block-editor/src/lib/extensions/asset-form/components/dot-asset-search/components/dot-asset-card/dot-asset-card.component.scss rename to core-web/libs/ui/src/lib/components/dot-asset-search/components/dot-asset-card/dot-asset-card.component.scss diff --git a/core-web/libs/ui/src/lib/components/dot-asset-search/components/dot-asset-card/dot-asset-card.component.spec.ts b/core-web/libs/ui/src/lib/components/dot-asset-search/components/dot-asset-card/dot-asset-card.component.spec.ts new file mode 100644 index 000000000000..55b2cd24ddd9 --- /dev/null +++ b/core-web/libs/ui/src/lib/components/dot-asset-search/components/dot-asset-card/dot-asset-card.component.spec.ts @@ -0,0 +1,59 @@ +import { createComponentFactory, Spectator } from '@ngneat/spectator'; + +import { Card, CardModule } from 'primeng/card'; + +import { DotCMSContentlet } from '@dotcms/dotcms-models'; +import { EMPTY_CONTENTLET } from '@dotcms/utils-testing'; + +import { DotAssetCardComponent } from './dot-asset-card.component'; + +const contentlet: DotCMSContentlet = { + ...EMPTY_CONTENTLET, + title: 'test title', + fileName: 'test fileName', + language: 'en' +}; + +describe('DotAssetCardComponent', () => { + let spectator: Spectator; + + const createComponent = createComponentFactory({ + component: DotAssetCardComponent, + imports: [CardModule] + }); + + beforeEach(() => { + spectator = createComponent({ + props: { + contentlet + } + }); + }); + + it('should use the dot-contentlet-thumbnail', () => { + const card = spectator.query(Card); + const thumbnail = spectator.query('dot-contentlet-thumbnail'); + expect(thumbnail).toBeDefined(); + expect(card).toBeDefined(); + }); + + it('should display the contentlet file name and language', () => { + const title = spectator.query('[data-testId="dot-card-title"]'); + const language = spectator.query('[data-testId="dot-card-language"]'); + + expect(title.innerHTML.trim()).toBe(contentlet.fileName); + expect(language.innerHTML.trim()).toBe(contentlet.language); + }); + + it('should display the contentlet title when the fileName property is empty', () => { + spectator.setInput('contentlet', { + ...contentlet, + fileName: '' + }); + + spectator.detectChanges(); + + const title = spectator.query('[data-testId="dot-card-title"]'); + expect(title.innerHTML.trim()).toBe(contentlet.title); + }); +}); diff --git a/core-web/libs/ui/src/lib/components/dot-asset-search/components/dot-asset-card/dot-asset-card.component.ts b/core-web/libs/ui/src/lib/components/dot-asset-search/components/dot-asset-card/dot-asset-card.component.ts new file mode 100644 index 000000000000..1de5aba3768c --- /dev/null +++ b/core-web/libs/ui/src/lib/components/dot-asset-search/components/dot-asset-card/dot-asset-card.component.ts @@ -0,0 +1,18 @@ +import { CUSTOM_ELEMENTS_SCHEMA, ChangeDetectionStrategy, Component, Input } from '@angular/core'; + +import { CardModule } from 'primeng/card'; + +import { DotCMSContentlet } from '@dotcms/dotcms-models'; + +@Component({ + selector: 'dot-asset-card', + templateUrl: './dot-asset-card.component.html', + styleUrls: ['./dot-asset-card.component.scss'], + standalone: true, + imports: [CardModule], + changeDetection: ChangeDetectionStrategy.OnPush, + schemas: [CUSTOM_ELEMENTS_SCHEMA] +}) +export class DotAssetCardComponent { + @Input() contentlet: DotCMSContentlet; +} diff --git a/core-web/libs/ui/src/lib/components/dot-asset-search/components/dot-asset-search-dialog/dot-asset-search-dialog.component.html b/core-web/libs/ui/src/lib/components/dot-asset-search/components/dot-asset-search-dialog/dot-asset-search-dialog.component.html new file mode 100644 index 000000000000..3bce9c247e7f --- /dev/null +++ b/core-web/libs/ui/src/lib/components/dot-asset-search/components/dot-asset-search-dialog/dot-asset-search-dialog.component.html @@ -0,0 +1 @@ + diff --git a/core-web/libs/ui/src/lib/components/dot-asset-search/components/dot-asset-search-dialog/dot-asset-search-dialog.component.scss b/core-web/libs/ui/src/lib/components/dot-asset-search/components/dot-asset-search-dialog/dot-asset-search-dialog.component.scss new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/core-web/libs/ui/src/lib/components/dot-asset-search/components/dot-asset-search-dialog/dot-asset-search-dialog.component.spec.ts b/core-web/libs/ui/src/lib/components/dot-asset-search/components/dot-asset-search-dialog/dot-asset-search-dialog.component.spec.ts new file mode 100644 index 000000000000..9fef576437e6 --- /dev/null +++ b/core-web/libs/ui/src/lib/components/dot-asset-search/components/dot-asset-search-dialog/dot-asset-search-dialog.component.spec.ts @@ -0,0 +1,53 @@ +import { createComponentFactory, Spectator } from '@ngneat/spectator'; +import { MockComponent } from 'ng-mocks'; + +import { DynamicDialogConfig, DynamicDialogRef } from 'primeng/dynamicdialog'; + +import { EMPTY_CONTENTLET } from '@dotcms/utils-testing'; + +import { DotAssetSearchDialogComponent } from './dot-asset-search-dialog.component'; + +import { DotAssetSearchComponent } from '../../dot-asset-search.component'; + +describe('DotAssetSearchDialogComponent', () => { + let spectator: Spectator; + let dynamicDialogRef: DynamicDialogRef; + const createComponent = createComponentFactory({ + component: DotAssetSearchDialogComponent, + declarations: [MockComponent(DotAssetSearchComponent)], + providers: [ + { + provide: DynamicDialogRef, + useValue: { + close: (_) => { + /* */ + } + } + }, + { + provide: DynamicDialogConfig, + useValue: { + data: { + assetType: 'image' + } + } + } + ] + }); + + beforeEach(() => { + spectator = createComponent(); + dynamicDialogRef = spectator.inject(DynamicDialogRef, true); + }); + + it('should set editorAssetType from config data', () => { + const dotAssetSearchComponent = spectator.query(DotAssetSearchComponent); + expect(dotAssetSearchComponent.type).toBe('image'); + }); + + it('should close dialog with selected asset on addAsset', () => { + const spy = spyOn(dynamicDialogRef, 'close'); + spectator.triggerEventHandler(DotAssetSearchComponent, 'addAsset', EMPTY_CONTENTLET); + expect(spy).toHaveBeenCalledWith(EMPTY_CONTENTLET); + }); +}); diff --git a/core-web/libs/ui/src/lib/components/dot-asset-search/components/dot-asset-search-dialog/dot-asset-search-dialog.component.ts b/core-web/libs/ui/src/lib/components/dot-asset-search/components/dot-asset-search-dialog/dot-asset-search-dialog.component.ts new file mode 100644 index 000000000000..ddd50c066295 --- /dev/null +++ b/core-web/libs/ui/src/lib/components/dot-asset-search/components/dot-asset-search-dialog/dot-asset-search-dialog.component.ts @@ -0,0 +1,29 @@ +import { CommonModule } from '@angular/common'; +import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; + +import { DynamicDialogConfig, DynamicDialogRef } from 'primeng/dynamicdialog'; + +import { DotCMSContentlet, EditorAssetTypes } from '@dotcms/dotcms-models'; +import { DotAssetSearchComponent } from '@dotcms/ui'; + +@Component({ + selector: 'dot-asset-search-dialog', + standalone: true, + imports: [CommonModule, DotAssetSearchComponent], + templateUrl: './dot-asset-search-dialog.component.html', + styleUrl: './dot-asset-search-dialog.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class DotAssetSearchDialogComponent { + private readonly ref = inject(DynamicDialogRef); + + protected editorAssetType: EditorAssetTypes; + + constructor(private readonly config: DynamicDialogConfig) { + this.editorAssetType = this.config.data?.assetType; + } + + onSelectAsset(asset: DotCMSContentlet): void { + this.ref.close(asset); + } +} diff --git a/core-web/libs/block-editor/src/lib/extensions/asset-form/components/dot-asset-search/dot-asset-search.component.html b/core-web/libs/ui/src/lib/components/dot-asset-search/dot-asset-search.component.html similarity index 57% rename from core-web/libs/block-editor/src/lib/extensions/asset-form/components/dot-asset-search/dot-asset-search.component.html rename to core-web/libs/ui/src/lib/components/dot-asset-search/dot-asset-search.component.html index 0d9951168c89..66eb323f0df2 100644 --- a/core-web/libs/block-editor/src/lib/extensions/asset-form/components/dot-asset-search/dot-asset-search.component.html +++ b/core-web/libs/ui/src/lib/components/dot-asset-search/dot-asset-search.component.html @@ -1,7 +1,14 @@ @@ -12,6 +19,5 @@ [done]="vm.preventScroll" [loading]="vm.loading" (selectedItem)="addAsset.emit($event)" - (nextBatch)="offset$.next($event)" - > + (nextBatch)="offset$.next($event)"> diff --git a/core-web/libs/block-editor/src/lib/extensions/asset-form/components/dot-asset-search/dot-asset-search.component.scss b/core-web/libs/ui/src/lib/components/dot-asset-search/dot-asset-search.component.scss similarity index 100% rename from core-web/libs/block-editor/src/lib/extensions/asset-form/components/dot-asset-search/dot-asset-search.component.scss rename to core-web/libs/ui/src/lib/components/dot-asset-search/dot-asset-search.component.scss diff --git a/core-web/libs/ui/src/lib/components/dot-asset-search/dot-asset-search.component.spec.ts b/core-web/libs/ui/src/lib/components/dot-asset-search/dot-asset-search.component.spec.ts new file mode 100644 index 000000000000..6b5a44f011da --- /dev/null +++ b/core-web/libs/ui/src/lib/components/dot-asset-search/dot-asset-search.component.spec.ts @@ -0,0 +1,110 @@ +import { byTestId, createComponentFactory, Spectator } from '@ngneat/spectator'; +import { of } from 'rxjs'; + +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { fakeAsync, tick } from '@angular/core/testing'; + +import { InputTextModule } from 'primeng/inputtext'; + +import { DotContentSearchService, DotLanguagesService } from '@dotcms/data-access'; +import { EMPTY_CONTENTLET } from '@dotcms/utils-testing'; + +import { DotAssetCardComponent } from './components/dot-asset-card/dot-asset-card.component'; +import { DotAssetCardListComponent } from './components/dot-asset-card-list/dot-asset-card-list.component'; +import { DotAssetCardSkeletonComponent } from './components/dot-asset-card-skeleton/dot-asset-card-skeleton.component'; +import { DotAssetSearchComponent } from './dot-asset-search.component'; +import { DotAssetSearchStore } from './store/dot-asset-search.store'; + +describe('DotAssetSearchComponent', () => { + let spectator: Spectator; + + let store: DotAssetSearchStore; + + const createComponent = createComponentFactory({ + component: DotAssetSearchComponent, + providers: [ + { + provide: DotContentSearchService, + useValue: { + get: () => of({ jsonObjectView: { contentlets: [] } }) + } + }, + { + provide: DotLanguagesService, + useValue: { + get: () => + of([ + { + id: '1', + languageCode: 'en', + countryCode: 'us', + language: 'English', + country: 'United States' + } + ]) + } + } + ], + imports: [ + HttpClientTestingModule, + InputTextModule, + DotAssetCardComponent, + DotAssetCardListComponent, + DotAssetCardSkeletonComponent + ], + componentProviders: [DotAssetSearchStore] + }); + + beforeEach(() => { + spectator = createComponent({ + props: { + languageId: '1', + type: 'image' + } + }); + spectator.detectChanges(); + store = spectator.inject(DotAssetSearchStore, true); + spectator.detectChanges(); + }); + + it('should send the correct inputs to DotAssetCardListComponent', () => { + const dotAssetCardListComponent = spectator.query(DotAssetCardListComponent); + + // Default state + expect(dotAssetCardListComponent.contentlets).toEqual([]); + expect(dotAssetCardListComponent.done).toEqual(true); + expect(dotAssetCardListComponent.loading).toEqual(false); + }); + + it('should call store nextBatch', fakeAsync(() => { + const spy = spyOn(store, 'nextBatch'); + spectator.triggerEventHandler(DotAssetCardListComponent, 'nextBatch', 10); + tick(1000); + expect(spy).toHaveBeenCalledWith({ + languageId: '1', + assetType: 'image', + offset: 10, + search: '' + }); + })); + + it('should call addAsset Output', fakeAsync(() => { + const spy = spyOn(spectator.component.addAsset, 'emit'); + spectator.triggerEventHandler(DotAssetCardListComponent, 'selectedItem', EMPTY_CONTENTLET); + tick(1000); + expect(spy).toHaveBeenCalledWith(EMPTY_CONTENTLET); + })); + + it('should call store searchContentlet', fakeAsync(() => { + const spy = spyOn(store, 'searchContentlet'); + const inputElement = spectator.query(byTestId('input-search')) as HTMLInputElement; + spectator.typeInElement('search', inputElement); + tick(1000); + expect(spy).toHaveBeenCalledWith({ + languageId: '1', + assetType: 'image', + offset: 0, + search: 'search' + }); + })); +}); diff --git a/core-web/libs/ui/src/lib/components/dot-asset-search/dot-asset-search.component.ts b/core-web/libs/ui/src/lib/components/dot-asset-search/dot-asset-search.component.ts new file mode 100644 index 000000000000..86c4400c6b3f --- /dev/null +++ b/core-web/libs/ui/src/lib/components/dot-asset-search/dot-asset-search.component.ts @@ -0,0 +1,103 @@ +import { BehaviorSubject, fromEvent } from 'rxjs'; + +import { CommonModule } from '@angular/common'; +import { + AfterViewInit, + ChangeDetectionStrategy, + Component, + DestroyRef, + ElementRef, + EventEmitter, + Input, + OnInit, + Output, + ViewChild, + inject +} from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; + +import { InputTextModule } from 'primeng/inputtext'; + +import { debounceTime, skip, throttleTime } from 'rxjs/operators'; + +import { DotContentSearchService, DotLanguagesService } from '@dotcms/data-access'; +import { DotCMSContentlet, EditorAssetTypes } from '@dotcms/dotcms-models'; + +// services +import { DotAssetCardComponent } from './components/dot-asset-card/dot-asset-card.component'; +import { DotAssetCardListComponent } from './components/dot-asset-card-list/dot-asset-card-list.component'; +import { DotAssetCardSkeletonComponent } from './components/dot-asset-card-skeleton/dot-asset-card-skeleton.component'; +import { DotAssetSearchStore } from './store/dot-asset-search.store'; + +@Component({ + selector: 'dot-asset-search', + templateUrl: './dot-asset-search.component.html', + styleUrls: ['./dot-asset-search.component.scss'], + providers: [DotAssetSearchStore, DotContentSearchService, DotLanguagesService], + standalone: true, + imports: [ + DotAssetCardComponent, + DotAssetCardListComponent, + DotAssetCardSkeletonComponent, + DotAssetCardListComponent, + InputTextModule, + CommonModule + ], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class DotAssetSearchComponent implements OnInit, AfterViewInit { + @ViewChild('input') input!: ElementRef; + @Output() addAsset = new EventEmitter(); + + @Input() languageId = '*'; + @Input() type: EditorAssetTypes; + + private currentSearch = ''; + private readonly store = inject(DotAssetSearchStore); + private readonly destroyRef = inject(DestroyRef); + + offset$ = new BehaviorSubject(0); + vm$ = this.store.vm$; + + ngOnInit(): void { + // Initial load + this.store.searchContentlet({ + ...this.searchParams(), + search: '', + offset: 0 + }); + + this.offset$ + .pipe(takeUntilDestroyed(this.destroyRef), skip(1), throttleTime(450)) + .subscribe((offset) => + this.store.nextBatch({ + ...this.searchParams(), + offset + }) + ); + + requestAnimationFrame(() => this.input.nativeElement.focus()); + } + + ngAfterViewInit() { + fromEvent(this.input.nativeElement, 'input') + .pipe(takeUntilDestroyed(this.destroyRef), debounceTime(450)) + .subscribe(({ target }) => { + const value = (target as HTMLInputElement).value; + this.currentSearch = value; + this.store.searchContentlet({ + ...this.searchParams(), + search: value + }); + }); + } + + private searchParams() { + return { + languageId: this.languageId || '', + search: this.currentSearch, + assetType: this.type, + offset: this.offset$.value || 0 + }; + } +} diff --git a/core-web/libs/ui/src/lib/components/dot-asset-search/store/dot-asset-search.store.spec.ts b/core-web/libs/ui/src/lib/components/dot-asset-search/store/dot-asset-search.store.spec.ts new file mode 100644 index 000000000000..e9a055fc3f82 --- /dev/null +++ b/core-web/libs/ui/src/lib/components/dot-asset-search/store/dot-asset-search.store.spec.ts @@ -0,0 +1,210 @@ +import { SpectatorService, createServiceFactory } from '@ngneat/spectator'; +import { of } from 'rxjs'; + +import { + DotContentSearchService, + DotLanguagesService, + ESOrderDirectionSearch +} from '@dotcms/data-access'; +import { DotCMSContentlet } from '@dotcms/dotcms-models'; +import { EMPTY_IMAGE_CONTENTLET } from '@dotcms/utils-testing'; + +import { DotAssetSearchStore } from './dot-asset-search.store'; + +export const IMAGE_CONTENTLETS_MOCK: DotCMSContentlet[] = [ + { + ...EMPTY_IMAGE_CONTENTLET, + fileName: '1 rain-forest-view.jpg', + name: 'rain-forest-view.jpg', + description: 'rain-forest-view', + title: 'Rain-forest-view.jpg' + }, + { + ...EMPTY_IMAGE_CONTENTLET, + fileAsset: 'test2.jpg', + fileName: '2 Foto8.jpg', + name: 'Foto8.jpg', + description: 'Foto8', + title: 'Foto8.jpg' + }, + { + ...EMPTY_IMAGE_CONTENTLET, + fileAsset: 'test3.jpg', + fileName: '3 first-chair.jpg', + name: 'first-chair.jpg', + description: 'Stay at one of our resorts and get early hours with our first chair program.', + title: 'First to the Top' + }, + { + ...EMPTY_IMAGE_CONTENTLET, + fileAsset: 'test4.jpg', + fileName: '4 adult-antioxidant.jpg', + name: 'adult-antioxidant.jpg', + description: 'adult-antioxidant', + title: 'Adult-antioxidant.jpg' + } +]; + +const LANGUAGE_MOCK = [ + { + country: 'United States', + countryCode: 'US', + defaultLanguage: true, + id: 1, + language: 'English', + languageCode: 'en' + }, + { + country: 'Espana', + countryCode: 'ES', + defaultLanguage: false, + id: 2, + language: 'Espanol', + languageCode: 'es' + } +]; + +const CONTENTLETS_MOCK_WITH_LANG = IMAGE_CONTENTLETS_MOCK.splice(0, 4).map((contentlet) => ({ + ...contentlet, + language: 'en-US' +})); + +const INITIAL_STATE = { contentlets: [], loading: true, preventScroll: false }; + +describe('DotAssetSearchStore', () => { + let spectator: SpectatorService; + let service: DotAssetSearchStore; + let dotContentSearchService: DotContentSearchService; + + const createService = createServiceFactory({ + service: DotAssetSearchStore, + providers: [ + { + provide: DotContentSearchService, + useValue: { + get: () => of() + } + }, + { + provide: DotLanguagesService, + useValue: { + get: () => of(LANGUAGE_MOCK) + } + } + ] + }); + + beforeEach(() => { + spectator = createService(); + service = spectator.service; + dotContentSearchService = spectator.inject(DotContentSearchService); + }); + + it('should have inital state', (done) => { + service.vm$.subscribe((res) => { + expect(res).toEqual(INITIAL_STATE); + done(); + }); + }); + + describe('Updaters', () => { + it('should update contentlets', (done) => { + const contentlet = [IMAGE_CONTENTLETS_MOCK[0], IMAGE_CONTENTLETS_MOCK[1]]; + service.updateContentlets(contentlet); + + service.vm$.subscribe((res) => { + expect(res).toEqual({ + preventScroll: false, + loading: false, + contentlets: contentlet + }); + done(); + }); + }); + + it('should merge contentlets', (done) => { + const contentlet = [IMAGE_CONTENTLETS_MOCK[0], IMAGE_CONTENTLETS_MOCK[1]]; + service.mergeContentlets(contentlet); + + service.vm$.subscribe((res) => { + expect(res).toEqual({ + preventScroll: false, + loading: false, + contentlets: [...INITIAL_STATE.contentlets, ...contentlet] + }); + done(); + }); + }); + }); + + describe('Effects', () => { + it('should search contentlets', (done) => { + const contentlets = CONTENTLETS_MOCK_WITH_LANG.splice(0, 2); + + const spyLoading = spyOn(service, 'updateLoading'); + const spySearch = spyOn(dotContentSearchService, 'get').and.returnValue( + of({ jsonObjectView: { contentlets } }) + ); + + const params = { + search: 'image', + assetType: 'image', + languageId: 1, + offset: 0 + }; + + spectator.service.searchContentlet(params); + + spectator.service.vm$.subscribe((res) => { + expect(res).toEqual({ + preventScroll: false, + loading: false, + contentlets + }); + + done(); + }); + + expect(spyLoading).toHaveBeenCalledWith(true); + expect(spySearch).toHaveBeenCalledWith({ + query: `+catchall:${params.search}* title:'${params.search}'^15 +languageId:1 +baseType:(4 OR 9) +metadata.contenttype:image/* +deleted:false +working:true`, + sortOrder: ESOrderDirectionSearch.ASC, + limit: 20, + offset: 0 + }); + }); + + it('should load next banch', (done) => { + const contentlets = CONTENTLETS_MOCK_WITH_LANG.splice(0, 2); + const spySearch = spyOn(dotContentSearchService, 'get').and.returnValue( + of({ jsonObjectView: { contentlets } }) + ); + + const params = { + search: 'image', + assetType: 'image', + languageId: 1, + offset: 10 + }; + + spectator.service.nextBatch(params); + + spectator.service.vm$.subscribe((res) => { + expect(res).toEqual({ + preventScroll: false, + loading: false, + contentlets + }); + + done(); + }); + + expect(spySearch).toHaveBeenCalledWith({ + query: `+catchall:${params.search}* title:'${params.search}'^15 +languageId:1 +baseType:(4 OR 9) +metadata.contenttype:image/* +deleted:false +working:true`, + sortOrder: ESOrderDirectionSearch.ASC, + limit: 20, + offset: 10 + }); + }); + }); +}); diff --git a/core-web/libs/ui/src/lib/components/dot-asset-search/store/dot-asset-search.store.ts b/core-web/libs/ui/src/lib/components/dot-asset-search/store/dot-asset-search.store.ts new file mode 100644 index 000000000000..4c6cf60c368a --- /dev/null +++ b/core-web/libs/ui/src/lib/components/dot-asset-search/store/dot-asset-search.store.ts @@ -0,0 +1,165 @@ +import { ComponentStore, tapResponse } from '@ngrx/component-store'; + +import { Injectable } from '@angular/core'; + +import { Observable } from 'rxjs/internal/Observable'; +import { map, switchMap, tap } from 'rxjs/operators'; + +import { + DotContentSearchService, + ESOrderDirectionSearch, + EsQueryParamsSearch, + DotLanguagesService +} from '@dotcms/data-access'; +import { DotCMSContentlet, DotLanguage } from '@dotcms/dotcms-models'; + +export interface DotAssetSearch { + loading: boolean; + preventScroll: boolean; + contentlets: DotCMSContentlet[]; +} + +interface DotAssetSeachQuery { + search: string; + assetType: string; + offset?: number; + languageId?: number | string; +} + +const defaultState: DotAssetSearch = { + loading: true, + preventScroll: false, + contentlets: [] +}; + +@Injectable() +export class DotAssetSearchStore extends ComponentStore { + // Selectors + readonly vm$ = this.select((state) => state); + + readonly updateContentlets = this.updater((_state, contentlets) => ({ + contentlets, + preventScroll: !contentlets?.length, + loading: false + })); + + readonly mergeContentlets = this.updater((state, contentlets) => ({ + contentlets: [...state.contentlets, ...contentlets], + preventScroll: !contentlets?.length, + loading: false + })); + + readonly updateLoading = this.updater((state, loading) => { + return { + ...state, + loading + }; + }); + + private languages: { [key: string]: DotLanguage } = {}; + + constructor( + private dotContentSearchService: DotContentSearchService, + private dotLanguagesService: DotLanguagesService + ) { + super(defaultState); + + this.dotLanguagesService.get().subscribe((languages) => { + languages.forEach((lang) => { + this.languages[lang.id] = lang; + }); + }); + } + + /** + * Search for contentlets + * + * @memberof DotAssetSearchStore + */ + readonly searchContentlet = this.effect((params$: Observable) => { + return params$.pipe( + tap(() => this.updateLoading(true)), + switchMap((params) => { + return this.searchContentletsRequest(params).pipe( + tapResponse( + (contentlets) => this.updateContentlets(contentlets), + (_error) => { + /* */ + } + ) + ); + }) + ); + }); + + /** + * Load more contentlets + * + * @memberof DotAssetSearchStore + */ + readonly nextBatch = this.effect((params$: Observable) => { + return params$.pipe( + switchMap((params) => + this.searchContentletsRequest(params).pipe( + tapResponse( + (contentlets) => this.mergeContentlets(contentlets), + (_error) => { + /* */ + } + ) + ) + ) + ); + }); + + private searchContentletsRequest(params): Observable { + const query = this.queryParams(params); + + return this.dotContentSearchService.get(query).pipe( + map(({ jsonObjectView: { contentlets } }) => { + return this.setContentletLanguage(contentlets); + }) + ); + } + + private queryParams(data): EsQueryParamsSearch { + const { search, assetType, offset = 0, languageId = '' } = data; + const filter = search.includes('-') ? search : `${search}*`; + + return { + query: `+catchall:${filter} title:'${search}'^15 +languageId:${languageId} +baseType:(4 OR 9) +metadata.contenttype:${ + assetType || '' + }/* +deleted:false +working:true`, + sortOrder: ESOrderDirectionSearch.ASC, + limit: 20, + offset + }; + } + + /** + * This method add the Language to the contentets based on their languageId + * + * @private + * @param {DotCMSContentlet[]} contentlets + * @return {*} + * @memberof ImageTabviewFormComponent + */ + private setContentletLanguage(contentlets: DotCMSContentlet[]) { + return contentlets.map((contentlet) => { + return { + ...contentlet, + language: this.getLanguageBadge(contentlet.languageId) + }; + }); + } + + private getLanguageBadge(languageId: number): string { + const { languageCode, countryCode } = this.languages[languageId] || {}; + + if (!languageCode || !countryCode) { + return ''; + } + + return `${languageCode}-${countryCode}`; + } +} diff --git a/core-web/libs/utils-testing/src/lib/dot-contentlet.mock.ts b/core-web/libs/utils-testing/src/lib/dot-contentlet.mock.ts index 270fa42ffd29..38d795ce88e4 100644 --- a/core-web/libs/utils-testing/src/lib/dot-contentlet.mock.ts +++ b/core-web/libs/utils-testing/src/lib/dot-contentlet.mock.ts @@ -49,6 +49,40 @@ export const mockDotContentlet: StructureTypeView[] = [ } ]; +export const EMPTY_CONTENTLET: DotCMSContentlet = { + inode: '14dd5ad9-55ae-42a8-a5a7-e259b6d0901a', + variantId: 'DEFAULT', + locked: false, + stInode: 'd5ea385d-32ee-4f35-8172-d37f58d9cd7a', + contentType: 'Image', + height: 4000, + identifier: '93ca45e0-06d2-4eef-be1d-79bd6bf0fc99', + hasTitleImage: true, + sortOrder: 0, + hostName: 'demo.dotcms.com', + extension: 'jpg', + isContent: true, + baseType: 'FILEASSETS', + archived: false, + working: true, + live: true, + isContentlet: true, + languageId: 1, + titleImage: 'fileAsset', + hasLiveVersion: true, + deleted: false, + folder: '', + host: '', + modDate: '', + modUser: '', + modUserName: '', + owner: '', + title: '', + url: '', + contentTypeIcon: 'assessment', + __icon__: 'Icon' +}; + export const URL_MAP_CONTENTLET: DotCMSContentlet = { URL_MAP_FOR_CONTENT: '/blog/post/french-polynesia-everything-you-need-to-know', archived: false, @@ -112,3 +146,11 @@ export const URL_MAP_CONTENTLET: DotCMSContentlet = { urlTitle: 'french-polynesia-everything-you-need-to-know', working: true }; + +export const EMPTY_IMAGE_CONTENTLET: DotCMSContentlet = { + mimeType: 'image/jpeg', + type: 'file_asset', + fileAssetVersion: 'https://cdn.pixabay.com/photo/2015/04/23/22/00/tree-736885__480.jpg', + fileAsset: 'https://cdn.pixabay.com/photo/2015/04/23/22/00/tree-736885__480.jpg', + ...EMPTY_CONTENTLET +};