Skip to content

Commit

Permalink
feat: link value autocomplete, property values css adjustments (DEV-3718
Browse files Browse the repository at this point in the history
) (#1647)
  • Loading branch information
irmastnt authored Jun 13, 2024
1 parent 6e463ae commit fcea70c
Show file tree
Hide file tree
Showing 10 changed files with 169 additions and 52 deletions.
6 changes: 3 additions & 3 deletions apps/dsp-app/src/app/project/project.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,14 @@
</div>
<div class="main-content">
<!-- Project label -->
<div *ngIf="(readProject$ | async) !== undefined" class="project-title">
<div *ngIf="(currentProject$ | async) !== undefined" class="project-title">
<p
#projectTitle
matTooltip="{{(readProject$ | async)?.longname}}"
matTooltip="{{(currentProject$ | async)?.longname}}"
matTooltipShowDelay="500"
[matTooltipDisabled]="compareElementHeights(projectTitle)"
matTooltipPosition="right">
{{(readProject$ | async)?.longname}}
{{(currentProject$ | async)?.longname}}
</p>
</div>
<mat-list class="main-list">
Expand Down
8 changes: 1 addition & 7 deletions apps/dsp-app/src/app/project/project.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,13 +59,6 @@ export class ProjectComponent extends ProjectBase implements OnInit, OnDestroy {
)
);

readProject$: Observable<ReadProject> = (
this._store.select(ProjectsSelectors.allProjects) as Observable<ReadProject[]>
).pipe(
take(1),
map(projects => this.getCurrentProject(projects))
);

projectOntologies$: Observable<ReadOntology[]> = combineLatest([
this.isProjectsLoading$,
this._store.select(OntologiesSelectors.isLoading),
Expand All @@ -82,6 +75,7 @@ export class ProjectComponent extends ProjectBase implements OnInit, OnDestroy {
);

@Select(OntologiesSelectors.hasLoadingErrors) hasLoadingErrors$: Observable<boolean>;
@Select(ProjectsSelectors.currentProject) currentProject$: Observable<ReadProject>;

constructor(
private _componentCommsService: ComponentCommunicationEventService,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
1 change: 1 addition & 0 deletions libs/vre/shared/app-common/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ import { propertiesTypeMapping } from './resource-payloads-mapping';
.item {
flex: 1;
&.hover:hover {
background: $primary_50;
background: $primary_100;
}
}
`,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {}
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
}
Original file line number Diff line number Diff line change
@@ -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: `
<mat-form-field style="width: 100%">
<input
Expand All @@ -28,42 +44,55 @@ import { LinkValueDataService } from './link-value-data.service';
aria-label="resource"
placeholder="Name of an existing resource"
data-cy="link-input"
(input)="search()"
(input)="onInputValueChange()"
[matAutocomplete]="auto" />
<mat-autocomplete
#auto="matAutocomplete"
requireSelection
[displayWith]="displayResource.bind(this)"
(closed)="handleNonSelectedValues()">
<mat-option *ngIf="resources.length === 0 && !loading" [disabled]="true"> No results were found. </mat-option>
<mat-option *ngIf="searchResultCount === 0" [disabled]="true"> No results were found. </mat-option>
<mat-option
*ngFor="let rc of _linkValueDataService.resourceClasses"
*ngFor="let rc of _linkValueDataService.resourceClasses; trackBy: trackByResourceClassFn"
(click)="openCreateResourceDialog($event, rc.id, rc.label)">
Create New: {{ rc?.label }}
</mat-option>
<mat-option *ngFor="let res of resources" [value]="res.id"> {{ res.label }}</mat-option>
<mat-option *ngIf="searchResultCount > 0" [disabled]="true"> {{ searchResultCount }} results found </mat-option>
<mat-option *ngFor="let res of resources; trackBy: trackByResourcesFn" [value]="res.id">
{{ res.label }}
</mat-option>
<mat-option *ngIf="loading" [disabled]="true" class="loader">
<dasch-swiss-app-progress-indicator></dasch-swiss-app-progress-indicator>
</mat-option>
</mat-autocomplete>
<mat-spinner [diameter]="24" style="margin-right: 16px" *ngIf="loading" matSuffix></mat-spinner>
<mat-hint>{{ 'appLabels.form.action.searchHelp' | translate }}</mat-hint>
<mat-error *ngIf="control.errors as errors">{{ errors | humanReadableError }}</mat-error>
</mat-form-field>
`,
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<void>();
private cancelPreviousSearchRequest$ = new Subject<void>();

@Input({ required: true }) control!: FormControl<string>;
@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<HTMLInputElement>;
@HostBinding('attr.scroll') matAutocompleteOptionsScrollDirective: MatAutocompleteOptionsScrollDirective | undefined;

destroyed$ = new Subject<void>();
loading = false;
useDefaultValue = true;
resources: ReadResource[] = [];

readResource: ReadResource | undefined;
subscription: Subscription | undefined;

searchResultCount: number = 0;
nextPageNumber: number = 0;

constructor(
@Inject(DspApiConnectionToken)
Expand All @@ -78,39 +107,40 @@ export class LinkValueComponent implements OnInit, OnDestroy {
this._getResourceProperties();
}

ngAfterViewInit() {
this._initAutocompleteScroll();
}

handleNonSelectedValues() {
const text = this._getTextInput();
if (text !== this.displayResource(this.control.value)) {
this.input.nativeElement.value = '';
}
}

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
Expand All @@ -130,24 +160,90 @@ 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();
this._cd.detectChanges();
});
}

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;
if (resId === null) return '';
return this.resources.find(res => res.id === resId)?.label ?? '';
}

ngOnDestroy() {
this.destroyed$.next();
this.destroyed$.complete();
}

private _getResourcesListCount(searchValue: string, resourceClassIri: string): Observable<number> {
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
Expand All @@ -166,8 +262,4 @@ export class LinkValueComponent implements OnInit, OnDestroy {
private _getTextInput() {
return this.input.nativeElement.value;
}

ngOnDestroy() {
this.subscription?.unsubscribe();
}
}

0 comments on commit fcea70c

Please sign in to comment.