diff --git a/src/app/app.module.ts b/src/app/app.module.ts
index a77fb44..b27e04d 100644
--- a/src/app/app.module.ts
+++ b/src/app/app.module.ts
@@ -91,6 +91,8 @@ import { UiTemplateComponent } from "./components-small/ui-template/ui-template.
import { UiTemplateTabListComponent } from './components-small/ui-template-tab-list/ui-template-tab-list.component';
import { UiTemplateTabComponent } from './components-small/ui-template-tab/ui-template-tab.component';
import { PluginFilterViewComponent } from './components-small/plugin-filter-view/plugin-filter-view.component';
+import { UiTemplateTabFormComponent } from './components-small/ui-template-tab-form/ui-template-tab-form.component';
+import { PluginFilterFormComponent } from './components-small/plugin-filter-form/plugin-filter-form.component';
@NgModule({
declarations: [
@@ -141,7 +143,9 @@ import { PluginFilterViewComponent } from './components-small/plugin-filter-view
UiTemplateComponent,
UiTemplateTabListComponent,
UiTemplateTabComponent,
+ UiTemplateTabFormComponent,
PluginFilterViewComponent,
+ PluginFilterFormComponent,
],
imports: [
BrowserModule,
diff --git a/src/app/components-small/plugin-filter-form/plugin-filter-form.component.html b/src/app/components-small/plugin-filter-form/plugin-filter-form.component.html
new file mode 100644
index 0000000..5075b4d
--- /dev/null
+++ b/src/app/components-small/plugin-filter-form/plugin-filter-form.component.html
@@ -0,0 +1,90 @@
+@if (filterValue == null) {
+
Flter Plugins by:
+
+ Identifier
+ Name
+ Version
+ Type
+ Tag
+
+ @if(depth < 4) {
+ Or combine multiple filters with:
+
+ AND (match all filters)
+ OR (match any filter)
+
+ }
+} @else {
+
+ Exclude plugins matching this filter
+
+
+}
+@if (isNestedFilter) {
+
+ AND (match all filters)
+ OR (match any filter)
+
+
+
+
+
+
{{nestedFilterCombinator.toUpperCase()}}
+
+
+
+ @if (filterValue?.length === 0) {
+
Must have at least one plugin filter!
+ }
+ @for (f of filterValue; track $index) {
+
+ }
+
+
+
+}
+@if (!isNestedFilter && filterType != null) {
+
+
+ Filter By:
+
+ Identifier
+ Name
+ Tag
+ Version
+ Type
+
+
+
+ Match against:
+
+
+
+ Examples: ">= 0.1.0", "== 1.0.2", "< 2.0.0", ">= 1.0.0, < 2.0.0" , "!= 1.0.14"
+
+
+ Examples: "processing", "conversion", "dataloader", "visualization", "interaction"
+
+
+
+
+}
+@if (depth > 0 && (filterValue == null || filterValue === "")) {
+ Filter may not be empty!
+}
+
diff --git a/src/app/components-small/plugin-filter-form/plugin-filter-form.component.sass b/src/app/components-small/plugin-filter-form/plugin-filter-form.component.sass
new file mode 100644
index 0000000..8ee194e
--- /dev/null
+++ b/src/app/components-small/plugin-filter-form/plugin-filter-form.component.sass
@@ -0,0 +1,67 @@
+.toggle-group
+ height: 3em
+
+.filter-head
+ display: flex
+ align-items: center
+ justify-content: space-between
+ width: 100%
+ margin-block-end: 0.5rem
+
+.simple-filter
+ display: flex
+ align-items: start
+ gap: 0.5rem
+ width: 100%
+
+.filter-type-selec
+ width: 8rem
+
+.filter-value
+ width: unset
+ flex-grow: 1
+
+.filter-button
+ height: 3.5rem
+
+.nested-filter-container
+ display: flex
+ gap: 1rem
+ align-items: stretch
+ margin-block: 1rem
+
+.filter-combinator
+ display: flex
+ flex-direction: column
+ align-items: center
+ min-width: 2.1rem
+
+ .line
+ flex-grow: 1
+ width: 0px
+ border-inline-start: 1px solid var(--text-washed)
+
+.child-filter-list
+ display: flex
+ flex-direction: column
+ gap: 0.8rem
+ flex-grow: 1
+
+.child-filter-container
+ display: flex
+ align-items: stretch
+ gap: 0.5rem
+
+.child-filter
+ flex-grow: 1
+
+.child-filter-remove-button
+ height: unset
+ min-width: 2rem
+ padding: 0px
+ display: flex
+ flex-direction: column
+ align-items: center
+
+.warning
+ color: var(--accent-text)
diff --git a/src/app/components-small/plugin-filter-form/plugin-filter-form.component.ts b/src/app/components-small/plugin-filter-form/plugin-filter-form.component.ts
new file mode 100644
index 0000000..2cf90d1
--- /dev/null
+++ b/src/app/components-small/plugin-filter-form/plugin-filter-form.component.ts
@@ -0,0 +1,242 @@
+import { Component, EventEmitter, Input, 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';
+
+@Component({
+ selector: 'qhana-plugin-filter-form',
+ templateUrl: './plugin-filter-form.component.html',
+ styleUrl: './plugin-filter-form.component.sass'
+})
+export class PluginFilterFormComponent {
+ @Input() filterIn: any;
+ @Input() depth: number = 0;
+ @Output() filterOut = new EventEmitter();
+ @Output() valid = new EventEmitter();
+
+ filterValue: string | Array | null = null;
+ isNestedFilter: boolean = false;
+ nestedFilterCombinator: "and" | "or" = "or";
+ isNegated: boolean = false;
+ filterType: 'id' | 'name' | 'tag' | 'version' | 'type' | null = null;
+
+ childFiltersOutput: Array = [];
+ childFiltersValid: Array = [];
+
+ constructor(private dialog: MatDialog) { }
+
+ ngOnInit(): void { }
+
+ ngOnChanges(changes: SimpleChanges) {
+ if (changes.filterIn != null) {
+ this.setupFilter();
+ }
+ }
+
+ resetCurrentFilter() {
+ this.isNegated = false;
+ this.filterValue = null;
+ this.filterType = null;
+ this.isNestedFilter = false;
+ this.nestedFilterCombinator = "or";
+ this.childFiltersOutput = [];
+ this.childFiltersValid = [];
+ }
+
+ setupFilter() {
+ if (!this.filterIn) {
+ // null or empty
+ this.resetCurrentFilter();
+ this.updateFilterObject();
+ return;
+ }
+ let filter = this.filterIn
+ let filterKeys = Object.keys(filter);
+ if (filterKeys.length !== 1) {
+ this.resetCurrentFilter();
+ this.updateFilterObject();
+ return;
+ }
+
+ if (filterKeys[0] === "not") {
+ this.isNegated = true;
+ filter = filter.not;
+ filterKeys = Object.keys(filter);
+
+ if (filterKeys.length === 0) {
+ this.resetCurrentFilter();
+ // specifically keep negated flag if child filter is empty!
+ this.isNegated = true;
+ this.updateFilterObject();
+ return;
+ } else if (filterKeys.length > 1) {
+ this.resetCurrentFilter();
+ this.updateFilterObject();
+ return;
+ }
+ }
+
+ if (filterKeys[0] === "and") {
+ this.isNestedFilter = true;
+ this.nestedFilterCombinator = "and";
+ this.filterType = null;
+ this.childFiltersOutput = [...filter.and];
+ this.childFiltersValid = filter.and.map(() => true);
+ this.filterValue = filter.and;
+ this.updateFilterObject();
+ return;
+ }
+
+ if (filterKeys[0] === "or") {
+ this.isNestedFilter = true;
+ this.nestedFilterCombinator = "or";
+ this.filterType = null;
+ this.childFiltersOutput = [...filter.or];
+ this.childFiltersValid = filter.or.map(() => true);
+ this.filterValue = filter.or;
+ this.updateFilterObject();
+ return;
+ }
+
+ if (['id', 'name', 'tag', 'version', 'type'].some(t => t === filterKeys[0])) {
+ this.isNestedFilter = false;
+ this.nestedFilterCombinator = "or";
+ this.childFiltersOutput = [];
+ this.childFiltersValid = [];
+ this.filterType = filterKeys[0] as any;
+ this.filterValue = filter[filterKeys[0]];
+ this.updateFilterObject();
+ return;
+ }
+
+ this.resetCurrentFilter();
+ this.updateFilterObject();
+ return;
+ }
+
+ updateFilterObject() {
+ let filter: any = {};
+ if (this.isNestedFilter) {
+ filter[this.nestedFilterCombinator] = this.childFiltersOutput.filter(v => Boolean(v));
+ } else if (this.filterType != null) {
+ filter[this.filterType] = this.filterValue;
+ }
+
+ let isValid = true;
+ if (this.isNestedFilter) {
+ if (this.childFiltersOutput.length === 0) {
+ isValid = false; // nested filters must have at least one child filter
+ }
+ if (this.childFiltersValid.some(valid => !valid)) {
+ isValid = false; // nested filters cannot have invalid child filters
+ }
+ } else if (this.depth > 0) {
+ if (this.filterType == null) {
+ isValid = false; // nested filters cannot be empty
+ }
+ if (this.filterValue == null || this.filterValue === "") {
+ isValid = false; // nested filters cannot be empty
+ }
+ }
+
+ if (this.isNegated) {
+ filter = { not: filter };
+ }
+
+ if (this.childFiltersOutput === this.filterValue) {
+ console.warn("Possible infinite update loop, aborting update!");
+ return
+ }
+ Promise.resolve().then(() => {
+ this.valid.emit(isValid);
+ this.filterOut.emit(filter);
+ });
+ }
+
+ deleteChild(index: number) {
+ if (!this.isNestedFilter || !Array.isArray(this.filterValue)) {
+ return;
+ }
+ if (this.childFiltersOutput.length >= index + 1) {
+ const newFilterValue = [...this.childFiltersOutput]
+ newFilterValue.splice(index, 1);
+ this.childFiltersOutput = newFilterValue
+ this.childFiltersValid.splice(index, 1);
+ this.filterValue = [...newFilterValue];
+ this.updateFilterObject();
+ }
+ }
+
+ addChildFilter() {
+ if (!this.isNestedFilter || !Array.isArray(this.filterValue)) {
+ return;
+ }
+ const filter: any = {};
+ this.childFiltersOutput.push(filter);
+ this.childFiltersValid.push(true);
+ this.filterValue.push(filter);
+ this.updateFilterObject();
+ }
+
+ setType(type: 'and' | 'or' | 'id' | 'name' | 'tag' | 'version' | 'type') {
+ if (this.isNestedFilter || this.filterType != null) {
+ return; // already chose a filter type
+ }
+ if (type === "and" || type === "or") {
+ this.isNestedFilter = true;
+ this.nestedFilterCombinator = type;
+ this.filterType = null;
+ // start with one empty filter
+ this.childFiltersOutput = [null];
+ this.childFiltersValid = [true];
+ this.filterValue = [null];
+ this.updateFilterObject();
+ return;
+ }
+
+ this.isNestedFilter = false;
+ this.nestedFilterCombinator = "or";
+ this.filterType = type;
+ this.childFiltersOutput = [];
+ this.childFiltersValid = []
+ this.filterValue = "";
+ this.updateFilterObject();
+ }
+
+ changeFilterCombinator(combinator: "and" | "or") {
+ if (!this.isNestedFilter || this.nestedFilterCombinator === combinator) {
+ return;
+ }
+ this.nestedFilterCombinator = combinator;
+ this.updateFilterObject();
+ }
+
+ changeSimpleFilterType(type: 'id' | 'name' | 'tag' | 'version' | 'type') {
+ if (this.isNestedFilter || this.filterType === type) {
+ return;
+ }
+ this.filterType = type;
+ this.updateFilterObject();
+ }
+
+ openPluginChooser() {
+ const dialogRef = this.dialog.open(ChoosePluginDialog, {});
+ dialogRef.afterClosed().subscribe((result: PluginApiObject | null) => {
+ if (result == null) {
+ return; // nothing was selected
+ }
+ if (this.isNestedFilter) {
+ return;
+ }
+ if (this.filterType === "id") {
+ this.filterValue = result.identifier;
+ this.updateFilterObject();
+ }
+ if (this.filterType === "name") {
+ this.filterValue = result.title;
+ this.updateFilterObject();
+ }
+ });
+ }
+
+}
diff --git a/src/app/components-small/ui-template-tab-form/ui-template-tab-form.component.html b/src/app/components-small/ui-template-tab-form/ui-template-tab-form.component.html
new file mode 100644
index 0000000..f775fc5
--- /dev/null
+++ b/src/app/components-small/ui-template-tab-form/ui-template-tab-form.component.html
@@ -0,0 +1,43 @@
+
diff --git a/src/app/components-small/ui-template-tab-form/ui-template-tab-form.component.sass b/src/app/components-small/ui-template-tab-form/ui-template-tab-form.component.sass
new file mode 100644
index 0000000..2d17ff5
--- /dev/null
+++ b/src/app/components-small/ui-template-tab-form/ui-template-tab-form.component.sass
@@ -0,0 +1,25 @@
+.form-field
+ width: 100%
+ margin-block-end: 0.5rem
+
+.filter-example
+ resize: none
+ overflow: hidden
+
+.location-chooser
+ margin-block-end: 1rem
+
+.icon-field-wrapper
+ display: flex
+ gap: 1rem
+ align-items: baseline
+
+.icon-preview
+ font-size: 24px
+ margin-inline: 1rem
+
+.submit-button
+ margin-block-start: 2rem
+
+.form-error
+ color: var(--warn-text)
diff --git a/src/app/components-small/ui-template-tab-form/ui-template-tab-form.component.ts b/src/app/components-small/ui-template-tab-form/ui-template-tab-form.component.ts
new file mode 100644
index 0000000..675569d
--- /dev/null
+++ b/src/app/components-small/ui-template-tab-form/ui-template-tab-form.component.ts
@@ -0,0 +1,186 @@
+import { Component, EventEmitter, Input, OnChanges, OnDestroy, OnInit, Output, SimpleChanges } from '@angular/core';
+import { TAB_GROUP_NAME_OVERRIDES, TemplateTabApiObject } from 'src/app/services/templates.service';
+import { FormBuilder, FormGroup, ValidationErrors, Validators, ValidatorFn, AbstractControl } from '@angular/forms';
+import { Subscription } from 'rxjs';
+
+export function isInSetValidator(validValues: any[]): ValidatorFn {
+ return (control: AbstractControl): { [key: string]: any } | null => {
+ if (!validValues.includes(control.value)) {
+ return { invalidValue: true };
+ }
+ return null;
+ };
+}
+
+@Component({
+ selector: 'qhana-ui-template-tab-form',
+ templateUrl: './ui-template-tab-form.component.html',
+ styleUrl: './ui-template-tab-form.component.sass'
+})
+export class UiTemplateTabFormComponent implements OnChanges, OnDestroy, OnInit {
+
+ @Input() tabData: TemplateTabApiObject | null = null;
+
+ @Output() isDirty: EventEmitter = new EventEmitter(false);
+ @Output() isValid: EventEmitter = new EventEmitter(false);
+ @Output() data: EventEmitter = new EventEmitter();
+ @Output() formSubmit: EventEmitter = new EventEmitter();
+
+ private formStatusSubscription: Subscription | null = null;
+ private formValueSubscription: Subscription | null = null;
+
+ description: string = "";
+
+ currentPluginFilter: any = null;
+
+ updatedPluginFilter: any = null;
+ pluginFilterValid: boolean = false;
+
+ tabGroupNameOverrides = Object.entries(TAB_GROUP_NAME_OVERRIDES)
+ .map(entry => { return { key: entry[0], value: entry[1] }; })
+ .sort((a, b) => {
+ if (a.key === "workspace") {
+ return -1;
+ }
+ else if (b.key === "workspace") {
+ return 1;
+ }
+ return a.value.localeCompare(b.value)
+ });
+
+ private initialValues = {
+ name: "",
+ icon: null,
+ sortKey: 0,
+ location: "workspace",
+ locationExtra: "",
+ groupKey: "",
+ };
+
+ templateForm: FormGroup | null = null;
+
+ constructor(private fb: FormBuilder) { }
+
+ ngOnInit(): void {
+ this.ngOnChanges({});
+ }
+
+ ngOnDestroy(): void {
+ this.formStatusSubscription?.unsubscribe();
+ this.formValueSubscription?.unsubscribe();
+ }
+
+ ngOnChanges(changes: SimpleChanges): void {
+ this.formStatusSubscription?.unsubscribe();
+ this.formValueSubscription?.unsubscribe();
+ const templateForm = this.fb.group({
+ name: [this.initialValues.name, [Validators.required, Validators.minLength(1)]],
+ icon: [this.initialValues.locationExtra, [Validators.maxLength(64)]],
+ sortKey: this.initialValues.sortKey,
+ location: [this.initialValues.location, [Validators.required, isInSetValidator(Object.keys(TAB_GROUP_NAME_OVERRIDES))]],
+ locationExtra: [this.initialValues.locationExtra],
+ groupKey: [this.initialValues.locationExtra, [Validators.maxLength(32)]],
+ });
+ 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.tabData != null) {
+ const location = this.tabData.location;
+ const [baseLocation, locationExtra] = location.split(".", 2);
+ this.description = this.tabData.description;
+ try {
+ this.currentPluginFilter = JSON.parse(this.tabData.filterString);
+ } catch {
+ this.currentPluginFilter = null;
+ }
+ templateForm.setValue({
+ name: this.tabData.name,
+ icon: this.tabData.icon,
+ sortKey: this.tabData.sortKey,
+ groupKey: this.tabData.groupKey,
+ location: baseLocation,
+ locationExtra: locationExtra ?? "",
+ });
+ } else {
+ this.description = "";
+ }
+ this.formStatusSubscription = templateForm.statusChanges.subscribe(() => {
+ this.updateDirty();
+ this.isValid.emit(!templateForm.invalid);
+ });
+ this.formValueSubscription = templateForm.valueChanges.subscribe((value) => {
+ let location = value.location ?? "workspace";
+ if (value.locationExtra) {
+ location = `${location}.${value.locationExtra}`;
+ }
+ const data: any = {
+ "name": value.name,
+ "icon": value.icon,
+ "sortKey": value.sortKey,
+ "location": location,
+ "groupKey": value.groupKey ?? "",
+ };
+ data.description = this.description;
+ if (value.groupKey) {
+ data.filterString = ""
+ } else {
+ if (this.updatedPluginFilter) {
+ data.filterString = JSON.stringify(this.updatedPluginFilter);
+ } else {
+ data.filterString = "{}";
+ }
+ }
+ this.data.emit(data);
+ });
+ this.templateForm = templateForm;
+ }
+
+ public submitForm() {
+ this.templateForm?.updateValueAndValidity();
+ this.formSubmit.emit();
+ }
+
+ async onSubmit() {
+ this.formSubmit.emit();
+ }
+
+ updateDirty() {
+ let dirty = (this.templateForm?.dirty ?? false) || (this.description !== this.tabData?.description ?? "");
+
+ if (Boolean(this.currentPluginFilter) && Boolean(this.updatedPluginFilter)) {
+ if (JSON.stringify(this.currentPluginFilter) !== JSON.stringify(this.updatedPluginFilter)) {
+ dirty = true;
+ }
+ } else if (Boolean(this.currentPluginFilter) !== Boolean(this.updatedPluginFilter)) {
+ dirty = true;
+ }
+
+ Promise.resolve().then(() => {
+ this.isDirty.emit(dirty);
+ });
+ }
+
+ updateValid() {
+ let isValid = true;
+ if (this.templateForm == null || this.templateForm.invalid) {
+ isValid = false;
+ }
+ if (!this.pluginFilterValid) {
+ isValid = false;
+ }
+
+ Promise.resolve().then(() => {
+ this.isValid.emit(isValid);
+ });
+ }
+
+}
diff --git a/src/app/components-small/ui-template-tab-list/ui-template-tab-list.component.html b/src/app/components-small/ui-template-tab-list/ui-template-tab-list.component.html
index 0ca7c65..79c848b 100644
--- a/src/app/components-small/ui-template-tab-list/ui-template-tab-list.component.html
+++ b/src/app/components-small/ui-template-tab-list/ui-template-tab-list.component.html
@@ -1,4 +1,12 @@
- 0 || navigationGroup != null">
+
+
+ Create New Tab
+
+
+
+
+
+
0 || navigationGroup != null)">
Navigation Tab Groups
@for (tab of navigationGroup.tabs; track tab.href) {
@@ -11,7 +19,7 @@ Navigation Tab Groups
(Group: {{navigationGroup.groupKey}}.{{hrefToTab.get(tab.href)?.groupKey}})
-
+
}
@@ -33,17 +41,13 @@
(Group: {{group.groupKey}}.{{hrefToTab.get(tab.href)?.groupKey}})
-
+
}
}
-
- - {{navigationGroup.name}}
- - {{group.name}} - {{group.groupKey}}
-
-
+
Experiment Workspace Tabs
@for (tab of workspaceGroup.tabs; track tab.href) {
@@ -54,12 +58,12 @@ Experiment Workspace Tabs
{{tab.name}}
-
+
}
-
0 || experimentNavigationGroup != null">
+
0 || experimentNavigationGroup != null)">
Experiment Navigation Tab Groups
@for (tab of experimentNavigationGroup.tabs; track tab.href) {
@@ -72,7 +76,7 @@ Experiment Navigation Tab Groups
(Group: {{experimentNavigationGroup.groupKey}}.{{hrefToTab.get(tab.href)?.groupKey}})
-
+
}
@@ -94,13 +98,13 @@
(Group: {{group.groupKey}}.{{hrefToTab.get(tab.href)?.groupKey}})
-
+
}
}
-
0">
+
0">
Unknown Tab Groups
@for (group of unknownGroups; track group.groupKey) {
@@ -119,7 +123,7 @@
(Group: {{group.groupKey}}.{{hrefToTab.get(tab.href)?.groupKey}})
-
+
}
diff --git a/src/app/components-small/ui-template-tab-list/ui-template-tab-list.component.sass b/src/app/components-small/ui-template-tab-list/ui-template-tab-list.component.sass
index 8890269..5f4b709 100644
--- a/src/app/components-small/ui-template-tab-list/ui-template-tab-list.component.sass
+++ b/src/app/components-small/ui-template-tab-list/ui-template-tab-list.component.sass
@@ -4,3 +4,6 @@
.group-title-extra
color: var(--text-washed)
+
+.new-tab-button
+ margin-block-start: 1rem
diff --git a/src/app/components-small/ui-template-tab-list/ui-template-tab-list.component.ts b/src/app/components-small/ui-template-tab-list/ui-template-tab-list.component.ts
index ecbcc38..62e7c42 100644
--- a/src/app/components-small/ui-template-tab-list/ui-template-tab-list.component.ts
+++ b/src/app/components-small/ui-template-tab-list/ui-template-tab-list.component.ts
@@ -22,6 +22,8 @@ export class UiTemplateTabListComponent implements OnInit, OnChanges, OnDestroy
@Input() templateLink: ApiLink | null = null;
+ isLoading: boolean = true;
+
navigationGroup: NavTabGroup | null = null;
workspaceGroup: NavTabGroup | null = null;
experimentNavigationGroup: NavTabGroup | null = null;
@@ -31,6 +33,12 @@ export class UiTemplateTabListComponent implements OnInit, OnChanges, OnDestroy
private allGroups: NavTabGroup[] = [];
+ // new tab
+ createTabLink: ApiLink | null = null;
+ newTabData: any = null;
+ isValid: boolean = false;
+
+
hrefToTab: Map = new Map();
private newTabsSubscription: Subscription | null = null;
@@ -76,6 +84,7 @@ export class UiTemplateTabListComponent implements OnInit, OnChanges, OnDestroy
}
ngOnChanges(changes: SimpleChanges): void {
+ this.isLoading = true;
this.loadTemplateGroups();
}
@@ -88,9 +97,10 @@ export class UiTemplateTabListComponent implements OnInit, OnChanges, OnDestroy
this.navigationGroups = [];
this.experimentNavigationGroups = [];
this.unknownGroups = [];
+ this.isLoading = false;
return;
}
- const templateResponse = await this.registry.getByApiLink(this.templateLink, null, false);
+ const templateResponse = await this.registry.getByApiLink(this.templateLink, null, true);
if (templateResponse == null) {
this.hrefToTab = new Map();
this.workspaceGroup = null;
@@ -99,9 +109,12 @@ export class UiTemplateTabListComponent implements OnInit, OnChanges, OnDestroy
this.navigationGroups = [];
this.experimentNavigationGroups = [];
this.unknownGroups = [];
+ this.isLoading = false;
return;
}
+ this.createTabLink = templateResponse?.links?.find?.(link => link.rel.some(rel => rel === "create") && link.resourceType == "ui-template-tab") ?? null;
+
const hrefToTab = new Map();
const tabPromises: Promise[] = [];
@@ -118,7 +131,7 @@ export class UiTemplateTabListComponent implements OnInit, OnChanges, OnDestroy
const groupIcons = new Map();
const groupPromises = templateResponse.data.groups.map(async (group) => {
- const groupResponse = await this.registry.getByApiLink(group);
+ const groupResponse = await this.registry.getByApiLink(group, null, true);
// fetch tab data
groupResponse?.data?.items?.forEach?.(tabLink => {
@@ -177,6 +190,7 @@ export class UiTemplateTabListComponent implements OnInit, OnChanges, OnDestroy
this.experimentNavigationGroups = expNavGroups;
this.unknownGroups = unknownGroups;
this.allGroups = allGroups;
+ this.isLoading = false;
}
private async updateTemplateTab(tabLink: ApiLink) {
@@ -288,7 +302,7 @@ export class UiTemplateTabListComponent implements OnInit, OnChanges, OnDestroy
return;
}
if (group.tabs.some(t => t.href === tabLink.href)) {
- group.tabs = group.tabs.filter(t => t.href !== t.href);
+ group.tabs = group.tabs.filter(t => t.href !== tabLink.href);
}
if (group.tabs.length === 0) {
hasEmptyGroups = true;
@@ -303,7 +317,7 @@ export class UiTemplateTabListComponent implements OnInit, OnChanges, OnDestroy
let hasEmptyGroups = false;
this.allGroups.forEach(group => {
if (group.tabs.some(t => t.href === tabLink.href)) {
- group.tabs = group.tabs.filter(t => t.href !== t.href);
+ group.tabs = group.tabs.filter(t => t.href !== tabLink.href);
}
if (group.tabs.length === 0) {
hasEmptyGroups = true;
@@ -331,4 +345,15 @@ export class UiTemplateTabListComponent implements OnInit, OnChanges, OnDestroy
}
}
+ async createNewTab() {
+ if (this.createTabLink == null) {
+ return;
+ }
+ if (!this.isValid || !this.newTabData) {
+ return;
+ }
+
+ this.registry.submitByApiLink(this.createTabLink, this.newTabData);
+ }
+
}
diff --git a/src/app/components-small/ui-template-tab/ui-template-tab.component.html b/src/app/components-small/ui-template-tab/ui-template-tab.component.html
index 28e9091..1098ca0 100644
--- a/src/app/components-small/ui-template-tab/ui-template-tab.component.html
+++ b/src/app/components-small/ui-template-tab/ui-template-tab.component.html
@@ -1,13 +1,19 @@
-Description: {{tabData?.description ? '' : '–'}}
-
-@if(!isEditing && !(tabData?.groupKey ?? false) && tabFilterData) {
+@if (!isEditing) {
+ Description: {{tabData?.description ? '' : '–'}}
+
+}
+@if (!isEditing && !(tabData?.groupKey ?? false) && tabFilterData) {
Plugin filter:
}
-
+@if (isEditing) {
+
+
+}
diff --git a/src/app/components-small/ui-template-tab/ui-template-tab.component.sass b/src/app/components-small/ui-template-tab/ui-template-tab.component.sass
index d493905..1db4b10 100644
--- a/src/app/components-small/ui-template-tab/ui-template-tab.component.sass
+++ b/src/app/components-small/ui-template-tab/ui-template-tab.component.sass
@@ -4,3 +4,9 @@
justify-content: flex-end
width: 100%
gap: 0.5rem
+ margin-block-end: 0.5rem
+
+.header-buttons
+ display: flex
+ align-items: center
+ gap: 0.5rem
diff --git a/src/app/components-small/ui-template-tab/ui-template-tab.component.ts b/src/app/components-small/ui-template-tab/ui-template-tab.component.ts
index ee8b7ba..7339644 100644
--- a/src/app/components-small/ui-template-tab/ui-template-tab.component.ts
+++ b/src/app/components-small/ui-template-tab/ui-template-tab.component.ts
@@ -1,8 +1,11 @@
-import { Component, Input, OnChanges, OnDestroy, OnInit, SimpleChanges } from '@angular/core';
+import { Component, Input, OnChanges, OnDestroy, OnInit, SimpleChanges, ViewChild } from '@angular/core';
+import { MatDialog } from '@angular/material/dialog';
import { Subscription } from 'rxjs';
+import { DeleteDialog } from 'src/app/dialogs/delete-dialog/delete-dialog.dialog';
import { ApiLink } from 'src/app/services/api-data-types';
import { PluginRegistryBaseService } from 'src/app/services/registry.service';
import { TemplateTabApiObject } from 'src/app/services/templates.service';
+import { UiTemplateTabFormComponent } from '../ui-template-tab-form/ui-template-tab-form.component';
@Component({
selector: 'qhana-ui-template-tab',
@@ -12,17 +15,24 @@ import { TemplateTabApiObject } from 'src/app/services/templates.service';
export class UiTemplateTabComponent implements OnChanges, OnInit, OnDestroy {
@Input() tabLink: ApiLink | null = null;
+ @Input() useCache: boolean = false;
+
+ @ViewChild(UiTemplateTabFormComponent) tabFormChild: UiTemplateTabFormComponent | null = null;
tabData: TemplateTabApiObject | null = null;
tabFilterData: any = null;
- templateUpdateLink: ApiLink | null = null;
- templateDeleteLink: ApiLink | null = null;
+ tabUpdateLink: ApiLink | null = null;
+ tabDeleteLink: ApiLink | null = null;
isEditing: boolean = false;
+ isValid: boolean = false;
+ isDirty: boolean = false;
+ newData: any = null;
+
private updateSubscription: Subscription | null = null;
- constructor(private registry: PluginRegistryBaseService) { }
+ constructor(private registry: PluginRegistryBaseService, private dialog: MatDialog) { }
ngOnInit(): void {
this.updateSubscription = this.registry.apiObjectSubject.subscribe((apiObject) => {
@@ -30,7 +40,16 @@ export class UiTemplateTabComponent implements OnChanges, OnInit, OnDestroy {
return;
}
if (apiObject.self.resourceType === "ui-template-tab") {
- this.tabData = apiObject as TemplateTabApiObject;
+ const tab = apiObject as TemplateTabApiObject;
+ this.tabData = tab;
+
+ if (tab.filterString) {
+ try {
+ this.tabFilterData = JSON.parse(tab.filterString);
+ } catch {
+ // Ignore error
+ }
+ }
}
});
}
@@ -40,7 +59,9 @@ export class UiTemplateTabComponent implements OnChanges, OnInit, OnDestroy {
}
ngOnChanges(changes: SimpleChanges): void {
- this.loadTemplateTab();
+ if (changes.tabLink != null) {
+ this.loadTemplateTab();
+ }
}
private async loadTemplateTab() {
@@ -48,7 +69,7 @@ export class UiTemplateTabComponent implements OnChanges, OnInit, OnDestroy {
this.tabData = null;
return;
}
- const tabResponse = await this.registry.getByApiLink(this.tabLink, null, true);
+ const tabResponse = await this.registry.getByApiLink(this.tabLink, null, !this.useCache);
this.tabData = tabResponse?.data ?? null;
if (tabResponse?.data?.filterString) {
@@ -59,7 +80,33 @@ export class UiTemplateTabComponent implements OnChanges, OnInit, OnDestroy {
}
}
- this.templateUpdateLink = tabResponse?.links?.find?.(link => link.rel.some(rel => rel === "update") && link.resourceType == "ui-template-tab") ?? null;
- this.templateDeleteLink = tabResponse?.links?.find?.(link => link.rel.some(rel => rel === "delete") && link.resourceType == "ui-template-tab") ?? null;
+ this.tabUpdateLink = tabResponse?.links?.find?.(link => link.rel.some(rel => rel === "update") && link.resourceType == "ui-template-tab") ?? null;
+ this.tabDeleteLink = tabResponse?.links?.find?.(link => link.rel.some(rel => rel === "delete") && link.resourceType == "ui-template-tab") ?? null;
+ }
+
+ updateTab() {
+ if (this.tabUpdateLink == null) {
+ return;
+ }
+ if (!this.isValid || !this.newData) {
+ return;
+ }
+
+ this.registry.submitByApiLink(this.tabUpdateLink, this.newData);
+ }
+
+ async deleteTab() {
+ if (this.tabDeleteLink == null) {
+ return;
+ }
+
+ const dialogRef = this.dialog.open(DeleteDialog, {
+ data: this.tabLink,
+ });
+
+ const doDelete = await dialogRef.afterClosed().toPromise();
+ if (doDelete) {
+ this.registry.submitByApiLink(this.tabDeleteLink);
+ }
}
}