From 44cac355cad12f6a209977f08e1bf3a27914292d Mon Sep 17 00:00:00 2001 From: Xiao Gui <xgui3783@gmail.com> Date: Fri, 17 Mar 2023 14:59:29 +0100 Subject: [PATCH] fix: revert atlas/parc/tmpl chip fix: only show connectivity in human atlas fix: flatten list of list --- src/atlasComponents/sapi/sxplrTypes.ts | 3 + src/atlasComponents/sapi/translateV3.ts | 5 +- .../pureDumb/pureATPSelector.template.html | 8 +- src/features/category-acc.directive.ts | 6 +- .../connectivity/excludeConnectivity.pipe.ts | 16 --- src/features/connectivity/module.ts | 3 - src/features/entry/entry.component.ts | 10 +- .../entry/entry.flattened.component.html | 97 +++++++++++++++++++ .../entry/entry.flattened.component.scss | 10 ++ .../entry/entry.nestedExpPanel.component.html | 94 ++++++++++++++---- src/features/filterCategories.pipe.ts | 15 +++ src/features/list/list.component.ts | 56 +---------- src/features/list/list.directive.ts | 61 ++++++++++++ src/features/module.ts | 5 + 14 files changed, 290 insertions(+), 99 deletions(-) delete mode 100644 src/features/connectivity/excludeConnectivity.pipe.ts create mode 100644 src/features/entry/entry.flattened.component.html create mode 100644 src/features/entry/entry.flattened.component.scss create mode 100644 src/features/filterCategories.pipe.ts create mode 100644 src/features/list/list.directive.ts diff --git a/src/atlasComponents/sapi/sxplrTypes.ts b/src/atlasComponents/sapi/sxplrTypes.ts index 175f27ed0..0fcf7abfe 100644 --- a/src/atlasComponents/sapi/sxplrTypes.ts +++ b/src/atlasComponents/sapi/sxplrTypes.ts @@ -23,6 +23,7 @@ export type SxplrParcellation = { type: 'SxplrParcellation' id: string name: string + shortName: string modality?: string prevId?: string } & Partial<AdditionalInfo> @@ -31,12 +32,14 @@ export type SxplrTemplate = { type: 'SxplrTemplate' id: string name: string + shortName: string } & Partial<AdditionalInfo> export type SxplrAtlas = { type: 'SxplrAtlas' id: string name: string + species: string } & Partial<AdditionalInfo> export type AdditionalInfo = { diff --git a/src/atlasComponents/sapi/translateV3.ts b/src/atlasComponents/sapi/translateV3.ts index 5ee1ef8af..e083557a1 100644 --- a/src/atlasComponents/sapi/translateV3.ts +++ b/src/atlasComponents/sapi/translateV3.ts @@ -18,7 +18,8 @@ class TranslateV3 { return { id: atlas["@id"], type: "SxplrAtlas", - name: atlas.name + name: atlas.name, + species: atlas.species } } @@ -47,6 +48,7 @@ class TranslateV3 { modality: parcellation.modality, type: "SxplrParcellation", prevId, + shortName: parcellation.name, ...rest } } @@ -62,6 +64,7 @@ class TranslateV3 { const tmpl = { id: template["@id"], name: template.fullName, + shortName: template.shortName, type: "SxplrTemplate" as const } this.#sxplrTmplMap.set(tmpl.id, tmpl) diff --git a/src/atlasComponents/sapiViews/core/rich/ATPSelector/pureDumb/pureATPSelector.template.html b/src/atlasComponents/sapiViews/core/rich/ATPSelector/pureDumb/pureATPSelector.template.html index 3928304fa..dc0aca6a9 100644 --- a/src/atlasComponents/sapiViews/core/rich/ATPSelector/pureDumb/pureATPSelector.template.html +++ b/src/atlasComponents/sapiViews/core/rich/ATPSelector/pureDumb/pureATPSelector.template.html @@ -11,7 +11,7 @@ <ng-template sxplrSmartChipContent> <span class="chip-text"> - Parcellation + {{ ATP.parcellation.shortName }} </span> <span class="sxplr-ml-1 text-muted"> ({{ parcellations.length }}) @@ -56,7 +56,7 @@ <ng-template sxplrSmartChipContent> <span class="chip-text"> - Template + {{ ATP.template.shortName }} </span> <span class="sxplr-ml-1 text-muted"> ({{ availableTemplates.length }}) @@ -77,7 +77,7 @@ <ng-template sxplrSmartChipContent> <span class="chip-text"> - Atlas + {{ ATP.atlas.name }} </span> <span class="sxplr-ml-1 text-muted"> ({{ allAtlases.length}}) @@ -121,7 +121,7 @@ <span *ngIf="item" class="full-sized-button" [matTooltip]="item.version?.name || item.name || item.fullName" [matTooltipPosition]="'above'"> - {{ item.version?.name || item.name || item.fullName }} + {{ item.version?.name || item.shortName || item.name || item.fullName }} <ng-template [ngIf]="suffixText"> <span class="text-muted"> {{ suffixText }} diff --git a/src/features/category-acc.directive.ts b/src/features/category-acc.directive.ts index 661896191..353b405dc 100644 --- a/src/features/category-acc.directive.ts +++ b/src/features/category-acc.directive.ts @@ -1,8 +1,8 @@ import { AfterContentInit, ContentChildren, Directive, OnDestroy, QueryList } from '@angular/core'; import { BehaviorSubject, combineLatest, Subscription } from 'rxjs'; import { map } from 'rxjs/operators'; -import { ListComponent } from './list/list.component'; import { Feature } from "src/atlasComponents/sapi/sxplrTypes" +import { ListDirective } from './list/list.directive'; @Directive({ selector: '[sxplrCategoryAcc]', @@ -14,8 +14,8 @@ export class CategoryAccDirective implements AfterContentInit, OnDestroy { public total$ = new BehaviorSubject<number>(0) public features$ = new BehaviorSubject<Feature[]>([]) - @ContentChildren(ListComponent, { read: ListComponent, descendants: true }) - listCmps: QueryList<ListComponent> + @ContentChildren(ListDirective, { read: ListDirective, descendants: true }) + listCmps: QueryList<ListDirective> #changeSub: Subscription ngAfterContentInit(): void { diff --git a/src/features/connectivity/excludeConnectivity.pipe.ts b/src/features/connectivity/excludeConnectivity.pipe.ts deleted file mode 100644 index 736e5d56a..000000000 --- a/src/features/connectivity/excludeConnectivity.pipe.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { Pipe, PipeTransform } from "@angular/core" -import { PathReturn } from "src/atlasComponents/sapi/typeV3" -import { KeyValue } from "@angular/common" - -type DS = KeyValue<string, PathReturn<"/feature/_types">["items"]> - -@Pipe({ - name: 'isConnectivity', - pure: true -}) -export class ExcludeConnectivityPipe implements PipeTransform { - - public transform(datasets: DS[], filterForConnectivityFlag: boolean): DS[] { - return (datasets || []).filter(d => (d.key === 'connectivity') === filterForConnectivityFlag) - } -} diff --git a/src/features/connectivity/module.ts b/src/features/connectivity/module.ts index 96f346ac8..97a7793ff 100644 --- a/src/features/connectivity/module.ts +++ b/src/features/connectivity/module.ts @@ -7,7 +7,6 @@ import {FormsModule} from "@angular/forms"; import { DialogModule } from "src/ui/dialogInfo"; import { ConnectivityBrowserComponent } from "./connectivityBrowser/connectivityBrowser.component"; -import { ExcludeConnectivityPipe } from "./excludeConnectivity.pipe"; @NgModule({ imports: [ @@ -18,11 +17,9 @@ import { ExcludeConnectivityPipe } from "./excludeConnectivity.pipe"; ], declarations: [ ConnectivityBrowserComponent, - ExcludeConnectivityPipe ], exports: [ ConnectivityBrowserComponent, - ExcludeConnectivityPipe ], providers: [ SAPI, diff --git a/src/features/entry/entry.component.ts b/src/features/entry/entry.component.ts index c288748da..8d3160291 100644 --- a/src/features/entry/entry.component.ts +++ b/src/features/entry/entry.component.ts @@ -1,5 +1,5 @@ import { AfterViewInit, Component, OnDestroy, QueryList, ViewChildren } from '@angular/core'; -import { Store } from '@ngrx/store'; +import { select, Store } from '@ngrx/store'; import { map, scan, switchMap, tap } from 'rxjs/operators'; import { SAPI } from 'src/atlasComponents/sapi'; import { Feature } from 'src/atlasComponents/sapi/sxplrTypes'; @@ -25,8 +25,8 @@ const categoryAcc = <T extends Record<string, unknown>>(categories: T[]) => { @Component({ selector: 'sxplr-feature-entry', - templateUrl: './entry.nestedExpPanel.component.html', - styleUrls: ['./entry.nestedExpPanel.component.scss'], + templateUrl: './entry.flattened.component.html', + styleUrls: ['./entry.flattened.component.scss'], exportAs: 'featureEntryCmp' }) export class EntryComponent extends FeatureBase implements AfterViewInit, OnDestroy { @@ -87,7 +87,9 @@ export class EntryComponent extends FeatureBase implements AfterViewInit, OnDest ) } - public atlas = this.store.select(atlasSelection.selectors.selectedAtlas) + public selectedAtlas$ = this.store.pipe( + select(atlasSelection.selectors.selectedAtlas) + ) private featureTypes$ = this.sapi.v3Get("/feature/_types", {}).pipe( switchMap(resp => diff --git a/src/features/entry/entry.flattened.component.html b/src/features/entry/entry.flattened.component.html new file mode 100644 index 000000000..03bf62d88 --- /dev/null +++ b/src/features/entry/entry.flattened.component.html @@ -0,0 +1,97 @@ +<mat-accordion> + + <!-- show everything except for connectivity and dataset --> + <ng-template ngFor [ngForOf]="cateogryCollections$ | async | keyvalue | filterCategory : ['connectivity', 'dataset', 'other'] : false" let-keyvalue> + <ng-template [ngTemplateOutlet]="featureCategoryFeatureTmpl" + [ngTemplateOutletContext]="{ + $implicit: keyvalue + }"> + </ng-template> + </ng-template> + + + <!-- only show connectivity in human atlas for now --> + <ng-template [ngIf]="(selectedAtlas$ | async)?.species === 'Homo sapiens'"> + <ng-template [ngIf]="cateogryCollections$ | async | keyvalue | filterCategory : ['connectivity']" let-connectivity> + <ng-template ngFor [ngForOf]="connectivity" let-conn> + <mat-expansion-panel sxplr-sapiviews-features-connectivity-check + #connectivityAccordion + *ngIf="conn"> + <mat-expansion-panel-header> + <mat-panel-title> + {{ conn.key }} + </mat-panel-title> + </mat-expansion-panel-header> + + <sxplr-features-connectivity-browser class="pe-all flex-shrink-1" + [region]="region" + [sxplr-features-connectivity-browser-atlas]="selectedAtlas$ | async" + [sxplr-features-connectivity-browser-template]="template" + [sxplr-features-connectivity-browser-parcellation]="parcellation" + [accordionExpanded]="connectivityAccordion.expanded" + [types]="conn.value"> + </sxplr-features-connectivity-browser> + + </mat-expansion-panel> + </ng-template> + </ng-template> + </ng-template> + + <!-- show dataset/other at the very bottom --> + <ng-template ngFor [ngForOf]="cateogryCollections$ | async | keyvalue | filterCategory : ['dataset', 'other']" let-keyvalue> + <ng-template [ngTemplateOutlet]="featureCategoryFeatureTmpl" + [ngTemplateOutletContext]="{ + $implicit: keyvalue + }"> + </ng-template> + </ng-template> +</mat-accordion> + +<!-- template for collected category --> +<ng-template #featureCategoryFeatureTmpl let-keyvalue> + <mat-expansion-panel + sxplrCategoryAcc + #categoryAcc="categoryAcc" + [ngClass]="{ + 'sxplr-d-none': !(categoryAcc.isBusy$ | async) && (categoryAcc.total$ | async) === 0 + }"> + <mat-expansion-panel-header> + <mat-panel-title> + {{ keyvalue.key }} + </mat-panel-title> + <mat-panel-description> + <spinner-cmp *ngIf="categoryAcc.isBusy$ | async"></spinner-cmp> + <ng-template [ngIf]="categoryAcc.total$ | async" let-total> + <span> + {{ total }} + </span> + </ng-template> + </mat-panel-description> + </mat-expansion-panel-header> + + <ng-template ngFor [ngForOf]="keyvalue.value" let-feature> + + <div sxplr-feature-list-directive + [template]="template" + [parcellation]="parcellation" + [region]="region" + [bbox]="bbox" + [queryParams]="queryParams | mergeObj : { type: (feature.name | featureNamePipe) }" + [featureRoute]="feature.path" + #featureListDirective="featureListDirective"> + </div> + </ng-template> + + <cdk-virtual-scroll-viewport itemSize="36" + class="virtual-scroll-viewport"> + <button *cdkVirtualFor="let feature of categoryAcc.features$ | async" + mat-button + class="virtual-scroll-item sxplr-w-100" + [matTooltip]="feature.name" + matTooltipPosition="right" + (click)="onClickFeature(feature)"> + {{ feature.name }} + </button> + </cdk-virtual-scroll-viewport> + </mat-expansion-panel> +</ng-template> \ No newline at end of file diff --git a/src/features/entry/entry.flattened.component.scss b/src/features/entry/entry.flattened.component.scss new file mode 100644 index 000000000..24438a43b --- /dev/null +++ b/src/features/entry/entry.flattened.component.scss @@ -0,0 +1,10 @@ +cdk-virtual-scroll-viewport +{ + height: 10rem; +} + +cdk-virtual-scroll-viewport button +{ + text-align: left; + height: 36px; +} \ No newline at end of file diff --git a/src/features/entry/entry.nestedExpPanel.component.html b/src/features/entry/entry.nestedExpPanel.component.html index 3522c1f48..9c82b50fe 100644 --- a/src/features/entry/entry.nestedExpPanel.component.html +++ b/src/features/entry/entry.nestedExpPanel.component.html @@ -1,5 +1,5 @@ <mat-accordion> - <mat-expansion-panel *ngFor="let keyvalue of (cateogryCollections$ | async | keyvalue | isConnectivity : false)" + <mat-expansion-panel *ngFor="let keyvalue of (cateogryCollections$ | async | keyvalue | filterCategory : ['connectivity', 'dataset', 'other'] : false)" sxplrCategoryAcc #categoryAcc="categoryAcc" [ngClass]="{ @@ -56,29 +56,89 @@ </mat-expansion-panel> - <ng-template [ngIf]="cateogryCollections$ | async | keyvalue | isConnectivity : true" let-connectivity> - <ng-template ngFor [ngForOf]="connectivity" let-conn> - <mat-expansion-panel sxplr-sapiviews-features-connectivity-check - #connectivityAccordion - *ngIf="conn"> + <!-- only show connectivity in human atlas for now --> + <ng-template [ngIf]="(selectedAtlas$ | async)?.species === 'Homo sapiens'"> + <ng-template [ngIf]="cateogryCollections$ | async | keyvalue | filterCategory : ['connectivity']" let-connectivity> + <ng-template ngFor [ngForOf]="connectivity" let-conn> + <mat-expansion-panel sxplr-sapiviews-features-connectivity-check + #connectivityAccordion + *ngIf="conn"> + <mat-expansion-panel-header> + <mat-panel-title> + {{ conn.key }} + </mat-panel-title> + </mat-expansion-panel-header> + + <sxplr-features-connectivity-browser class="pe-all flex-shrink-1" + [region]="region" + [sxplr-features-connectivity-browser-atlas]="selectedAtlas$ | async" + [sxplr-features-connectivity-browser-template]="template" + [sxplr-features-connectivity-browser-parcellation]="parcellation" + [accordionExpanded]="connectivityAccordion.expanded" + [types]="conn.value"> + </sxplr-features-connectivity-browser> + + </mat-expansion-panel> + </ng-template> + </ng-template> + </ng-template> + + + <mat-expansion-panel *ngFor="let keyvalue of (cateogryCollections$ | async | keyvalue | filterCategory : ['dataset', 'other'])" + sxplrCategoryAcc + #categoryAcc="categoryAcc" + [ngClass]="{ + 'sxplr-d-none': !(categoryAcc.isBusy$ | async) && (categoryAcc.total$ | async) === 0 + }"> + + <mat-expansion-panel-header> + + <mat-panel-title> + {{ keyvalue.key }} + </mat-panel-title> + + <mat-panel-description> + <spinner-cmp *ngIf="categoryAcc.isBusy$ | async"></spinner-cmp> + <ng-template [ngIf]="categoryAcc.total$ | async" let-total> + <span> + {{ total }} + </span> + </ng-template> + </mat-panel-description> + </mat-expansion-panel-header> + + <mat-accordion> + <mat-expansion-panel class="mat-elevation-z4" + *ngFor="let feature of keyvalue.value" + [ngClass]="{ + 'sxplr-d-none': (list.state$ | async) === 'noresult' + }"> + <mat-expansion-panel-header> <mat-panel-title> - {{ conn.key }} + <span class="sxplr-white-space-nowrap"> + {{ feature.name | featureNamePipe }} + </span> </mat-panel-title> </mat-expansion-panel-header> - - <sxplr-features-connectivity-browser class="pe-all flex-shrink-1" + + <spinner-cmp *ngIf="(list.state$ | async) === 'busy'"></spinner-cmp> + <sxplr-feature-list + [template]="template" + [parcellation]="parcellation" [region]="region" - [sxplr-features-connectivity-browser-atlas]="atlas | async" - [sxplr-features-connectivity-browser-template]="template" - [sxplr-features-connectivity-browser-parcellation]="parcellation" - [accordionExpanded]="connectivityAccordion.expanded" - [types]="conn.value"> - </sxplr-features-connectivity-browser> + [bbox]="bbox" + [queryParams]="queryParams | mergeObj : { type: (feature.name | featureNamePipe) }" + [featureRoute]="feature.path" + (onClickFeature)="onClickFeature($event)" + #list="featureList" + > + </sxplr-feature-list> </mat-expansion-panel> - </ng-template> - </ng-template> + </mat-accordion> + </mat-expansion-panel> + </mat-accordion> diff --git a/src/features/filterCategories.pipe.ts b/src/features/filterCategories.pipe.ts new file mode 100644 index 000000000..8f1033287 --- /dev/null +++ b/src/features/filterCategories.pipe.ts @@ -0,0 +1,15 @@ +import { KeyValue } from "@angular/common" +import { Pipe, PipeTransform } from "@angular/core" +import { PathReturn } from "src/atlasComponents/sapi/typeV3" + +type DS = KeyValue<string, PathReturn<"/feature/_types">["items"]> + +@Pipe({ + name: 'filterCategory', + pure: true +}) +export class FilterCategoriesPipe implements PipeTransform{ + public transform(datasets: DS[], keys: string[], inclFlag: boolean=true) { + return (datasets || []).filter(d => inclFlag === keys.includes(d.key) ) + } +} diff --git a/src/features/list/list.component.ts b/src/features/list/list.component.ts index 52dbcc1ff..29500aca7 100644 --- a/src/features/list/list.component.ts +++ b/src/features/list/list.component.ts @@ -1,10 +1,7 @@ -import { Component, EventEmitter, Input, Output, SimpleChanges } from '@angular/core'; -import { BehaviorSubject, combineLatest, Observable, of, throwError } from 'rxjs'; -import { catchError, switchMap, tap } from 'rxjs/operators'; +import { Component, EventEmitter, Output } from '@angular/core'; import { SAPI } from 'src/atlasComponents/sapi'; import { Feature } from 'src/atlasComponents/sapi/sxplrTypes'; -import { FeatureType } from 'src/atlasComponents/sapi/typeV3'; -import { AllFeatures, FeatureBase } from '../base'; +import { ListDirective } from './list.directive'; @Component({ selector: 'sxplr-feature-list', @@ -12,58 +9,15 @@ import { AllFeatures, FeatureBase } from '../base'; styleUrls: ['./list.component.scss'], exportAs: "featureList" }) -export class ListComponent extends FeatureBase { +export class ListComponent extends ListDirective { @Output() onClickFeature = new EventEmitter<Feature>() - @Input() - featureRoute: string - private guardedRoute$ = new BehaviorSubject<FeatureType>(null) - - public state$ = new BehaviorSubject<'busy'|'noresult'|'result'>('noresult') - - constructor(private sapi: SAPI) { - super() + constructor(sapi: SAPI) { + super(sapi) } - ngOnChanges(sc: SimpleChanges): void { - super.ngOnChanges(sc) - const { featureRoute } = sc - if (featureRoute) { - const featureType = (featureRoute.currentValue || '').split("/").slice(-1)[0] - this.guardedRoute$.next(AllFeatures[featureType]) - } - } - - public features$: Observable<Feature[]> = combineLatest([ - this.guardedRoute$, - this.TPRBbox$, - ]).pipe( - tap(() => this.state$.next('busy')), - switchMap(([route, { template, parcellation, region, bbox }]) => { - if (!route) { - return throwError("noresult") - } - const query = {} - if (template) query['space_id'] = template.id - if (parcellation) query['parcellation_id'] = parcellation.id - if (region) query['region_id'] = region.name - if (bbox) query['bbox'] = JSON.stringify(bbox) - return this.sapi.getV3Features(route, { - query: { - ...this.queryParams, - ...query, - } as any - }) - }), - catchError(() => { - this.state$.next("noresult") - return of([] as Feature[]) - }), - tap(result => this.state$.next(result.length > 0 ? 'result' : 'noresult')), - ) - onClickItem(feature: Feature){ this.onClickFeature.emit(feature) } diff --git a/src/features/list/list.directive.ts b/src/features/list/list.directive.ts new file mode 100644 index 000000000..c5c4734d9 --- /dev/null +++ b/src/features/list/list.directive.ts @@ -0,0 +1,61 @@ +import { Input, Directive, SimpleChanges } from "@angular/core"; +import { BehaviorSubject, combineLatest, Observable, of, throwError } from "rxjs"; +import { catchError, switchMap, tap } from "rxjs/operators"; +import { SAPI } from "src/atlasComponents/sapi"; +import { Feature } from "src/atlasComponents/sapi/sxplrTypes"; +import { FeatureType } from "src/atlasComponents/sapi/typeV3"; +import { AllFeatures, FeatureBase } from "../base"; + +@Directive({ + selector: '[sxplr-feature-list-directive]', + exportAs: 'featureListDirective' +}) +export class ListDirective extends FeatureBase{ + + @Input() + featureRoute: string + private guardedRoute$ = new BehaviorSubject<FeatureType>(null) + + public state$ = new BehaviorSubject<'busy'|'noresult'|'result'>('noresult') + + constructor(private sapi: SAPI) { + super() + } + + ngOnChanges(sc: SimpleChanges): void { + super.ngOnChanges(sc) + const { featureRoute } = sc + if (featureRoute) { + const featureType = (featureRoute.currentValue || '').split("/").slice(-1)[0] + this.guardedRoute$.next(AllFeatures[featureType]) + } + } + + public features$: Observable<Feature[]> = combineLatest([ + this.guardedRoute$, + this.TPRBbox$, + ]).pipe( + tap(() => this.state$.next('busy')), + switchMap(([route, { template, parcellation, region, bbox }]) => { + if (!route) { + return throwError("noresult") + } + const query = {} + if (template) query['space_id'] = template.id + if (parcellation) query['parcellation_id'] = parcellation.id + if (region) query['region_id'] = region.name + if (bbox) query['bbox'] = JSON.stringify(bbox) + return this.sapi.getV3Features(route, { + query: { + ...this.queryParams, + ...query, + } as any + }) + }), + catchError(() => { + this.state$.next("noresult") + return of([] as Feature[]) + }), + tap(result => this.state$.next(result.length > 0 ? 'result' : 'noresult')), + ) +} diff --git a/src/features/module.ts b/src/features/module.ts index 7341ca0f9..48bf3848f 100644 --- a/src/features/module.ts +++ b/src/features/module.ts @@ -22,6 +22,8 @@ import { FeatureViewComponent } from "./feature-view/feature-view.component"; import { TransformPdToDsPipe } from "./transform-pd-to-ds.pipe"; import { NgLayerCtlModule } from "src/viewerModule/nehuba/ngLayerCtlModule/module"; import { VoiBboxDirective } from "./voi-bbox.directive"; +import { FilterCategoriesPipe } from "./filterCategories.pipe"; +import { ListDirective } from "./list/list.directive"; @NgModule({ imports: [ @@ -46,6 +48,8 @@ import { VoiBboxDirective } from "./voi-bbox.directive"; EntryComponent, ListComponent, FeatureViewComponent, + FilterCategoriesPipe, + ListDirective, CategoryAccDirective, VoiBboxDirective, @@ -57,6 +61,7 @@ import { VoiBboxDirective } from "./voi-bbox.directive"; EntryComponent, FeatureViewComponent, VoiBboxDirective, + ListDirective, ], schemas: [ CUSTOM_ELEMENTS_SCHEMA, -- GitLab