-
+

- {{(readProject$ | async)?.longname}} + {{(currentProject$ | async)?.longname}}

diff --git a/apps/dsp-app/src/app/project/project.component.ts b/apps/dsp-app/src/app/project/project.component.ts index b14029655a..e2ab9b943a 100644 --- a/apps/dsp-app/src/app/project/project.component.ts +++ b/apps/dsp-app/src/app/project/project.component.ts @@ -59,13 +59,6 @@ export class ProjectComponent extends ProjectBase implements OnInit, OnDestroy { ) ); - readProject$: Observable = ( - this._store.select(ProjectsSelectors.allProjects) as Observable - ).pipe( - take(1), - map(projects => this.getCurrentProject(projects)) - ); - projectOntologies$: Observable = combineLatest([ this.isProjectsLoading$, this._store.select(OntologiesSelectors.isLoading), @@ -82,6 +75,7 @@ export class ProjectComponent extends ProjectBase implements OnInit, OnDestroy { ); @Select(OntologiesSelectors.hasLoadingErrors) hasLoadingErrors$: Observable; + @Select(ProjectsSelectors.currentProject) currentProject$: Observable; constructor( private _componentCommsService: ComponentCommunicationEventService, diff --git a/libs/vre/advanced-search/src/lib/ui/property-form/property-form-link-value/property-form-link-value.component.ts b/libs/vre/advanced-search/src/lib/ui/property-form/property-form-link-value/property-form-link-value.component.ts index a639177e20..ec0af0833d 100644 --- a/libs/vre/advanced-search/src/lib/ui/property-form/property-form-link-value/property-form-link-value.component.ts +++ b/libs/vre/advanced-search/src/lib/ui/property-form/property-form-link-value/property-form-link-value.component.ts @@ -3,11 +3,11 @@ import { AfterViewInit, ChangeDetectionStrategy, Component, EventEmitter, Input, import { FormControl, FormsModule, ReactiveFormsModule } from '@angular/forms'; import { MatAutocompleteModule, MatAutocompleteSelectedEvent } from '@angular/material/autocomplete'; import { MatInputModule } from '@angular/material/input'; +import { MatAutocompleteOptionsScrollDirective } from '@dasch-swiss/vre/shared/app-common'; import { AppProgressIndicatorComponent } from '@dasch-swiss/vre/shared/app-progress-indicator'; import { debounceTime, distinctUntilChanged } from 'rxjs/operators'; import { ApiData } from '../../../data-access/advanced-search-service/advanced-search.service'; import { PropertyFormItem } from '../../../data-access/advanced-search-store/advanced-search-store.service'; -import { MatAutocompleteOptionsScrollDirective } from '../../directives/mat-autocomplete-options-scroll.directive'; @Component({ selector: 'dasch-swiss-property-form-link-value', diff --git a/libs/vre/shared/app-common/src/index.ts b/libs/vre/shared/app-common/src/index.ts index b602926548..df78f11d76 100644 --- a/libs/vre/shared/app-common/src/index.ts +++ b/libs/vre/shared/app-common/src/index.ts @@ -3,3 +3,4 @@ export * from './lib/custom-regex'; export * from './lib/dsp-resource'; export * from './lib/property-info-values.interface'; export * from './lib/common'; +export * from './lib/directives/mat-autocomplete-options-scroll.directive'; diff --git a/libs/vre/advanced-search/src/lib/ui/directives/mat-autocomplete-options-scroll.directive.ts b/libs/vre/shared/app-common/src/lib/directives/mat-autocomplete-options-scroll.directive.ts similarity index 93% rename from libs/vre/advanced-search/src/lib/ui/directives/mat-autocomplete-options-scroll.directive.ts rename to libs/vre/shared/app-common/src/lib/directives/mat-autocomplete-options-scroll.directive.ts index 167cb3b3a3..5b1c82878d 100644 --- a/libs/vre/advanced-search/src/lib/ui/directives/mat-autocomplete-options-scroll.directive.ts +++ b/libs/vre/shared/app-common/src/lib/directives/mat-autocomplete-options-scroll.directive.ts @@ -32,7 +32,9 @@ export class MatAutocompleteOptionsScrollDirective implements OnDestroy { setTimeout(() => { // Note: remove listner just for safety, in case the close event is skipped. this.removeScrollEventListener(); - this.autoComplete.panel.nativeElement.addEventListener('scroll', this.onScroll.bind(this)); + if (this.autoComplete.panel) { + this.autoComplete.panel.nativeElement.addEventListener('scroll', this.onScroll.bind(this)); + } }, 500); }), takeUntil(this._onDestroy) diff --git a/libs/vre/shared/app-resource-properties/src/lib/property-value.component.ts b/libs/vre/shared/app-resource-properties/src/lib/property-value.component.ts index d33c2f6dd6..69ae7defe4 100644 --- a/libs/vre/shared/app-resource-properties/src/lib/property-value.component.ts +++ b/libs/vre/shared/app-resource-properties/src/lib/property-value.component.ts @@ -72,7 +72,7 @@ import { propertiesTypeMapping } from './resource-payloads-mapping'; .item { flex: 1; &.hover:hover { - background: $primary_50; + background: $primary_100; } } `, diff --git a/libs/vre/shared/app-resource-properties/src/lib/property-values.component.ts b/libs/vre/shared/app-resource-properties/src/lib/property-values.component.ts index 223ea3bc63..56c14c93cc 100644 --- a/libs/vre/shared/app-resource-properties/src/lib/property-values.component.ts +++ b/libs/vre/shared/app-resource-properties/src/lib/property-values.component.ts @@ -19,9 +19,10 @@ import { propertiesTypeMapping } from './resource-payloads-mapping'; } div.property-value { display: flex; - padding: 5px; + padding: 0 5px; &:nth-child(even) { + padding: 5px; background-color: $primary_50; } } diff --git a/libs/vre/shared/app-resource-properties/src/lib/switch-components/link-switch.component.ts b/libs/vre/shared/app-resource-properties/src/lib/switch-components/link-switch.component.ts index 2600ba16ef..d260e28376 100644 --- a/libs/vre/shared/app-resource-properties/src/lib/switch-components/link-switch.component.ts +++ b/libs/vre/shared/app-resource-properties/src/lib/switch-components/link-switch.component.ts @@ -36,7 +36,7 @@ export class LinkSwitchComponent implements IsSwitchComponent { } get link() { - return `/resource${this._resourceService.getResourcePath(this.control.value)}`; + return this.control.value ? `/resource${this._resourceService.getResourcePath(this.control.value)}` : '#'; } constructor(private _resourceService: ResourceService) {} diff --git a/libs/vre/shared/app-resource-properties/src/lib/value-components/link-value.component.scss b/libs/vre/shared/app-resource-properties/src/lib/value-components/link-value.component.scss new file mode 100644 index 0000000000..5c7b25031d --- /dev/null +++ b/libs/vre/shared/app-resource-properties/src/lib/value-components/link-value.component.scss @@ -0,0 +1,27 @@ +@use '../../../../../../../apps/dsp-app/src/styles/config' as *; + +::ng-deep { + div.mat-mdc-autocomplete-panel { + max-height: 40vh !important; + + .mat-mdc-option.loader { + flex-direction: column; + + .mdc-list-item__primary-text { + margin-right: initial; + } + } + + .mat-mdc-option { + padding: 10px; + + &:hover:not(.mdc-list-item--disabled) { + background: $primary_100; + } + + &:nth-child(even) { + background-color: $primary_50; + } + } + } +} \ No newline at end of file diff --git a/libs/vre/shared/app-resource-properties/src/lib/value-components/link-value.component.ts b/libs/vre/shared/app-resource-properties/src/lib/value-components/link-value.component.ts index 8c4a345f59..9a64c0efaf 100644 --- a/libs/vre/shared/app-resource-properties/src/lib/value-components/link-value.component.ts +++ b/libs/vre/shared/app-resource-properties/src/lib/value-components/link-value.component.ts @@ -1,24 +1,40 @@ -import { ChangeDetectorRef, Component, ElementRef, Inject, Input, OnDestroy, OnInit, ViewChild } from '@angular/core'; +import { + AfterViewInit, + ChangeDetectorRef, + Component, + ElementRef, + HostBinding, + Inject, + Input, + OnDestroy, + OnInit, + ViewChild, +} from '@angular/core'; import { FormControl } from '@angular/forms'; -import { MatAutocompleteTrigger } from '@angular/material/autocomplete'; +import { MatAutocomplete, MatAutocompleteTrigger } from '@angular/material/autocomplete'; import { MatDialog } from '@angular/material/dialog'; import { + ApiResponseError, + CountQueryResponse, KnoraApiConnection, ReadProject, ReadResource, ReadResourceSequence, ResourceClassAndPropertyDefinitions, + ResourceClassDefinition, } from '@dasch-swiss/dsp-js'; +import { MatAutocompleteOptionsScrollDirective } from '@dasch-swiss/vre/shared/app-common'; import { DspApiConnectionToken } from '@dasch-swiss/vre/shared/app-config'; import { ProjectsSelectors } from '@dasch-swiss/vre/shared/app-state'; import { Store } from '@ngxs/store'; -import { Subscription } from 'rxjs'; -import { filter, finalize, switchMap } from 'rxjs/operators'; +import { Observable, Subject, of } from 'rxjs'; +import { debounceTime, filter, finalize, map, switchMap, take, takeUntil } from 'rxjs/operators'; import { CreateResourceDialogComponent, CreateResourceDialogProps } from '../create-resource-dialog.component'; import { LinkValueDataService } from './link-value-data.service'; @Component({ selector: 'app-link-value', + styleUrls: ['./link-value.component.scss'], template: ` - No results were found. + No results were found. Create New: {{ rc?.label }} - {{ res.label }} + {{ searchResultCount }} results found + + {{ res.label }} + + + + - {{ 'appLabels.form.action.searchHelp' | translate }} {{ errors | humanReadableError }} `, - providers: [LinkValueDataService], + providers: [LinkValueDataService, MatAutocompleteOptionsScrollDirective], }) -export class LinkValueComponent implements OnInit, OnDestroy { +export class LinkValueComponent implements OnInit, AfterViewInit, OnDestroy { + private readonly pageResultsLimit: number = 25; + private cancelPreviousCountRequest$ = new Subject(); + private cancelPreviousSearchRequest$ = new Subject(); + @Input({ required: true }) control!: FormControl; @Input({ required: true }) propIri!: string; @Input({ required: true }) resourceClassIri!: string; @Input({ required: true }) defaultValue!: string; @ViewChild(MatAutocompleteTrigger) autoComplete!: MatAutocompleteTrigger; + @ViewChild(MatAutocomplete) auto!: MatAutocomplete; @ViewChild('input') input!: ElementRef; + @HostBinding('attr.scroll') matAutocompleteOptionsScrollDirective: MatAutocompleteOptionsScrollDirective | undefined; + destroyed$ = new Subject(); loading = false; useDefaultValue = true; resources: ReadResource[] = []; - readResource: ReadResource | undefined; - subscription: Subscription | undefined; + + searchResultCount: number = 0; + nextPageNumber: number = 0; constructor( @Inject(DspApiConnectionToken) @@ -78,6 +107,10 @@ export class LinkValueComponent implements OnInit, OnDestroy { this._getResourceProperties(); } + ngAfterViewInit() { + this._initAutocompleteScroll(); + } + handleNonSelectedValues() { const text = this._getTextInput(); if (text !== this.displayResource(this.control.value)) { @@ -85,32 +118,29 @@ export class LinkValueComponent implements OnInit, OnDestroy { } } - search() { - const readResource = this.readResource as ReadResource; + onInputValueChange() { + this.searchResultCount = 0; + this.nextPageNumber = 0; + this.resources = []; const searchTerm = this._getTextInput(); if (searchTerm?.length < 3) { return; } - this.loading = true; - this.subscription = this._dspApiConnection.v2.search - .doSearchByLabel(searchTerm, 0, { - limitToResourceClass: this._getRestrictToResourceClass(readResource), - }) - .pipe( - finalize(() => { - this.loading = false; - }) - ) - .subscribe(response => { - this.resources = (response as ReadResourceSequence).resources; - this._cd.detectChanges(); + this.loading = true; + const resourceClassIri = this._getRestrictToResourceClass(this.readResource as ReadResource)!; + this._getResourcesListCount(searchTerm, resourceClassIri) + .pipe(take(1)) + .subscribe(count => { + this.searchResultCount = count; + this._cd.markForCheck(); }); + + this._search(searchTerm); } openCreateResourceDialog(event: any, resourceClassIri: string, resourceType: string) { let myResourceId: string; - event.stopPropagation(); const projectIri = (this._store.selectSnapshot(ProjectsSelectors.currentProject) as ReadProject).id; this._dialog @@ -130,6 +160,8 @@ export class LinkValueComponent implements OnInit, OnDestroy { }) ) .subscribe(res => { + this.resources = []; + this.searchResultCount = 1; this.resources.push(res as ReadResource); this.control.setValue(myResourceId); this.autoComplete.closePanel(); @@ -137,10 +169,9 @@ export class LinkValueComponent implements OnInit, OnDestroy { }); } - private _getRestrictToResourceClass(resource: ReadResource) { - const linkType = resource.getLinkPropertyIriFromLinkValuePropertyIri(this.propIri); - return resource.entityInfo.properties[linkType].objectType; - } + trackByResourcesFn = (index: number, item: ReadResource) => `${index}-${item.id}`; + + trackByResourceClassFn = (index: number, item: ResourceClassDefinition) => `${index}-${item.id}`; displayResource(resId: string | null): string { if (this.useDefaultValue) return this.defaultValue; @@ -148,6 +179,71 @@ export class LinkValueComponent implements OnInit, OnDestroy { return this.resources.find(res => res.id === resId)?.label ?? ''; } + ngOnDestroy() { + this.destroyed$.next(); + this.destroyed$.complete(); + } + + private _getResourcesListCount(searchValue: string, resourceClassIri: string): Observable { + this.cancelPreviousCountRequest$.next(); + if (!searchValue || searchValue.length <= 2 || typeof searchValue !== 'string') return of(0); + + return this._dspApiConnection.v2.search + .doSearchByLabelCountQuery(searchValue, { + limitToResourceClass: resourceClassIri, + }) + .pipe( + takeUntil(this.cancelPreviousCountRequest$), + switchMap((response: CountQueryResponse | ApiResponseError) => { + const countQuery = response as CountQueryResponse; + return of(countQuery.numberOfResults); + }) + ); + } + + private _search(searchTerm: string, offset = 0) { + this.cancelPreviousSearchRequest$.next(); + const resourceClassIri = this._getRestrictToResourceClass(this.readResource as ReadResource)!; + this._dspApiConnection.v2.search + .doSearchByLabel(searchTerm, offset, { + limitToResourceClass: resourceClassIri, + }) + .pipe( + takeUntil(this.cancelPreviousSearchRequest$), + take(1), + finalize(() => { + this.loading = false; + }) + ) + .subscribe(response => { + this.resources = this.resources.concat((response as ReadResourceSequence).resources); + this.nextPageNumber += 1; + this._cd.markForCheck(); + }); + } + + private _getRestrictToResourceClass(resource: ReadResource) { + const linkType = resource.getLinkPropertyIriFromLinkValuePropertyIri(this.propIri); + return resource.entityInfo.properties[linkType].objectType; + } + + private _initAutocompleteScroll() { + this.matAutocompleteOptionsScrollDirective = new MatAutocompleteOptionsScrollDirective(this.auto); + this.matAutocompleteOptionsScrollDirective.scrollEvent + .pipe( + filter(() => { + return !this.loading && this.searchResultCount > this.resources.length; + }), + takeUntil(this.destroyed$), + map(() => { + this.loading = true; + this._cd.markForCheck(); + }), + debounceTime(300) + ) + .subscribe(() => this._search(this._getTextInput(), this.nextPageNumber)); + } + private _getResourceProperties() { const ontologyIri = this.resourceClassIri.split('#')[0]; this._dspApiConnection.v2.ontologyCache @@ -166,8 +262,4 @@ export class LinkValueComponent implements OnInit, OnDestroy { private _getTextInput() { return this.input.nativeElement.value; } - - ngOnDestroy() { - this.subscription?.unsubscribe(); - } }