Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/sidebar improvements #69

Merged
merged 6 commits into from
Sep 27, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
<div *ngIf="plugin!=null || link != null">
<div ngClass="(plugin?.tags?.length ?? 0) === 0 ? 'single-line' : ''" *ngIf="plugin!=null || link != null">
<span>
<span *ngIf="plugin==null">{{link?.name}}</span>
<span *ngIf="plugin!=null">{{plugin.title}} ({{plugin.version}})</span>
<qhana-plugin-last-used class="plugin-status" [plugin]="plugin" [color]="'accent'" [spinner]="16" *ngIf="plugin!=null">
<qhana-plugin-last-used class="plugin-status" [plugin]="plugin" [color]="'accent'" [spinner]="16"
*ngIf="plugin!=null">
</qhana-plugin-last-used>
</span>
<mat-chip-list class="tags">
<mat-chip-list class="tags" [hidden]="(plugin?.tags?.length ?? 0) === 0">
<mat-chip class="tag" *ngFor="let tag of plugin?.tags ?? []">{{tag}}</mat-chip>
</mat-chip-list>
</div>
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,9 @@

.tag
min-height: 1.8em

.single-line
display: flex
flex-direction: row
align-items: stretch
justify-content: space-between
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Component, Input, OnChanges, SimpleChanges } from '@angular/core';
import { Component, EventEmitter, Input, OnChanges, Output, SimpleChanges } from '@angular/core';
import { ApiLink, ApiResponse } from 'src/app/services/api-data-types';
import { PluginApiObject } from 'src/app/services/qhana-api-data-types';
import { PluginRegistryBaseService } from 'src/app/services/registry.service';
Expand All @@ -13,7 +13,7 @@ export class PluginListItemComponent implements OnChanges {
@Input() link: ApiLink | null = null;
@Input() search: string | null = null;

isInSearch: boolean = false;
@Output() isInSearch: EventEmitter<boolean> = new EventEmitter(true);

plugin: PluginApiObject | null = null;

Expand Down Expand Up @@ -87,7 +87,7 @@ export class PluginListItemComponent implements OnChanges {
*/
deferredIsInSearchUpdate(value: boolean) {
Promise.resolve().then(() => {
this.isInSearch = value;
this.isInSearch.emit(value);
});
}

Expand Down
7 changes: 4 additions & 3 deletions src/app/components/growing-list/growing-list.component.html
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
<ul class="list">
<ng-container [ngSwitch]="link.resourceType" *ngFor="let link of items; trackBy: trackBy">
<ng-container [ngSwitch]="link.resourceType" *ngFor="let link of items; index as index trackBy: trackBy">

<li class="list-item {{isHighlighted(link) ? 'highlighted' : ''}}" *ngSwitchCase="'plugin'"
(click)="onItemClick(link)" [hidden]="!pluginItem.isInSearch">
<qhana-plugin-list-item class="default-content" [link]="link" [search]="normalizedSearch" #pluginItem>
(click)="onItemClick(link)" [hidden]="!(pluginItem.isInSearch|async)">
<qhana-plugin-list-item class="default-content" [link]="link" [search]="normalizedSearch"
(isInSearch)="itemsInSearch[index] = $event; onItemsInSearchChanged()" #pluginItem>
</qhana-plugin-list-item>
</li>

Expand Down
3 changes: 2 additions & 1 deletion src/app/components/growing-list/growing-list.component.sass
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
min-height: 3rem
padding-block: 0.2rem
padding-inline: 1rem
cursor: pointer

.list-item:not(:last-of-type)
border-block-end: 1px solid var(--border-color)
Expand Down Expand Up @@ -40,7 +41,7 @@
align-items: center

.default-content
min-height: 100%
min-height: calc(3rem - 0.4rem)
padding-inline: 1rem
cursor: pointer

Expand Down
73 changes: 72 additions & 1 deletion src/app/components/growing-list/growing-list.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export class GrowingListComponent implements OnInit, OnDestroy {
@Input() set search(value: string | null) {
this.normalizedSearch = value?.toLowerCase()?.trim() ?? null;
}
@Input() autoloadOnSearch: boolean | number = false;

normalizedSearch: string | null = null;

Expand All @@ -42,7 +43,9 @@ export class GrowingListComponent implements OnInit, OnDestroy {
@Input() highlightByKey: string | null = null;

@Output() itemsChanged: EventEmitter<ApiLink[]> = new EventEmitter<ApiLink[]>();
@Output() visibleItems: EventEmitter<ApiLink[]> = new EventEmitter<ApiLink[]>();
@Output() collectionSize: EventEmitter<number> = new EventEmitter<number>();
@Output() visibleCollectionSize: EventEmitter<number> = new EventEmitter<number>();
@Output() clickItem: EventEmitter<ApiLink> = new EventEmitter<ApiLink>();
@Output() editItem: EventEmitter<ApiLink> = new EventEmitter<ApiLink>();
@Output() deleteItem: EventEmitter<ApiLink> = new EventEmitter<ApiLink>();
Expand All @@ -63,6 +66,7 @@ export class GrowingListComponent implements OnInit, OnDestroy {
private deletedItemsSubscription: Subscription | null = null;

items: ApiLink[] = [];
itemsInSearch: boolean[] = [];

constructor(private registry: PluginRegistryBaseService, private dialog: MatDialog) { }

Expand Down Expand Up @@ -116,8 +120,12 @@ export class GrowingListComponent implements OnInit, OnDestroy {
this.loadMoreClicked = false;
this.lastCollectionSize = null;
this.items = [];
this.itemsInSearch = [];
this.setupGrowingList();
}
if (changes.search) {
this.updateIsInSearch();
}
}

private setupGrowingList() {
Expand Down Expand Up @@ -148,6 +156,7 @@ export class GrowingListComponent implements OnInit, OnDestroy {
this.loadMoreApiLink = null;
this.lastCollectionSize = null;
this.collectionSize.emit(0);
this.visibleCollectionSize.emit(0);
const query = this.query;
this.updateQueue.next(() => this.replaceApiLinkQueued(newApiLink, query));
}
Expand Down Expand Up @@ -179,18 +188,25 @@ export class GrowingListComponent implements OnInit, OnDestroy {
this.isLoading = false;
return;
}
const itemsInSearch = new Array(response.data.items.length);
response.data.items.forEach((link, index) => itemsInSearch[index] = this.isInSearch(link));
this.startApiLink = newApiLink;
this.startQueryArgs = query;
this.items = [...response.data.items];
this.itemsInSearch = itemsInSearch;
this.loadMoreApiLink = response.links.find(link => matchesLinkRel(link, "next")) ?? null;
this.isLoading = false;
this.loadMoreClicked = false;
this.itemsChanged.emit([...this.items]);
this.lastCollectionSize = response.data.collectionSize;
this.collectionSize.emit(response.data.collectionSize);
this.onItemsInSearchChanged();
}

loadMore() {
if (this.loadMoreClicked) {
return; // do not allow multiple parallel loadMore commands in queue
}
this.loadMoreClicked = true;
this.updateQueue.next(() => this.loadMoreQueued());
}
Expand All @@ -217,13 +233,17 @@ export class GrowingListComponent implements OnInit, OnDestroy {
this.isLoading = false;
return;
}
let newItemsInSearch = new Array(response.data.items.length);
response.data.items.forEach((link, index) => newItemsInSearch[index] = this.isInSearch(link));
this.items = [...items, ...response.data.items];
this.itemsInSearch = [...this.itemsInSearch, ...newItemsInSearch];
this.loadMoreApiLink = response.links.find(link => matchesLinkRel(link, "next")) ?? null;
this.isLoading = false;
this.loadMoreClicked = false;
this.itemsChanged.emit([...this.items]);
this.lastCollectionSize = response.data.collectionSize;
this.collectionSize.emit(response.data.collectionSize);
this.onItemsInSearchChanged();
}

private async onNewObjectQueued(newObjectLink: ApiLink) {
Expand All @@ -233,9 +253,11 @@ export class GrowingListComponent implements OnInit, OnDestroy {
return;
}
this.items = [...this.items, newObjectLink];
this.itemsInSearch = [...this.itemsInSearch, this.isInSearch(newObjectLink)];
this.itemsChanged.emit([...this.items]);
this.lastCollectionSize = (this.lastCollectionSize ?? 0) + 1; // extrapolate collection size
this.collectionSize.emit(this.lastCollectionSize ?? 0);
this.onItemsInSearchChanged();
}

private async onChangedObjectQueued(changedObjectLink: ApiLink) {
Expand All @@ -256,8 +278,12 @@ export class GrowingListComponent implements OnInit, OnDestroy {
return;
}
newItems[index] = changedObjectLink;
const newItemsInSearch = [...this.itemsInSearch];
newItemsInSearch[index] = this.isInSearch(changedObjectLink);
this.items = newItems;
this.itemsInSearch = newItemsInSearch;
this.itemsChanged.emit([...this.items]);
this.onItemsInSearchChanged();
}

const newItemRels = this.newItemRels;
Expand All @@ -267,11 +293,19 @@ export class GrowingListComponent implements OnInit, OnDestroy {
}

private async onDeletedObjectQueued(deletedObjectLink: ApiLink) {
const newItems = this.items.filter(link => link.href !== deletedObjectLink.href);
const toRemove = new Set<number>();
const newItems = this.items.filter((link, index) => {
if (link.href === deletedObjectLink.href) {
toRemove.add(index);
return false; // filter out matches
}
return true;
});
if (newItems.length === this.items.length) {
return; // nothing filtered
}
this.items = newItems;
this.itemsInSearch = this.itemsInSearch.filter((value, index) => !toRemove.has(index));
this.itemsChanged.emit([...this.items]);
if (this.lastCollectionSize == null) {
this.collectionSize.emit(0);
Expand All @@ -282,6 +316,19 @@ export class GrowingListComponent implements OnInit, OnDestroy {
this.lastCollectionSize -= 1; // extrapolate collection size
this.collectionSize.emit(this.lastCollectionSize);
}
this.onItemsInSearchChanged();
}

private updateIsInSearch() {
const newItemsInSearch = [...this.itemsInSearch];
this.items.forEach((link, index) => {
if (link.resourceType === "plugin") {
return; // plugins have special search inclusions
}
newItemsInSearch[index] = this.isInSearch(link);
});
this.itemsInSearch = newItemsInSearch;
this.onItemsInSearchChanged();
}

trackBy: TrackByFunction<ApiLink> = (index, item: ApiLink): string => {
Expand All @@ -304,6 +351,30 @@ export class GrowingListComponent implements OnInit, OnDestroy {
return link.name?.toLowerCase()?.includes(search) ?? false;
}

onItemsInSearchChanged() {
const seen = new Set<string>();
const itemsInSearch = this.items.filter((link, index) => {
if (seen.has(link.href)) {
return false;
}
seen.add(link.href);
return this.itemsInSearch[index];
});
const itemsInSearchCount = itemsInSearch.length;
Promise.resolve().then(() => {
// defer update for angualar change detection
this.visibleItems.emit(itemsInSearch);
this.visibleCollectionSize.emit(itemsInSearchCount);
});
if (this.autoloadOnSearch !== false) {
const minItems = this.autoloadOnSearch === true ? 0 : this.autoloadOnSearch;
const underMinItems = itemsInSearchCount <= minItems;
if (underMinItems && this.loadMoreApiLink != null) {
this.loadMore();
}
}
}

onItemClick(link: ApiLink) {
this.clickItem.emit(link);
}
Expand Down
18 changes: 12 additions & 6 deletions src/app/components/plugin-sidebar/plugin-sidebar.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -31,20 +31,25 @@ <h2 class="template-header">
</h2>
</summary>
<mat-divider></mat-divider>
<button mat-button class="w-100 all-plugins-button" (click)="selectTemplate(null)"
*ngIf="defaultTemplate">
<button mat-button
class="w-100 builtin-template-button {{(templateId==null && useExternalDefaultTemplate) ? 'active' : ''}}"
(click)="selectTemplate(null)" *ngIf="defaultTemplate">
use default template
</button>
<mat-divider *ngIf="defaultTemplate"></mat-divider>
<button mat-button class="w-100 all-plugins-button" (click)="selectTemplate(null, 'all-plugins')">
<button mat-button
class="w-100 builtin-template-button {{(templateId==null && !useExternalDefaultTemplate) ? 'active' : ''}}"
(click)="selectTemplate(null, 'all-plugins')">
show all plugins
</button>
<mat-divider></mat-divider>
<qhana-growing-list [rels]="['ui-template', 'collection']" [newItemRels]="['ui-template']"
[highlighted]="highlightedTemplates" [highlightByKey]="'uiTemplateId'"
(clickItem)="selectTemplate($event)"></qhana-growing-list>
<mat-divider></mat-divider>
<button mat-ripple class="sidebar-button w-100" (click)="createTemplate()">Create Template</button>
<button class="new-template-button" mat-stroked-button (click)="createTemplate()">
Create Template
</button>
</details>
<mat-form-field class="full-width" color="primary"
[hidden]="activeArea == 'detail' || activeArea == 'templates'">
Expand All @@ -66,15 +71,16 @@ <h3 class="plugin-group-header">
<span class="plugin-group-header-text">
<mat-icon>extension</mat-icon>
{{group.name}}
({{pluginList.visibleCollectionSize|async}}/{{pluginList.collectionSize|async}})
</span>
<mat-icon matSuffix [hidden]="group.open">expand_more</mat-icon>
<mat-icon matSuffix [hidden]="!group.open">expand_less</mat-icon>
</h3>
<mat-divider></mat-divider>
</summary>
<qhana-growing-list [apiLink]="group.link" [query]="group?.query ?? null" [search]="searchInput.value"
[highlighted]="highlightedPlugins" [highlightByKey]="'pluginId'"
(clickItem)="selectPlugin($event)"></qhana-growing-list>
[autoloadOnSearch]="true" [highlighted]="highlightedPlugins" [highlightByKey]="'pluginId'"
(clickItem)="selectPlugin($event)" #pluginList></qhana-growing-list>
<mat-divider></mat-divider>
</details>
</div>
Expand Down
15 changes: 14 additions & 1 deletion src/app/components/plugin-sidebar/plugin-sidebar.component.sass
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,23 @@
padding-inline: 1rem
cursor: pointer

.all-plugins-button
.builtin-template-button
min-height: 3rem
display: inline-flex
align-items: center
font-size: initial

.builtin-template-button:hover, .builtin-template-button:focus, .builtin-template-button:active, .builtin-template-button.active
background-color: var(--primary-lighter)

@media (prefers-color-scheme: dark)
.builtin-template-button:hover, .builtin-template-button:focus, .builtin-template-button:active, .builtin-template-button.active
background-color: var(--primary)

.new-template-button
width: calc(100% - 1rem)
margin-inline: 0.5rem
margin-block-start: 1rem

.plugin-group
margin-block-end: 1rem
Expand Down
Loading