diff --git a/backend/i18n/frontend_en.json b/backend/i18n/frontend_en.json index def321ad7..1fef6b24b 100644 --- a/backend/i18n/frontend_en.json +++ b/backend/i18n/frontend_en.json @@ -307,6 +307,7 @@ "common.lastExecuted": "Last Executed", "common.latitudeShort": "Lat", "common.loading": "Loading", + "common.loadMore": "Load More", "common.logout": "Logout", "common.logs": "Logs", "common.longitudeShort": "Lon", @@ -490,6 +491,7 @@ "contents.statusQueries": "Status Queries", "contents.stockPhotoEmpty": "Nothing selected", "contents.stockPhotoSearch": "Search for Photos by Unsplash", + "contents.stockPhotoSearchEmpty": "Use the search bar above to find photos.", "contents.tableHeaders.created": "Created", "contents.tableHeaders.createdBy": "Created By", "contents.tableHeaders.createdByShort": "By", diff --git a/backend/i18n/frontend_it.json b/backend/i18n/frontend_it.json index 9f451f078..ac66d0cff 100644 --- a/backend/i18n/frontend_it.json +++ b/backend/i18n/frontend_it.json @@ -307,6 +307,7 @@ "common.lastExecuted": "Last Executed", "common.latitudeShort": "Lat", "common.loading": "Caricamento", + "common.loadMore": "Load More", "common.logout": "Esci", "common.logs": "Log", "common.longitudeShort": "Lon", @@ -490,6 +491,7 @@ "contents.statusQueries": "Stato Query", "contents.stockPhotoEmpty": "Nessuna selezione", "contents.stockPhotoSearch": "Cerca foto su Unsplash", + "contents.stockPhotoSearchEmpty": "Use the search bar above to find photos.", "contents.tableHeaders.created": "Creato", "contents.tableHeaders.createdBy": "Creato da", "contents.tableHeaders.createdByShort": "Da", diff --git a/backend/i18n/frontend_nl.json b/backend/i18n/frontend_nl.json index 074693f7d..53815c466 100644 --- a/backend/i18n/frontend_nl.json +++ b/backend/i18n/frontend_nl.json @@ -307,6 +307,7 @@ "common.lastExecuted": "Laatst Uitgevoerd", "common.latitudeShort": "Lat", "common.loading": "Laden", + "common.loadMore": "Load More", "common.logout": "Uitloggen", "common.logs": "Logboeken", "common.longitudeShort": "Lon", @@ -490,6 +491,7 @@ "contents.statusQueries": "Statusquery's", "contents.stockPhotoEmpty": "Niets geselecteerd", "contents.stockPhotoSearch": "Zoeken naar foto's op Unsplash", + "contents.stockPhotoSearchEmpty": "Use the search bar above to find photos.", "contents.tableHeaders.created": "Gemaakt", "contents.tableHeaders.createdBy": "Gemaakt door", "contents.tableHeaders.createdByShort": "Door", diff --git a/backend/i18n/frontend_zh.json b/backend/i18n/frontend_zh.json index 8b99a5258..e4bafdc99 100644 --- a/backend/i18n/frontend_zh.json +++ b/backend/i18n/frontend_zh.json @@ -307,6 +307,7 @@ "common.lastExecuted": "Last Executed", "common.latitudeShort": "纬度", "common.loading": "正在加载", + "common.loadMore": "Load More", "common.logout": "注销", "common.logs": "日志", "common.longitudeShort": "Lon", @@ -490,6 +491,7 @@ "contents.statusQueries": "状态查询", "contents.stockPhotoEmpty": "未选择任何内容", "contents.stockPhotoSearch": "通过 Unsplash 搜索照片", + "contents.stockPhotoSearchEmpty": "Use the search bar above to find photos.", "contents.tableHeaders.created": "创建", "contents.tableHeaders.createdBy": "创建者", "contents.tableHeaders.createdByShort": "By", diff --git a/backend/i18n/source/frontend_en.json b/backend/i18n/source/frontend_en.json index def321ad7..1fef6b24b 100644 --- a/backend/i18n/source/frontend_en.json +++ b/backend/i18n/source/frontend_en.json @@ -307,6 +307,7 @@ "common.lastExecuted": "Last Executed", "common.latitudeShort": "Lat", "common.loading": "Loading", + "common.loadMore": "Load More", "common.logout": "Logout", "common.logs": "Logs", "common.longitudeShort": "Lon", @@ -490,6 +491,7 @@ "contents.statusQueries": "Status Queries", "contents.stockPhotoEmpty": "Nothing selected", "contents.stockPhotoSearch": "Search for Photos by Unsplash", + "contents.stockPhotoSearchEmpty": "Use the search bar above to find photos.", "contents.tableHeaders.created": "Created", "contents.tableHeaders.createdBy": "Created By", "contents.tableHeaders.createdByShort": "By", diff --git a/frontend/src/app/features/content/shared/forms/stock-photo-editor.component.html b/frontend/src/app/features/content/shared/forms/stock-photo-editor.component.html index cf23773d7..b341476a2 100644 --- a/frontend/src/app/features/content/shared/forms/stock-photo-editor.component.html +++ b/frontend/src/app/features/content/shared/forms/stock-photo-editor.component.html @@ -1,41 +1,52 @@ -
-
- - +
+ -
- -
+ - -
- {{ 'contents.stockPhotoEmpty' | sqxTranslate }} -
-
-
+ +
+ +
+ -
- + +
- + + + + + + + - +
- - + +
+ {{ 'contents.stockPhotoSearchEmpty' | sqxTranslate }} +
+ +
+
- -
-
\ No newline at end of file + + + diff --git a/frontend/src/app/features/content/shared/forms/stock-photo-editor.component.scss b/frontend/src/app/features/content/shared/forms/stock-photo-editor.component.scss index e3dd07f82..dca77b4c0 100644 --- a/frontend/src/app/features/content/shared/forms/stock-photo-editor.component.scss +++ b/frontend/src/app/features/content/shared/forms/stock-photo-editor.component.scss @@ -3,68 +3,45 @@ $color-user-background: rgba(0, 0, 0, 50%); $color-background: #000; -$height: 300px; - -.col-image { - @include force-width(400px); - - &.expand { - @include force-width(100%); - } - - img { - max-width: 100%; - } -} - -.value { - padding-right: 2.5rem; - - &-clear { - @include absolute(0, 0, auto, auto); - } -} .preview { - @include force-height($height); align-items: center; background: $color-background; border: 0; - border-radius: .25rem; - color: $color-white; - display: flex; - justify-content: center; - margin: 0; - margin-top: .5rem; + border-radius: 0; + color: white; + text-align: center; - &-empty { - padding: 1rem; + i { + margin-top: 1rem; + margin-bottom: 1rem; } img { - max-height: $height; + max-width: 100%; } } -sqx-list-view { - border: 1px solid $color-input; - border-radius: .25rem; - height: $height; - margin-top: .5rem; +.spin2 { + font-size: 14px; +} + +.search { + display: inline-block; + margin-left: 0; + margin-right: 1rem; + width: 300px; } -.icon { - @include absolute($height * .5, auto, auto, 5px); - color: $color-border; - font-size: 30px; - font-weight: lighter; +.empty { + @include absolute(40%, 0, null, 0); } .photos { column-gap: 0; column-width: 200px; - margin-left: -1rem; margin-right: -1rem; + margin-left: 0; } .photo { diff --git a/frontend/src/app/features/content/shared/forms/stock-photo-editor.component.ts b/frontend/src/app/features/content/shared/forms/stock-photo-editor.component.ts index 3c7f8e999..b4dafb10a 100644 --- a/frontend/src/app/features/content/shared/forms/stock-photo-editor.component.ts +++ b/frontend/src/app/features/content/shared/forms/stock-photo-editor.component.ts @@ -7,9 +7,9 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, forwardRef, Input, OnInit } from '@angular/core'; import { FormControl, NG_VALUE_ACCESSOR } from '@angular/forms'; -import { of } from 'rxjs'; -import { debounceTime, switchMap, tap } from 'rxjs/operators'; -import { StatefulControlComponent, StockPhotoDto, StockPhotoService, thumbnail, Types, value$, valueProjection$ } from '@app/shared'; +import { BehaviorSubject, of } from 'rxjs'; +import { debounceTime, map, switchMap, tap } from 'rxjs/operators'; +import { DialogModel, StatefulControlComponent, StockPhotoDto, StockPhotoService, thumbnail, Types, value$, valueProjection$ } from '@app/shared'; export const SQX_STOCK_PHOTO_EDITOR_CONTROL_VALUE_ACCESSOR: any = { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => StockPhotoEditorComponent), multi: true, @@ -19,10 +19,18 @@ interface State { // True when loading assets. isLoading?: boolean; - // True, when width less than 600 pixels. - isCompact?: boolean; + // The photos. + stockPhotos: ReadonlyArray; + + // True if more photos are available. + hasMore?: boolean; + + // The status of the thumbnail. + thumbnailStatus?: 'Loaded' | 'Failed'; } +type Request = { search?: string; page: number }; + @Component({ selector: 'sqx-stock-photo-editor', styleUrls: ['./stock-photo-editor.component.scss'], @@ -40,41 +48,64 @@ export class StockPhotoEditorComponent extends StatefulControlComponent thumbnail(x, 400) || x); + public stockPhotoRequests = new BehaviorSubject({ page: 1 }); + public stockPhotoThumbnail = valueProjection$(this.valueControl, x => thumbnail(x, undefined, 300) || x); public stockPhotoSearch = new FormControl(''); - public stockPhotos = - value$(this.stockPhotoSearch).pipe( - debounceTime(500), - tap(query => { - if (query && query.length > 0) { - this.next({ isLoading: true }); - } - }), - switchMap(query => { - if (query && query.length > 0) { - return this.stockPhotoService.getImages(query); - } else { - return of([]); - } - }), - tap(() => { - this.next({ isLoading: false }); - })); + public searchDialog = new DialogModel(); constructor(changeDetector: ChangeDetectorRef, private readonly stockPhotoService: StockPhotoService, ) { - super(changeDetector, {}); + super(changeDetector, { + stockPhotos: [], + }); } public ngOnInit() { + this.own( + value$(this.valueControl) + .subscribe(() => { + this.next({ thumbnailStatus: undefined }); + })); + + this.own( + value$(this.stockPhotoSearch) + .subscribe(search => { + this.stockPhotoRequests.next({ search, page: 1 }); + })); + this.own( this.valueControl.valueChanges .subscribe(value => { this.callChange(value); this.callTouched(); })); + + this.own( + this.stockPhotoRequests.pipe( + debounceTime(500), + tap(request => { + if (request.search && request.search.length > 0) { + this.next({ isLoading: true }); + } + }), + switchMap(request => { + if (request.search && request.search.length > 0) { + return this.stockPhotoService.getImages(request.search, request.page).pipe(map(result => ({ request, result }))); + } else { + return of(({ request, result: [] })); + } + }), + tap(({ request, result }) => { + this.next(s => ({ + ...s, + isLoading: false, + isDisabled: s.isDisabled, + stockPhotos: request.page > 1 ? [...s.stockPhotos, ...result] : result, + hasMore: result.length === 20, + })); + }))); } public writeValue(obj: string) { @@ -93,13 +124,11 @@ export class StockPhotoEditorComponent extends StatefulControlComponent { + this.own( + this.renderer.listen(this.element.nativeElement, 'load', () => { + this.onLoad(); + })); + + this.own( + this.renderer.listen(this.element.nativeElement, 'error', () => { + this.onError(); + })); + }); + } + + public ngOnChanges() { + this.onError(); + } + + public onLoad() { + this.renderer.setStyle(this.element.nativeElement, 'visibility', 'visible'); + } + + public onError() { + this.renderer.setStyle(this.element.nativeElement, 'visibility', 'hidden'); + } +} diff --git a/frontend/src/app/framework/angular/list-view.component.scss b/frontend/src/app/framework/angular/list-view.component.scss index c6daedc7e..d35a7da5d 100644 --- a/frontend/src/app/framework/angular/list-view.component.scss +++ b/frontend/src/app/framework/angular/list-view.component.scss @@ -1,12 +1,20 @@ @import 'mixins'; @import 'vars'; +%transition-opacity { + transition: opacity .2s ease; +} + +%disabled { + pointer-events: none; +} + :host { display: flex; flex-direction: column; flex-grow: 1; overflow: hidden; - padding: 0; + position: relative; } .inner { @@ -20,10 +28,6 @@ } } -%transition-opacity { - transition: opacity .2s ease; -} - .list { &-content { @extend %transition-opacity; @@ -96,16 +100,13 @@ overflow: visible; } -%disabled { - pointer-events: none; -} - .loading-indicator { @extend %disabled; opacity: .5; } .loader { - @include absolute(20%, 0, auto, 0); + @include absolute(50%, 0, auto, 0); @extend %disabled; + margin-top: -.5rem; } \ No newline at end of file diff --git a/frontend/src/app/framework/declarations.ts b/frontend/src/app/framework/declarations.ts index 626c0439d..3c208da70 100644 --- a/frontend/src/app/framework/declarations.ts +++ b/frontend/src/app/framework/declarations.ts @@ -43,6 +43,7 @@ export * from './angular/http/caching.interceptor'; export * from './angular/http/http-extensions'; export * from './angular/http/loading.interceptor'; export * from './angular/image-source.directive'; +export * from './angular/image-url.directive'; export * from './angular/language-selector.component'; export * from './angular/layout-container.directive'; export * from './angular/layout.component'; diff --git a/frontend/src/app/framework/module.ts b/frontend/src/app/framework/module.ts index 0bc75db2b..73c0570ba 100644 --- a/frontend/src/app/framework/module.ts +++ b/frontend/src/app/framework/module.ts @@ -11,7 +11,7 @@ import { ModuleWithProviders, NgModule } from '@angular/core'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { RouterModule } from '@angular/router'; import { ColorPickerModule } from 'ngx-color-picker'; -import { AnalyticsService, AutocompleteComponent, AvatarComponent, CachingInterceptor, CanDeactivateGuard, CheckboxGroupComponent, ClipboardService, CodeComponent, CodeEditorComponent, ColorPickerComponent, ConfirmClickDirective, ControlErrorsComponent, ControlErrorsMessagesComponent, CopyDirective, DarkenPipe, DatePipe, DateTimeEditorComponent, DayOfWeekPipe, DayPipe, DialogRendererComponent, DialogService, DisplayNamePipe, DropdownComponent, DropdownMenuComponent, DurationPipe, EditableTitleComponent, ExternalLinkDirective, FileDropDirective, FileSizePipe, FocusOnInitDirective, FormAlertComponent, FormErrorComponent, FormHintComponent, FromNowPipe, FullDateTimePipe, HighlightPipe, HoverBackgroundDirective, ImageSourceDirective, IndeterminateValueDirective, ISODatePipe, KeysPipe, KNumberPipe, LanguageSelectorComponent, LayoutComponent, LayoutContainerDirective, LightenPipe, ListViewComponent, LoadingInterceptor, LoadingService, LocalizedInputComponent, LocalStoreService, MarkdownDirective, MarkdownInlinePipe, MarkdownPipe, MessageBus, ModalDialogComponent, ModalDirective, ModalPlacementDirective, MonthPipe, OnboardingService, OnboardingTooltipComponent, PagerComponent, ParentLinkDirective, ProgressBarComponent, ResizedDirective, ResizeService, ResourceLoaderService, RootViewComponent, SafeHtmlPipe, SafeResourceUrlPipe, SafeUrlPipe, ScrollActiveDirective, ShortcutComponent, ShortcutDirective, ShortcutService, ShortDatePipe, ShortTimePipe, StarsComponent, StatusIconComponent, StopClickDirective, StopDragDirective, SyncScollingDirective, SyncWidthDirective, TabRouterlinkDirective, TagEditorComponent, TemplateWrapperDirective, TempService, TitleComponent, TitleService, ToggleComponent, ToolbarComponent, TooltipDirective, TransformInputDirective, TranslatePipe, VideoPlayerComponent } from './declarations'; +import { AnalyticsService, AutocompleteComponent, AvatarComponent, CachingInterceptor, CanDeactivateGuard, CheckboxGroupComponent, ClipboardService, CodeComponent, CodeEditorComponent, ColorPickerComponent, ConfirmClickDirective, ControlErrorsComponent, ControlErrorsMessagesComponent, CopyDirective, DarkenPipe, DatePipe, DateTimeEditorComponent, DayOfWeekPipe, DayPipe, DialogRendererComponent, DialogService, DisplayNamePipe, DropdownComponent, DropdownMenuComponent, DurationPipe, EditableTitleComponent, ExternalLinkDirective, FileDropDirective, FileSizePipe, FocusOnInitDirective, FormAlertComponent, FormErrorComponent, FormHintComponent, FromNowPipe, FullDateTimePipe, HighlightPipe, HoverBackgroundDirective, ImageSourceDirective, ImageUrlDirective, IndeterminateValueDirective, ISODatePipe, KeysPipe, KNumberPipe, LanguageSelectorComponent, LayoutComponent, LayoutContainerDirective, LightenPipe, ListViewComponent, LoadingInterceptor, LoadingService, LocalizedInputComponent, LocalStoreService, MarkdownDirective, MarkdownInlinePipe, MarkdownPipe, MessageBus, ModalDialogComponent, ModalDirective, ModalPlacementDirective, MonthPipe, OnboardingService, OnboardingTooltipComponent, PagerComponent, ParentLinkDirective, ProgressBarComponent, ResizedDirective, ResizeService, ResourceLoaderService, RootViewComponent, SafeHtmlPipe, SafeResourceUrlPipe, SafeUrlPipe, ScrollActiveDirective, ShortcutComponent, ShortcutDirective, ShortcutService, ShortDatePipe, ShortTimePipe, StarsComponent, StatusIconComponent, StopClickDirective, StopDragDirective, SyncScollingDirective, SyncWidthDirective, TabRouterlinkDirective, TagEditorComponent, TemplateWrapperDirective, TempService, TitleComponent, TitleService, ToggleComponent, ToolbarComponent, TooltipDirective, TransformInputDirective, TranslatePipe, VideoPlayerComponent } from './declarations'; @NgModule({ imports: [ @@ -55,6 +55,7 @@ import { AnalyticsService, AutocompleteComponent, AvatarComponent, CachingInterc HighlightPipe, HoverBackgroundDirective, ImageSourceDirective, + ImageUrlDirective, IndeterminateValueDirective, ISODatePipe, KeysPipe, @@ -138,6 +139,7 @@ import { AnalyticsService, AutocompleteComponent, AvatarComponent, CachingInterc HighlightPipe, HoverBackgroundDirective, ImageSourceDirective, + ImageUrlDirective, IndeterminateValueDirective, ISODatePipe, KeysPipe, diff --git a/frontend/src/app/shared/services/stock-photo.service.spec.ts b/frontend/src/app/shared/services/stock-photo.service.spec.ts index e49a89696..889220cb7 100644 --- a/frontend/src/app/shared/services/stock-photo.service.spec.ts +++ b/frontend/src/app/shared/services/stock-photo.service.spec.ts @@ -29,11 +29,11 @@ describe('StockPhotoService', () => { inject([StockPhotoService, HttpTestingController], (stockPhotoService: StockPhotoService, httpMock: HttpTestingController) => { let images: ReadonlyArray; - stockPhotoService.getImages('my-query').subscribe(result => { + stockPhotoService.getImages('my-query', 4).subscribe(result => { images = result; }); - const req = httpMock.expectOne('https://stockphoto.squidex.io/?query=my-query&pageSize=100'); + const req = httpMock.expectOne('https://stockphoto.squidex.io/?query=my-query&page=4'); expect(req.request.method).toEqual('GET'); expect(req.request.headers.get('If-Match')).toBeNull(); @@ -64,7 +64,7 @@ describe('StockPhotoService', () => { images = result; }); - const req = httpMock.expectOne('https://stockphoto.squidex.io/?query=my-query&pageSize=100'); + const req = httpMock.expectOne('https://stockphoto.squidex.io/?query=my-query&page=1'); expect(req.request.method).toEqual('GET'); expect(req.request.headers.get('If-Match')).toBeNull(); diff --git a/frontend/src/app/shared/services/stock-photo.service.ts b/frontend/src/app/shared/services/stock-photo.service.ts index e9cec8b83..02ebcc109 100644 --- a/frontend/src/app/shared/services/stock-photo.service.ts +++ b/frontend/src/app/shared/services/stock-photo.service.ts @@ -27,8 +27,8 @@ export class StockPhotoService { ) { } - public getImages(query: string): Observable> { - const url = `https://stockphoto.squidex.io/?query=${query}&pageSize=100`; + public getImages(query: string, page = 1): Observable> { + const url = `https://stockphoto.squidex.io/?query=${query}&page=${page}`; return this.http.get(url).pipe( map(body => { diff --git a/frontend/src/app/theme/_common.scss b/frontend/src/app/theme/_common.scss index 1d691f0ae..a51c266d7 100644 --- a/frontend/src/app/theme/_common.scss +++ b/frontend/src/app/theme/_common.scss @@ -175,6 +175,10 @@ hr { display: none; } +.hidden-important { + display: none !important; +} + // Hidden helper (fast *ngIf replacement) .invisible { visibility: hidden; diff --git a/frontend/src/app/theme/_forms.scss b/frontend/src/app/theme/_forms.scss index f6b88d183..9e1df422d 100644 --- a/frontend/src/app/theme/_forms.scss +++ b/frontend/src/app/theme/_forms.scss @@ -21,6 +21,14 @@ } } } + + &.preview { + background-color: $color-input; + border: 0; + border-radius: $border-radius; + opacity: .4; + pointer-events: none; + } } // @@ -134,14 +142,6 @@ } -.preview { - background-color: $color-input; - border: 0; - border-radius: $border-radius; - opacity: .4; - pointer-events: none; -} - // // Control Dropdown item //