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

UI Template Tab Improvements #77

Merged
merged 19 commits into from
Aug 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
701ffcc
Add custom scrollbar styles
buehlefs Aug 2, 2024
e518652
Fix potential cyclic updates during angular change detection
buehlefs Aug 2, 2024
eca828d
Remove list item decorations in growing list component
buehlefs Aug 2, 2024
a1e1288
Refactor plugin sidebar styles to allow independent scrolling in sidebar
buehlefs Aug 2, 2024
456cfa8
Add icon support to plugin groups in sidebar
buehlefs Aug 2, 2024
a917eb9
Refactor tab group sorting and prefer group link name in some cases
buehlefs Aug 2, 2024
ccb9390
Fix plugin filter editor error on empty string inputs
buehlefs Aug 2, 2024
ebba207
Add group_key and icon attributes to template tab object and edit form
buehlefs Aug 2, 2024
9f7c6c0
Display group key for group tabs in template details
buehlefs Aug 2, 2024
2365e88
Display tab icons and group indicator in nav bar
buehlefs Aug 2, 2024
ad732c2
Use tab icons in plugin sidebar
buehlefs Aug 2, 2024
86fce95
Reimplement plugin tab component to work with nested groups and displ…
buehlefs Aug 2, 2024
4936878
Fix plugin types autocomplete triggereing for wrong filter type
buehlefs Aug 2, 2024
4ddd13f
Add dialog to select plugin name/id in plugin filter editor
buehlefs Aug 2, 2024
fc865c6
fix default details marker visible in Safari
PhilWun Aug 5, 2024
fd15d7e
Fix angular not reusing tab component over multiple routes
buehlefs Aug 5, 2024
424b370
Improve template tab form validity checking
buehlefs Aug 5, 2024
9945720
Fix scroll into view not working for safari
buehlefs Aug 5, 2024
a3abb2e
fix TypeError in Safari
PhilWun Aug 5, 2024
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
99 changes: 94 additions & 5 deletions src/app/app-routing.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
*/

import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { RouterModule, Routes, UrlMatcher, UrlMatchResult, UrlSegment } from '@angular/router';
import { DataDetailComponent } from './components/data-detail/data-detail.component';
import { ExperimentDataComponent } from './components/experiment-data/experiment-data.component';
import { ExperimentTimelineComponent } from './components/experiment-timeline/experiment-timeline.component';
Expand All @@ -26,11 +26,98 @@ import { PluginTabComponent } from './components/plugin-tab/plugin-tab.component
import { SettingsPageComponent } from './components/settings-page/settings-page.component';
import { TimelineStepComponent } from './components/timeline-step/timeline-step.component';

const NUMBER_REGEX = /^[0-9]+$/;

const extraTabsMatcher: UrlMatcher = (segments: UrlSegment[], group, route): UrlMatchResult | null => {
const consumed: UrlSegment[] = [];
const params: { [props: string]: UrlSegment } = {};

let tabId: UrlSegment | null = null;
let pluginId: UrlSegment | null = null;

// match: /experiments/:experimentId
let index = 0;
if (segments[index]?.path === "experiments") {
consumed.push(segments[index]);
index += 1;
if ((segments[index]?.path ?? "").match(NUMBER_REGEX)) {
params.experimentId = segments[index];
consumed.push(segments[index]);
index += 1;
} else {
return null;
}
}

console.log(consumed, params)

// match: ./extra[/:path]/:templateTabId
if (segments[index]?.path !== "extra") {
return null;
}
consumed.push(segments[index]);
index += 1;
if (segments[index + 1]?.path.match(NUMBER_REGEX)) {
// push [/:path]
params.path = segments[index];
consumed.push(segments[index]);
index += 1;
// push /:templateTabId
tabId = segments[index];
consumed.push(segments[index]);
index += 1;
} else if (segments[index]?.path.match(NUMBER_REGEX)) {
// push /:templateTabId
tabId = segments[index];
consumed.push(segments[index]);
index += 1;
}

if (tabId == null) {
// sanity check
return null;
}
params.templateTabId = tabId;

console.log(consumed, params)

// found full match?
if (index === segments.length) {
return {
consumed: consumed,
posParams: params,
}
}

// match: ./plugin/:pluginId
if (segments[index]?.path !== "plugins") {
return null;
}
consumed.push(segments[index]);
index += 1;
if (segments[index]?.path.match(NUMBER_REGEX)) {
pluginId = segments[index];
consumed.push(segments[index]);
index += 1;
}

console.log(consumed, params)

// found full match?
if (index === segments.length && pluginId != null) {
params.pluginId = pluginId;
return {
consumed: consumed,
posParams: params,
}
}

return null;
}

const routes: Routes = [
{ path: '', component: ExperimentsPageComponent },
{ path: 'settings', component: SettingsPageComponent },
{ path: 'extra/:templateTabId', component: PluginTabComponent },
{ path: 'extra/:templateTabId/plugin/:pluginId', component: PluginTabComponent },
{ path: 'experiments', component: ExperimentsPageComponent },
{ path: 'experiments/:experimentId', redirectTo: "info" },
{ path: 'experiments/:experimentId/info', component: ExperimentComponent },
Expand All @@ -41,8 +128,10 @@ const routes: Routes = [
{ path: 'experiments/:experimentId/timeline', component: ExperimentTimelineComponent },
{ path: 'experiments/:experimentId/timeline/:step', component: TimelineStepComponent },
{ path: 'experiments/:experimentId/timeline/:step/:stepTabId', component: TimelineStepComponent },
{ path: 'experiments/:experimentId/extra/:templateTabId', component: PluginTabComponent },
{ path: 'experiments/:experimentId/extra/:templateTabId/plugin/:pluginId', component: PluginTabComponent },
{/* path: '[experiments/:experimentId/]extra/[:path/]:templateTabId/[plugins/:pluginId]' */
matcher: extraTabsMatcher,
component: PluginTabComponent,
},
];

@NgModule({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ export class PluginFilterEditorComponent implements OnInit {
}
this.registry.getByApiLink<TemplateTabApiObject>(this.tabLink).then(response => {
this.filterString = response?.data?.filterString ?? this.filterString;
if (!this.filterString) {
this.filterString = "{}";
}
this.filterObject = JSON.parse(this.filterString);
this.updateFilter(this.filterObject);
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@
</mat-form-field>
<mat-form-field class="form-field">
<mat-label>Filter String:</mat-label>
<input matInput [value]="value" (focusout)="updateFilterString($event)" list="plugin-types">
<input matInput [value]="value" (focusout)="updateFilterString($event)" [attr.list]="type === 'type' ? 'plugin-types' : null">
<datalist *ngIf="type === 'type'" id="plugin-types">
<option value="processing"></option>
<option value="visualization"></option>
Expand All @@ -80,7 +80,10 @@
Examples: "processing", "conversion", "dataloader", "visualization", "interaction"
</mat-hint>
</mat-form-field>
<button mat-raised-button type="button" (click)="delete.emit()">
<button mat-raised-button class="config-filter-button" (click)="openPluginChooser()" *ngIf="type === 'name' || type === 'id'">
choose
</button>
<button mat-raised-button class="config-filter-button" (click)="delete.emit()">
<mat-icon>delete</mat-icon>
</button>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,16 @@
display: flex
flex-direction: row
justify-content: space-between
align-items: center
align-items: flex-start
gap: 10px
font-size: 12px

.config-filter-button
display: inline-flex
align-items: center
justify-content: center
min-height: 56px

::ng-deep .config-filter > mat-form-field > .mat-mdc-form-field-wrapper
margin: 0px
padding: 0px
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import { Component, EventEmitter, Input, OnInit, Output, SimpleChanges } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { ChoosePluginDialog } from 'src/app/dialogs/choose-plugin/choose-plugin.dialog';
import { PluginApiObject } from 'src/app/services/qhana-api-data-types';

// Define filter types ('not' excluded)
// The PluginFilterNodeComponent component is designed to encapsulate a filter object and the information wether the filter is inverted ('not').
Expand All @@ -25,7 +28,7 @@ export class PluginFilterNodeComponent implements OnInit {
inverted: boolean = false;
isEmpty: boolean = true;

constructor() { }
constructor(private dialog: MatDialog) { }

ngOnInit(): void {
this.setupFilter();
Expand Down Expand Up @@ -156,4 +159,22 @@ export class PluginFilterNodeComponent implements OnInit {
}
this.filterOut.emit(JSON.parse(JSON.stringify(this.filterObject)));
}

openPluginChooser() {
const dialogRef = this.dialog.open(ChoosePluginDialog, {});
dialogRef.afterClosed().subscribe((result: PluginApiObject | null) => {
if (result == null) {
return; // nothing was selected
}
if (this.type === "id") {
this.value = result.identifier;
this.updateFilterObject();
}
if (this.type === "name") {
this.value = result.title;
this.updateFilterObject();
}
});

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@ <h2>Template Tab</h2>
</mat-radio-button>
</mat-radio-group>
</div>
<mat-form-field class="form-field" [hidden]="!templateForm.value.location || templateForm.value.location === 'workspace'">
<mat-label>Tab Group:</mat-label>
<input matInput formControlName="locationExtra">
<mat-hint>The group key of the tab this tab should be grouped under. (Use '.' to separate group keys for nested groups.)</mat-hint>
</mat-form-field>
<mat-form-field class="form-field">
<mat-label>Name:</mat-label>
<input matInput formControlName="name">
Expand All @@ -22,8 +27,20 @@ <h2>Template Tab</h2>
<mat-label>Sort Key:</mat-label>
<input matInput type="number" formControlName="sortKey">
</mat-form-field>
<div class="icon-field-wrapper">
<mat-form-field class="form-field">
<mat-label>Icon:</mat-label>
<input matInput formControlName="icon">
</mat-form-field>
<mat-icon class="icon-preview">{{templateForm.value.icon || "extension"}}</mat-icon>
</div>
<mat-form-field class="form-field">
<mat-label>Group Key:</mat-label>
<input matInput pattern="[^\.]*" formControlName="groupKey">
<mat-hint>Establish this tab as its own tab group. (Cannot contain '.')</mat-hint>
</mat-form-field>
<qhana-plugin-filter-editor [tabLink]="tabLink"
(filterEmitter)="filterString = $event"></qhana-plugin-filter-editor>
<br>
<button mat-raised-button type="submit" color="primary">{{templateLink ? 'Create' : 'Update'}} Tab</button>
(filterEmitter)="filterString = $event" [hidden]="templateForm.value.groupKey"></qhana-plugin-filter-editor>
<p class="form-error" *ngIf="templateForm?.errors?.groupKeyForbidden">Group key cannot be used in experiment workspace tabs!</p>
<button mat-raised-button class="submit-button" type="submit" color="primary" [disabled]="templateForm.errors">{{templateLink ? 'Create' : 'Update'}} Tab</button>
</form>
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,17 @@
.location-chooser
margin-block-end: 1rem

details
margin-left: 2rem
.icon-field-wrapper
display: flex
gap: 1rem
align-items: baseline

summary
margin-left: -2rem
.icon-preview
font-size: 24px
margin-inline: 1rem

dt
font-weight: bold
.submit-button
margin-block-start: 2rem

dl,
dd
font-size: 0.9rem

dd
margin-bottom: 1em
.form-error
color: var(--warn-text)
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Component, Input, OnInit } from '@angular/core';
import { FormBuilder, FormControl, FormGroup, Validators } from '@angular/forms';
import { FormBuilder, FormControl, FormGroup, ValidationErrors, Validators } from '@angular/forms';
import { ApiLink, ApiResponse } from 'src/app/services/api-data-types';
import { PluginRegistryBaseService } from 'src/app/services/registry.service';
import { TAB_GROUP_NAME_OVERRIDES, TemplateApiObject, TemplateTabApiObject } from 'src/app/services/templates.service';
Expand Down Expand Up @@ -40,27 +40,50 @@ export class TemplateDetailsComponent implements OnInit {
private initialValues = {
name: "",
description: "",
icon: null,
sortKey: 0,
location: "workspace"
location: "workspace",
locationExtra: "",
groupKey: "",
};

templateForm: FormGroup = this.fb.group({
name: [this.initialValues.name, [Validators.required, Validators.minLength(1)]],
description: this.initialValues.description,
icon: [this.initialValues.locationExtra, [Validators.maxLength(64)]],
sortKey: this.initialValues.sortKey,
location: [this.initialValues.location, [Validators.required, isInSetValidator(Object.keys(TAB_GROUP_NAME_OVERRIDES))]]
location: [this.initialValues.location, [Validators.required, isInSetValidator(Object.keys(TAB_GROUP_NAME_OVERRIDES))]],
locationExtra: [this.initialValues.locationExtra],
groupKey: [this.initialValues.locationExtra, [Validators.maxLength(32)]],
});

constructor(private registry: PluginRegistryBaseService, private fb: FormBuilder) { }

ngOnInit() {
this.templateForm.addValidators((control): ValidationErrors | null => {
const loc = control.get("location")?.getRawValue() ?? "";
if (loc === "workspace") {
const groupKey = control.get("groupKey")?.getRawValue() ?? "";
if (groupKey) {
return {
groupKeyForbidden: true,
};
}
}
return null;
});
if (this.tabLink != null) {
this.registry.getByApiLink<TemplateTabApiObject>(this.tabLink).then(response => {
const location = response?.data?.location ?? this.initialValues.location
const [baseLocation, locationExtra] = location.split(".", 2);
this.templateForm.setValue({
name: response?.data?.name ?? this.initialValues.name,
description: response?.data?.description ?? this.initialValues.description,
icon: response?.data?.icon ?? this.initialValues.icon,
sortKey: response?.data?.sortKey ?? this.initialValues.sortKey,
location: response?.data?.location ?? this.initialValues.location
groupKey: response?.data?.groupKey ?? this.initialValues.groupKey,
location: baseLocation,
locationExtra: locationExtra ?? "",
});
});
}
Expand All @@ -84,12 +107,23 @@ export class TemplateDetailsComponent implements OnInit {
}
const link = response?.links?.find(link => link.rel.some(rel => rel === findString) && link.resourceType == "ui-template-tab") ?? null;
if (link != null) {
let iconValue = this.templateForm.value.icon;
if (!iconValue) {
iconValue = null;
}
const location = [this.templateForm.value.location]
if (this.templateForm.value.location !== "workspace" && this.templateForm.value.locationExtra) {
location.push(this.templateForm.value.locationExtra);
}
const groupKey = this.templateForm.value.groupKey;
this.registry.submitByApiLink<TemplateTabApiObject>(link, {
name: this.templateForm.value.name,
description: this.templateForm.value.description,
icon: iconValue,
sortKey: this.templateForm.value.sortKey,
filterString: this.filterString,
location: this.templateForm.value.location
groupKey: groupKey,
filterString: Boolean(groupKey) ? "" : this.filterString,
location: location.join("."),
});
if (this.templateLink != null) {
this.templateForm.reset(this.initialValues);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,14 +67,19 @@ <h3 mat-subheader>Create New Template Tab</h3>
</mat-expansion-panel>
<mat-divider></mat-divider>

<ng-container *ngFor="let group of templateTabLinks | keyvalue:tabOrder; trackBy:trackByTabLink">
<h3 mat-subheader>{{tabGroupNameOverrides[group.key] ?? group.key}}</h3>
<mat-expansion-panel *ngFor="let tab of group.value" [expanded]="tabId == tab.resourceKey?.uiTemplateTabId"
(afterExpand)="selectTab(tab.resourceKey?.uiTemplateTabId ?? 'null')" (closed)="deselectTab(tab.resourceKey?.uiTemplateTabId ?? null)"
<ng-container *ngFor="let group of templateTabLinks; trackBy:trackByTabLink">
<h3 mat-subheader>{{tabGroupNameOverrides[group.group] ?? group.name}}</h3>
<mat-expansion-panel *ngFor="let tab of group.tabs" [expanded]="tabId == tab.resourceKey?.uiTemplateTabId"
(afterExpand)="selectTab(tab.resourceKey?.uiTemplateTabId ?? 'null')" (closed)="deselectTab(tab.resourceKey?.uiTemplateTabId ?? null)"
[id]="'tab-' + tab.resourceKey?.uiTemplateTabId">
<mat-expansion-panel-header>
<mat-panel-title>
{{tab.name}}
<span>
{{tab.name}}
<span *ngIf="templateTabObjects[tab.resourceKey?.uiTemplateTabId ?? '']?.groupKey">
(Group: {{templateTabObjects[tab.resourceKey?.uiTemplateTabId ?? '']?.groupKey}})
</span>
</span>
</mat-panel-title>
<mat-panel-description>
{{templateTabObjects[tab.resourceKey?.uiTemplateTabId ?? '']?.description}}
Expand Down
Loading
Loading