From 7cedd92bb60704c3a206b55616af8e527b3f363f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabian=20B=C3=BChler?= Date: Fri, 26 Jul 2024 10:13:28 +0200 Subject: [PATCH 01/14] Start dedicated ui templates page (WIP) --- src/app/app-routing.module.ts | 3 + src/app/app.module.ts | 2 + .../components/navbar/navbar.component.html | 11 +++ .../ui-templates-page.component.html | 19 ++++ .../ui-templates-page.component.sass | 67 ++++++++++++++ .../ui-templates-page.component.ts | 88 +++++++++++++++++++ 6 files changed, 190 insertions(+) create mode 100644 src/app/components/ui-templates-page/ui-templates-page.component.html create mode 100644 src/app/components/ui-templates-page/ui-templates-page.component.sass create mode 100644 src/app/components/ui-templates-page/ui-templates-page.component.ts diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts index ed98425..e92a9d5 100644 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -25,6 +25,7 @@ import { ExperimentsPageComponent } from './components/experiments-page/experime 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'; +import { UiTemplatesPageComponent } from './components/ui-templates-page/ui-templates-page.component'; const NUMBER_REGEX = /^[0-9]+$/; @@ -118,6 +119,8 @@ const extraTabsMatcher: UrlMatcher = (segments: UrlSegment[], group, route): Url const routes: Routes = [ { path: '', component: ExperimentsPageComponent }, { path: 'settings', component: SettingsPageComponent }, + { path: 'templates', component: UiTemplatesPageComponent }, + { path: 'templates/:templateId', component: UiTemplatesPageComponent }, { path: 'experiments', component: ExperimentsPageComponent }, { path: 'experiments/:experimentId', redirectTo: "info" }, { path: 'experiments/:experimentId/info', component: ExperimentComponent }, diff --git a/src/app/app.module.ts b/src/app/app.module.ts index ff3c9a4..1e64aa9 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -78,6 +78,7 @@ import { TabGroupListComponent } from './components/tab-group-list/tab-group-lis import { TimelineStepNavComponent } from './components/timeline-step-nav/timeline-step-nav.component'; import { TimelineStepComponent } from './components/timeline-step/timeline-step.component'; import { TimelineSubstepsComponent } from './components/timeline-substeps/timeline-substeps.component'; +import { UiTemplatesPageComponent } from './components/ui-templates-page/ui-templates-page.component'; import { ChangeUiTemplateDialog } from './dialogs/change-ui-template/change-ui-template.dialog'; import { ChooseDataDialog } from './dialogs/choose-data/choose-data.dialog'; import { ChoosePluginDialog } from './dialogs/choose-plugin/choose-plugin.dialog'; @@ -132,6 +133,7 @@ import { MarkdownHelpDialog } from './dialogs/markdown-help/markdown-help.dialog PluginFilterEditorComponent, TabGroupListComponent, ChooseTemplateDialog, + UiTemplatesPageComponent, ], imports: [ BrowserModule, diff --git a/src/app/components/navbar/navbar.component.html b/src/app/components/navbar/navbar.component.html index b4680ce..2f6b376 100644 --- a/src/app/components/navbar/navbar.component.html +++ b/src/app/components/navbar/navbar.component.html @@ -4,6 +4,17 @@ {{title}} + + + Experiments + + + UI Templates + + + diff --git a/src/app/components/ui-templates-page/ui-templates-page.component.html b/src/app/components/ui-templates-page/ui-templates-page.component.html new file mode 100644 index 0000000..5a2541f --- /dev/null +++ b/src/app/components/ui-templates-page/ui-templates-page.component.html @@ -0,0 +1,19 @@ +
+ +
+
+ +
+
+
+
diff --git a/src/app/components/ui-templates-page/ui-templates-page.component.sass b/src/app/components/ui-templates-page/ui-templates-page.component.sass new file mode 100644 index 0000000..8498478 --- /dev/null +++ b/src/app/components/ui-templates-page/ui-templates-page.component.sass @@ -0,0 +1,67 @@ +.templates-content + display: grid + grid-template-areas: "sidebar main spacer" + grid-template-columns: minmax(min-content, calc( (100vw - 60rem)/2 )) minmax(20rem, 1fr) minmax(0.5rem, calc( (100vw - 60rem)/2 )) + grid-template-rows: 1fr + +.sidebar + position: sticky + top: 0px + grid-area: sidebar + margin-inline-end: 0.5rem + min-height: 100vh + display: flex + flex-direction: column + min-width: 15rem + width: 25vw + max-width: 20rem + padding-block-end: 4rem + background: var(--background) + border-inline-end: 1px solid var(--border-color) + +@media (prefers-color-scheme: dark) + .sidebar + border-right: none + +.new-template-button + margin-block: 1rem + margin-inline: 1rem + +.sidebar-header + display: flex + gap: 0.3rem + align-content: center + justify-content: space-between + padding-inline: 1rem + +.w-100 + width: 100% + +.full-width + width: calc(100% - 2rem) + margin-inline: 1rem + +.content-container + grid-area: main + +.content-container .main-content + max-width: 60rem + margin-inline: auto + +.title + display: flex + justify-content: space-between + align-items: center + +.spacer + flex-grow: 1 + +.card-list + margin-inline: -16px + +.active-list-item + background-color: var(--primary-lighter) + color: var(--text-primary) + +.active-list-item:focus + background-color: var(--primary-lighter) diff --git a/src/app/components/ui-templates-page/ui-templates-page.component.ts b/src/app/components/ui-templates-page/ui-templates-page.component.ts new file mode 100644 index 0000000..a68078c --- /dev/null +++ b/src/app/components/ui-templates-page/ui-templates-page.component.ts @@ -0,0 +1,88 @@ +import { Component, OnDestroy, OnInit } from '@angular/core'; +import { MatDialog } from '@angular/material/dialog'; +import { ActivatedRoute, Router } from '@angular/router'; +import { Subscription } from 'rxjs'; +import { ChangeUiTemplateDialog } from 'src/app/dialogs/change-ui-template/change-ui-template.dialog'; +import { ApiLink, PageApiObject } from 'src/app/services/api-data-types'; +import { PluginRegistryBaseService } from 'src/app/services/registry.service'; +import { TemplateApiObject, TemplatesService } from 'src/app/services/templates.service'; + +@Component({ + selector: 'qhana-ui-templates-page', + templateUrl: './ui-templates-page.component.html', + styleUrl: './ui-templates-page.component.sass' +}) +export class UiTemplatesPageComponent implements OnInit, OnDestroy { + selectedTemplate: ApiLink | null = null; + + highlightedTemplates: Set = new Set(); + + templateId: string | null = null; + private routeParamSubscription: Subscription | null = null; + + + constructor(private route: ActivatedRoute, private router: Router, private registry: PluginRegistryBaseService, private templates: TemplatesService, private dialog: MatDialog) { } + + + ngOnInit(): void { + this.routeParamSubscription = this.route.paramMap.subscribe(params => { + let templateId = params.get('templateId'); + if (templateId != null) { + this.highlightedTemplates = new Set([templateId]); + } else { + this.highlightedTemplates.clear(); + } + console.log(this.highlightedTemplates, templateId) + this.loadActiveTemplateFromId(templateId); + }); + + } + + ngOnDestroy(): void { + this.routeParamSubscription?.unsubscribe(); + } + + selectTemplate(templateLink: ApiLink | null) { + console.log(templateLink) + if (templateLink == null) { + this.router.navigate(["/templates"]); + return; + } + this.selectedTemplate = templateLink; + this.router.navigate(["/templates", templateLink.resourceKey?.uiTemplateId ?? null]); + } + + private async loadActiveTemplateFromId(newTemplateId: string | null) { + if (newTemplateId == null) { + this.selectedTemplate = null; + this.highlightedTemplates = new Set(); + return; + } + const activeTemplateId = this.selectedTemplate?.resourceKey?.uiTemplateId ?? null; + if (newTemplateId === activeTemplateId) { + // nothing to do, link already loaded + return; + } + // need to fetch template api link from plugin registry + const query = new URLSearchParams(); + query.set("template-id", newTemplateId); + const templatePage = await this.registry.getByRel([["ui-template", "collection"]], query, true); + if (templatePage?.data.collectionSize === 1) { + // only expect one template since IDs are unique + this.selectedTemplate = templatePage.data.items[0]; + } else { + console.warn(`Template API returned an ambiguous response for template id ${newTemplateId}`, templatePage); + } + } + + + async createTemplate() { + const dialogRef = this.dialog.open(ChangeUiTemplateDialog, { data: { template: null }, minWidth: "20rem", maxWidth: "40rem", width: "60%" }); + const templateData: TemplateApiObject = await dialogRef.afterClosed().toPromise(); + + if (!templateData) { + return; + } + this.templates.addTemplate(templateData); + } +} From eccfd86ad05f1c326041a313670fccf00680a27b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabian=20B=C3=BChler?= Date: Wed, 7 Aug 2024 13:36:43 +0200 Subject: [PATCH 02/14] Fix markdown editor not updating correctly --- src/app/components/markdown/markdown.component.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/components/markdown/markdown.component.ts b/src/app/components/markdown/markdown.component.ts index 1571c5b..78c0e40 100644 --- a/src/app/components/markdown/markdown.component.ts +++ b/src/app/components/markdown/markdown.component.ts @@ -358,14 +358,14 @@ export class MarkdownComponent implements OnChanges { // }); } if (changes.editable != null) { - this.editor?.action(forceUpdate); + this.editor?.action(forceUpdate()); } } resetEditMode() { if (this.editable) { this.showAsPreview = false; - this.editor?.action(forceUpdate); + this.editor?.action(forceUpdate()); } } From 85746b15710eb84029d5a0e57ffcc1362a0d5492 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabian=20B=C3=BChler?= Date: Wed, 7 Aug 2024 15:08:19 +0200 Subject: [PATCH 03/14] Add edit and delete functions for template details in templates page --- src/app/app.module.ts | 2 + .../ui-template/ui-template.component.html | 42 +++++ .../ui-template/ui-template.component.sass | 23 +++ .../ui-template/ui-template.component.ts | 144 ++++++++++++++++++ .../ui-templates-page.component.html | 1 + .../ui-templates-page.component.sass | 1 + .../ui-templates-page.component.ts | 27 +++- 7 files changed, 238 insertions(+), 2 deletions(-) create mode 100644 src/app/components-small/ui-template/ui-template.component.html create mode 100644 src/app/components-small/ui-template/ui-template.component.sass create mode 100644 src/app/components-small/ui-template/ui-template.component.ts diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 1e64aa9..1bb6f06 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -87,6 +87,7 @@ import { CreateExperimentDialog } from './dialogs/create-experiment/create-exper import { DeleteDialog } from './dialogs/delete-dialog/delete-dialog.dialog'; import { ExportExperimentDialog } from './dialogs/export-experiment/export-experiment.dialog'; import { MarkdownHelpDialog } from './dialogs/markdown-help/markdown-help.dialog'; +import { UiTemplateComponent } from "./components-small/ui-template/ui-template.component"; @NgModule({ declarations: [ @@ -134,6 +135,7 @@ import { MarkdownHelpDialog } from './dialogs/markdown-help/markdown-help.dialog TabGroupListComponent, ChooseTemplateDialog, UiTemplatesPageComponent, + UiTemplateComponent, ], imports: [ BrowserModule, diff --git a/src/app/components-small/ui-template/ui-template.component.html b/src/app/components-small/ui-template/ui-template.component.html new file mode 100644 index 0000000..25c2021 --- /dev/null +++ b/src/app/components-small/ui-template/ui-template.component.html @@ -0,0 +1,42 @@ + + +
+ UI Template: {{templateData.name}} +
+ + UI Template Name: + + +
+ + + +
+
+ + + {{tag}} + + + Tags + + @for (tag of currentTags; track tag) { + + {{tag}} + + + } + + + + + +
diff --git a/src/app/components-small/ui-template/ui-template.component.sass b/src/app/components-small/ui-template/ui-template.component.sass new file mode 100644 index 0000000..e4ce0ec --- /dev/null +++ b/src/app/components-small/ui-template/ui-template.component.sass @@ -0,0 +1,23 @@ +.header + display: flex + align-items: center + justify-content: space-between + gap: 1rem + +.header-buttons + display: flex + align-items: center + gap: 0.5rem + +.content, .content:last-child + padding: 0 + margin-block-start: 0.5rem + +.name-field + flex-grow: 1 + +.tags + margin-block-end: 0.5rem + +.tags-field + width: 100% diff --git a/src/app/components-small/ui-template/ui-template.component.ts b/src/app/components-small/ui-template/ui-template.component.ts new file mode 100644 index 0000000..fcf048a --- /dev/null +++ b/src/app/components-small/ui-template/ui-template.component.ts @@ -0,0 +1,144 @@ +import { Component, Input, OnChanges, OnDestroy, OnInit, SimpleChanges } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { ApiLink, ChangedApiObject } from 'src/app/services/api-data-types'; +import { TemplateApiObject } from 'src/app/services/templates.service'; +import { PluginRegistryBaseService } from 'src/app/services/registry.service'; +import { MatChipInputEvent } from '@angular/material/chips'; +import { Subscription } from 'rxjs'; +import { MatDialog } from '@angular/material/dialog'; +import { DeleteDialog } from 'src/app/dialogs/delete-dialog/delete-dialog.dialog'; + +@Component({ + selector: 'qhana-ui-template', + templateUrl: './ui-template.component.html', + styleUrl: './ui-template.component.sass' +}) +export class UiTemplateComponent implements OnChanges, OnInit, OnDestroy { + + @Input() templateLink: ApiLink | null = null; + + + templateData: TemplateApiObject | null = null; + templateUpdateLink: ApiLink | null = null; + templateDeleteLink: ApiLink | null = null; + + isEditing: boolean = false; + + currentName: string | null = null; + currentTags: string[] | null = null; + currentDescription: string | null = null; + + private tagsDirty: boolean = false; + private updateSubscription: Subscription | null = null; + + constructor(private registry: PluginRegistryBaseService, private dialog: MatDialog) { } + + ngOnInit(): void { + this.updateSubscription = this.registry.apiObjectSubject.subscribe((apiObject) => { + if (apiObject.self.href !== this.templateLink?.href) { + return; + } + if (apiObject.self.resourceType === "ui-template") { + this.templateData = apiObject as TemplateApiObject; + if (this.isEditing) { + this.currentTags = [...(apiObject as TemplateApiObject).tags]; + this.tagsDirty = false; + } + } + }); + } + + ngOnDestroy(): void { + this.updateSubscription?.unsubscribe(); + } + + ngOnChanges(changes: SimpleChanges): void { + this.loadTemplate(); + } + + private async loadTemplate() { + if (this.templateLink == null) { + this.templateData = null; + return; + } + const templateResponse = await this.registry.getByApiLink(this.templateLink); + this.templateData = templateResponse?.data ?? null; + + this.templateUpdateLink = templateResponse?.links?.find(link => link.rel.some(rel => rel === "update") && link.resourceType == "ui-template") ?? null; + this.templateDeleteLink = templateResponse?.links?.find(link => link.rel.some(rel => rel === "delete") && link.resourceType == "ui-template") ?? null; + } + + get isDirty() { + if (!this.isEditing) { + return false; + } + if (this.currentName !== this.templateData?.name) { + return true; + } + if (this.currentDescription !== this.templateData?.description) { + return true; + } + if (this.tagsDirty) { + return true; + } + return false; + } + + toggleEdit() { + if (this.templateData == null || this.templateUpdateLink == null) { + this.isEditing = false; + } else { + this.isEditing = !this.isEditing; + } + if (this.isEditing) { + this.currentName = this.templateData?.name ?? null; + this.currentTags = [...(this.templateData?.tags ?? [])]; + this.currentDescription = this.templateData?.description ?? null; + this.tagsDirty = false; + } else { + this.currentName = null; + this.currentTags = null; + this.currentDescription = null; + this.tagsDirty = false; + } + } + + removeTag(tag: string) { + this.currentTags = this.currentTags?.filter(t => t !== tag) ?? null; + this.tagsDirty = true; + } + + addTag(event: MatChipInputEvent) { + const tag = event.value; + this.currentTags?.push(tag); + this.tagsDirty = true; + } + + async updateTemplate() { + if (!this.isDirty || this.templateUpdateLink == null) { + return; + } + + this.registry.submitByApiLink(this.templateUpdateLink, { + name: this.currentName, + description: this.currentDescription, + tags: this.currentTags, + }); + } + + async deleteTemplate() { + if (this.templateDeleteLink == null) { + return; + } + + const dialogRef = this.dialog.open(DeleteDialog, { + data: this.templateLink, + }); + + const doDelete = await dialogRef.afterClosed().toPromise(); + if (doDelete) { + this.registry.submitByApiLink(this.templateDeleteLink); + } + } + +} diff --git a/src/app/components/ui-templates-page/ui-templates-page.component.html b/src/app/components/ui-templates-page/ui-templates-page.component.html index 5a2541f..bb0f343 100644 --- a/src/app/components/ui-templates-page/ui-templates-page.component.html +++ b/src/app/components/ui-templates-page/ui-templates-page.component.html @@ -13,6 +13,7 @@
+
diff --git a/src/app/components/ui-templates-page/ui-templates-page.component.sass b/src/app/components/ui-templates-page/ui-templates-page.component.sass index 8498478..d7f5fe7 100644 --- a/src/app/components/ui-templates-page/ui-templates-page.component.sass +++ b/src/app/components/ui-templates-page/ui-templates-page.component.sass @@ -47,6 +47,7 @@ .content-container .main-content max-width: 60rem margin-inline: auto + padding-block-start: 2rem .title display: flex diff --git a/src/app/components/ui-templates-page/ui-templates-page.component.ts b/src/app/components/ui-templates-page/ui-templates-page.component.ts index a68078c..cca6c4a 100644 --- a/src/app/components/ui-templates-page/ui-templates-page.component.ts +++ b/src/app/components/ui-templates-page/ui-templates-page.component.ts @@ -18,7 +18,10 @@ export class UiTemplatesPageComponent implements OnInit, OnDestroy { highlightedTemplates: Set = new Set(); templateId: string | null = null; + private routeParamSubscription: Subscription | null = null; + private templateCreatedSubscription: Subscription | null = null; + private templateDeletedSubscription: Subscription | null = null; constructor(private route: ActivatedRoute, private router: Router, private registry: PluginRegistryBaseService, private templates: TemplatesService, private dialog: MatDialog) { } @@ -32,18 +35,37 @@ export class UiTemplatesPageComponent implements OnInit, OnDestroy { } else { this.highlightedTemplates.clear(); } - console.log(this.highlightedTemplates, templateId) + this.templateId = templateId; this.loadActiveTemplateFromId(templateId); }); + this.templateCreatedSubscription = this.registry.newApiObjectSubject.subscribe(created => { + if (created.new.resourceType === "ui-template") { + // new template created + if (this.templateId == null) { + this.selectTemplate(created.new); + } + } + }); + + this.templateDeletedSubscription = this.registry.deletedApiObjectSubject.subscribe(deleted => { + if (this.templateId == null) { + return; + } + if (deleted.deleted.resourceType === "ui-template" && deleted.deleted.resourceKey?.uiTemplateId === this.templateId) { + // current template was deleted, navigate to overview + this.selectTemplate(null); + } + }); } ngOnDestroy(): void { this.routeParamSubscription?.unsubscribe(); + this.templateDeletedSubscription?.unsubscribe(); + this.templateCreatedSubscription?.unsubscribe(); } selectTemplate(templateLink: ApiLink | null) { - console.log(templateLink) if (templateLink == null) { this.router.navigate(["/templates"]); return; @@ -72,6 +94,7 @@ export class UiTemplatesPageComponent implements OnInit, OnDestroy { this.selectedTemplate = templatePage.data.items[0]; } else { console.warn(`Template API returned an ambiguous response for template id ${newTemplateId}`, templatePage); + this.selectTemplate(null); } } From e6f964088465bf8d66a373f135dbd5b16a8c3a44 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabian=20B=C3=BChler?= Date: Thu, 8 Aug 2024 16:19:27 +0200 Subject: [PATCH 04/14] Show template tab list in templates page (WIP) Tab details are still missing. --- src/app/app.module.ts | 2 + .../ui-template-tab-list.component.html | 127 +++++++ .../ui-template-tab-list.component.sass | 6 + .../ui-template-tab-list.component.ts | 334 ++++++++++++++++++ .../ui-template/ui-template.component.ts | 9 +- .../ui-templates-page.component.html | 1 + .../ui-templates-page.component.sass | 12 +- 7 files changed, 477 insertions(+), 14 deletions(-) create mode 100644 src/app/components-small/ui-template-tab-list/ui-template-tab-list.component.html create mode 100644 src/app/components-small/ui-template-tab-list/ui-template-tab-list.component.sass create mode 100644 src/app/components-small/ui-template-tab-list/ui-template-tab-list.component.ts diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 1bb6f06..05839cc 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -88,6 +88,7 @@ import { DeleteDialog } from './dialogs/delete-dialog/delete-dialog.dialog'; import { ExportExperimentDialog } from './dialogs/export-experiment/export-experiment.dialog'; import { MarkdownHelpDialog } from './dialogs/markdown-help/markdown-help.dialog'; import { UiTemplateComponent } from "./components-small/ui-template/ui-template.component"; +import { UiTemplateTabListComponent } from './components-small/ui-template-tab-list/ui-template-tab-list.component'; @NgModule({ declarations: [ @@ -136,6 +137,7 @@ import { UiTemplateComponent } from "./components-small/ui-template/ui-template. ChooseTemplateDialog, UiTemplatesPageComponent, UiTemplateComponent, + UiTemplateTabListComponent, ], imports: [ BrowserModule, 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 new file mode 100644 index 0000000..182269f --- /dev/null +++ b/src/app/components-small/ui-template-tab-list/ui-template-tab-list.component.html @@ -0,0 +1,127 @@ +
+

Navigation Tab Groups

+ + @for (tab of navigationGroup.tabs; track tab.href) { + + + + {{hrefToTab.get(tab.href)?.icon}} + {{tab.name}} + more_vert + + (Group: {{navigationGroup.groupKey}}.{{hrefToTab.get(tab.href)?.groupKey}}) + +

TODO: CONTENT

+
+ } +
+ + @for (group of navigationGroups; track group.groupKey) { +

+ {{group.icon}} + {{group.name}} + (Group: {{group.groupKey}}) +

+ + @for (tab of group.tabs; track tab.href) { + + + + {{hrefToTab.get(tab.href)?.icon}} + {{tab.name}} + more_vert + + (Group: {{group.groupKey}}.{{hrefToTab.get(tab.href)?.groupKey}}) + +

TODO: CONTENT

+
+ } +
+ } +
    +
  • {{navigationGroup.name}}
  • +
  • {{group.name}} - {{group.groupKey}}
  • +
+
+
+

Experiment Workspace Tabs

+ + @for (tab of workspaceGroup.tabs; track tab.href) { + + + + {{hrefToTab.get(tab.href)?.icon}} + {{tab.name}} + + +

TODO: CONTENT

+
+ } +
+
+
+

Experiment Navigation Tab Groups

+ + @for (tab of experimentNavigationGroup.tabs; track tab.href) { + + + + {{hrefToTab.get(tab.href)?.icon}} + {{tab.name}} + more_vert + + (Group: {{experimentNavigationGroup.groupKey}}.{{hrefToTab.get(tab.href)?.groupKey}}) + +

TODO: CONTENT

+
+ } +
+ + @for (group of experimentNavigationGroups; track group.groupKey) { +

+ {{group.icon}} + {{group.name}} + (Group: {{group.groupKey}}) +

+ + @for (tab of group.tabs; track tab.href) { + + + + {{hrefToTab.get(tab.href)?.icon}} + {{tab.name}} + more_vert + + (Group: {{group.groupKey}}.{{hrefToTab.get(tab.href)?.groupKey}}) + +

TODO: CONTENT

+
+ } +
+ } +
+
+

Unknown Tab Groups

+ @for (group of unknownGroups; track group.groupKey) { +

+ {{group.icon}} + {{group.name}} + (Group: {{group.groupKey}}) +

+ + @for (tab of group.tabs; track tab.href) { + + + + {{hrefToTab.get(tab.href)?.icon}} + {{tab.name}} + more_vert + + (Group: {{group.groupKey}}.{{hrefToTab.get(tab.href)?.groupKey}}) + +

TODO: CONTENT

+
+ } +
+ } +
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 new file mode 100644 index 0000000..8890269 --- /dev/null +++ b/src/app/components-small/ui-template-tab-list/ui-template-tab-list.component.sass @@ -0,0 +1,6 @@ +.tab-title + display: inline-flex + gap: 0.5rem + +.group-title-extra + color: var(--text-washed) 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 new file mode 100644 index 0000000..ecbcc38 --- /dev/null +++ b/src/app/components-small/ui-template-tab-list/ui-template-tab-list.component.ts @@ -0,0 +1,334 @@ +import { Component, Input, OnChanges, OnDestroy, OnInit, SimpleChanges } from '@angular/core'; +import { TemplateApiObject, TemplateTabApiObject } from 'src/app/services/templates.service'; +import { PluginRegistryBaseService } from 'src/app/services/registry.service'; +import { MatDialog } from '@angular/material/dialog'; +import { Subscription } from 'rxjs'; +import { ApiLink, CollectionApiObject } from 'src/app/services/api-data-types'; + +interface NavTabGroup { + groupKey: string; + name: string; + icon?: string | null; + tabs: ApiLink[]; +} + + +@Component({ + selector: 'qhana-ui-template-tab-list', + templateUrl: './ui-template-tab-list.component.html', + styleUrl: './ui-template-tab-list.component.sass' +}) +export class UiTemplateTabListComponent implements OnInit, OnChanges, OnDestroy { + + @Input() templateLink: ApiLink | null = null; + + navigationGroup: NavTabGroup | null = null; + workspaceGroup: NavTabGroup | null = null; + experimentNavigationGroup: NavTabGroup | null = null; + navigationGroups: NavTabGroup[] = []; + experimentNavigationGroups: NavTabGroup[] = []; + unknownGroups: NavTabGroup[] = []; + + private allGroups: NavTabGroup[] = []; + + hrefToTab: Map = new Map(); + + private newTabsSubscription: Subscription | null = null; + private changedTabsSubscription: Subscription | null = null; + private deletedTabsSubscription: Subscription | null = null; + + constructor(private registry: PluginRegistryBaseService, private dialog: MatDialog) { } + + ngOnInit(): void { + this.newTabsSubscription = this.registry.newApiObjectSubject.subscribe((apiObject) => { + if (apiObject.new.resourceType !== "ui-template-tab") { + return; + } + if (apiObject.new.resourceKey?.uiTemplateId !== this.templateLink?.resourceKey?.uiTemplateId) { + return; + } + this.updateTemplateTab(apiObject.new); + }); + this.changedTabsSubscription = this.registry.changedApiObjectSubject.subscribe((apiObject) => { + if (apiObject.changed.resourceType !== "ui-template-tab") { + return; + } + if (apiObject.changed.resourceKey?.uiTemplateId !== this.templateLink?.resourceKey?.uiTemplateId) { + return; + } + this.updateTemplateTab(apiObject.changed); + }); + this.deletedTabsSubscription = this.registry.deletedApiObjectSubject.subscribe((apiObject) => { + if (apiObject.deleted.resourceType !== "ui-template-tab") { + return; + } + if (apiObject.deleted.resourceKey?.uiTemplateId !== this.templateLink?.resourceKey?.uiTemplateId) { + return; + } + this.removeTemplateTab(apiObject.deleted); + }); + } + + ngOnDestroy(): void { + this.newTabsSubscription?.unsubscribe(); + this.changedTabsSubscription?.unsubscribe(); + this.deletedTabsSubscription?.unsubscribe(); + } + + ngOnChanges(changes: SimpleChanges): void { + this.loadTemplateGroups(); + } + + private async loadTemplateGroups() { + if (this.templateLink == null) { + this.hrefToTab = new Map(); + this.workspaceGroup = null; + this.navigationGroup = null; + this.experimentNavigationGroup = null; + this.navigationGroups = []; + this.experimentNavigationGroups = []; + this.unknownGroups = []; + return; + } + const templateResponse = await this.registry.getByApiLink(this.templateLink, null, false); + if (templateResponse == null) { + this.hrefToTab = new Map(); + this.workspaceGroup = null; + this.navigationGroup = null; + this.experimentNavigationGroup = null; + this.navigationGroups = []; + this.experimentNavigationGroups = []; + this.unknownGroups = []; + return; + } + + const hrefToTab = new Map(); + + const tabPromises: Promise[] = []; + + let navGroup: NavTabGroup | null = null; + let workspace: NavTabGroup | null = null; + let expNavGroup: NavTabGroup | null = null; + const navGroups: NavTabGroup[] = []; + const expNavGroups: NavTabGroup[] = []; + const unknownGroups: NavTabGroup[] = []; + + const allGroups: NavTabGroup[] = []; + + const groupIcons = new Map(); + + const groupPromises = templateResponse.data.groups.map(async (group) => { + const groupResponse = await this.registry.getByApiLink(group); + + // fetch tab data + groupResponse?.data?.items?.forEach?.(tabLink => { + const prom = this.registry.getByApiLink(tabLink).then(tab => { + if (tab) { + hrefToTab.set(tab.data.self.href, tab.data); + if (tab.data.groupKey && tab.data.icon) { + groupIcons.set(`${tab.data.location}.${tab.data.groupKey}`, tab.data.icon); + } + } + }); + tabPromises.push(prom); + }); + + const tabGroup: NavTabGroup = { + groupKey: group.resourceKey?.["?group"] ?? "unknwon", + name: group.name ?? "UNNAMED", + tabs: groupResponse?.data?.items ?? [], + }; + + allGroups.push(tabGroup); + + if (tabGroup.groupKey === "workspace") { + workspace = tabGroup; + } else if (tabGroup.groupKey === "navigation") { + navGroup = tabGroup; + } else if (tabGroup.groupKey === "experiment-navigation") { + expNavGroup = tabGroup; + } else if (tabGroup.groupKey.startsWith("navigation")) { + navGroups.push(tabGroup); + } else if (tabGroup.groupKey.startsWith("experiment-navigation")) { + expNavGroups.push(tabGroup); + } else { + unknownGroups.push(tabGroup); + } + }); + + await Promise.allSettled(tabPromises); + await Promise.allSettled(groupPromises); + + navGroups.forEach(g => { + g.icon = groupIcons.get(g.groupKey); + }); + expNavGroups.forEach(g => { + g.icon = groupIcons.get(g.groupKey); + }); + unknownGroups.forEach(g => { + g.icon = groupIcons.get(g.groupKey); + }); + + this.hrefToTab = hrefToTab; + this.navigationGroup = navGroup; + this.workspaceGroup = workspace; + this.experimentNavigationGroup = expNavGroup; + this.navigationGroups = navGroups; + this.experimentNavigationGroups = expNavGroups; + this.unknownGroups = unknownGroups; + this.allGroups = allGroups; + } + + private async updateTemplateTab(tabLink: ApiLink) { + const tabResponse = await this.registry.getByApiLink(tabLink, null, false); + if (tabResponse == null) { + return; // cannot update tab + } + + const groupLink = tabResponse.links.find(link => { + return link.resourceType === "ui-template-tab" && link.rel.some(r => r === "collection") && link.resourceKey?.["?group"]; + }) ?? null; + + const tab = tabResponse.data; + + this.hrefToTab.set(tab.self.href, tab); + + if (tab.location === "workspace") { + this.workspaceGroup = this.updateTabInGroup(this.workspaceGroup, tabLink, groupLink); + } else if (tab.location === "navigation") { + this.navigationGroup = this.updateTabInGroup(this.navigationGroup, tabLink, groupLink); + } else if (tab.location === "experiment-navigation") { + this.experimentNavigationGroup = this.updateTabInGroup(this.experimentNavigationGroup, tabLink, groupLink); + } else if (tab.location.startsWith("navigation")) { + if (this.navigationGroups.some(g => g.groupKey === tab.location)) { + this.navigationGroups.map(g => { + if (g.groupKey === tab.location) { + return this.updateTabInGroup(g, tabLink, groupLink); + } + return g; + }) + } else { + this.navigationGroups.push(this.updateTabInGroup(null, tabLink, groupLink)) + } + } else if (tab.location.startsWith("experiment-navigation")) { + if (this.experimentNavigationGroups.some(g => g.groupKey === tab.location)) { + this.experimentNavigationGroups.map(g => { + if (g.groupKey === tab.location) { + return this.updateTabInGroup(g, tabLink, groupLink); + } + return g; + }) + } else { + this.experimentNavigationGroups.push(this.updateTabInGroup(null, tabLink, groupLink)) + } + } else { + if (this.unknownGroups.some(g => g.groupKey === tab.location)) { + this.unknownGroups.map(g => { + if (g.groupKey === tab.location) { + return this.updateTabInGroup(g, tabLink, groupLink); + } + return g; + }) + } else { + this.unknownGroups.push(this.updateTabInGroup(null, tabLink, groupLink)) + } + } + + this.removeOldTemplateTabLinkFromOtherGroups(tabLink); + } + + private updateTabInGroup(group: NavTabGroup | null, tab: ApiLink, groupLink: ApiLink | null) { + if (group == null) { + if (groupLink != null) { + group = { + groupKey: groupLink.resourceKey?.["?group"] ?? "unknwon", + name: groupLink.name ?? "UNNAMED", + tabs: [], + } + } else { + group = { + groupKey: tab.resourceKey?.["?group"] ?? "unknwon", + name: tab.resourceKey?.["?group"] ?? "UNNAMED", + tabs: [], + } + } + group.icon = this.getGroupIcon(group.groupKey); + this.allGroups.push(group); + } + if (group.tabs.some(t => t.href === tab.href)) { + group.tabs.map(t => { + if (t.href === tab.href) { + return tab; // insert new + } + return t; // keep original + }); + } else { + group.tabs.push(tab); + } + return group; + } + + private getGroupIcon(groupLocation: string) { + let icon: string | null = null; + this.hrefToTab.forEach((tab) => { + if (tab.groupKey && groupLocation.startsWith(tab.location)) { + if (groupLocation === `${tab.location}.${tab.groupKey}`) { + icon = tab.icon; + } + } + }); + return icon; + } + + private async removeOldTemplateTabLinkFromOtherGroups(tabLink: ApiLink) { + let hasEmptyGroups = false; + this.allGroups.forEach(group => { + if (group.groupKey === tabLink.resourceKey?.["?group"]) { + // do not remove tab from the group it is currently in! + return; + } + if (group.tabs.some(t => t.href === tabLink.href)) { + group.tabs = group.tabs.filter(t => t.href !== t.href); + } + if (group.tabs.length === 0) { + hasEmptyGroups = true; + } + }); + if (hasEmptyGroups) { + this.removeEmptyGroups(); + } + } + + private async removeTemplateTab(tabLink: ApiLink) { + 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); + } + if (group.tabs.length === 0) { + hasEmptyGroups = true; + } + }); + if (hasEmptyGroups) { + this.removeEmptyGroups(); + } + this.hrefToTab.delete(tabLink.href); + } + + private removeEmptyGroups() { + this.allGroups = this.allGroups.filter(g => g.tabs.length > 0); + this.navigationGroups = this.navigationGroups.filter(g => g.tabs.length > 0); + this.experimentNavigationGroups = this.experimentNavigationGroups.filter(g => g.tabs.length > 0); + this.unknownGroups = this.unknownGroups.filter(g => g.tabs.length > 0); + if (this.navigationGroup?.tabs?.length === 0) { + this.navigationGroup = null; + } + if (this.workspaceGroup?.tabs?.length === 0) { + this.workspaceGroup = null; + } + if (this.experimentNavigationGroup?.tabs?.length === 0) { + this.experimentNavigationGroup = null; + } + } + +} diff --git a/src/app/components-small/ui-template/ui-template.component.ts b/src/app/components-small/ui-template/ui-template.component.ts index fcf048a..0d82687 100644 --- a/src/app/components-small/ui-template/ui-template.component.ts +++ b/src/app/components-small/ui-template/ui-template.component.ts @@ -1,6 +1,5 @@ import { Component, Input, OnChanges, OnDestroy, OnInit, SimpleChanges } from '@angular/core'; -import { CommonModule } from '@angular/common'; -import { ApiLink, ChangedApiObject } from 'src/app/services/api-data-types'; +import { ApiLink } from 'src/app/services/api-data-types'; import { TemplateApiObject } from 'src/app/services/templates.service'; import { PluginRegistryBaseService } from 'src/app/services/registry.service'; import { MatChipInputEvent } from '@angular/material/chips'; @@ -61,11 +60,11 @@ export class UiTemplateComponent implements OnChanges, OnInit, OnDestroy { this.templateData = null; return; } - const templateResponse = await this.registry.getByApiLink(this.templateLink); + const templateResponse = await this.registry.getByApiLink(this.templateLink, null, true); this.templateData = templateResponse?.data ?? null; - this.templateUpdateLink = templateResponse?.links?.find(link => link.rel.some(rel => rel === "update") && link.resourceType == "ui-template") ?? null; - this.templateDeleteLink = templateResponse?.links?.find(link => link.rel.some(rel => rel === "delete") && link.resourceType == "ui-template") ?? null; + this.templateUpdateLink = templateResponse?.links?.find?.(link => link.rel.some(rel => rel === "update") && link.resourceType == "ui-template") ?? null; + this.templateDeleteLink = templateResponse?.links?.find?.(link => link.rel.some(rel => rel === "delete") && link.resourceType == "ui-template") ?? null; } get isDirty() { diff --git a/src/app/components/ui-templates-page/ui-templates-page.component.html b/src/app/components/ui-templates-page/ui-templates-page.component.html index bb0f343..3d2b0ed 100644 --- a/src/app/components/ui-templates-page/ui-templates-page.component.html +++ b/src/app/components/ui-templates-page/ui-templates-page.component.html @@ -14,6 +14,7 @@
+
diff --git a/src/app/components/ui-templates-page/ui-templates-page.component.sass b/src/app/components/ui-templates-page/ui-templates-page.component.sass index d7f5fe7..af0fbac 100644 --- a/src/app/components/ui-templates-page/ui-templates-page.component.sass +++ b/src/app/components/ui-templates-page/ui-templates-page.component.sass @@ -57,12 +57,6 @@ .spacer flex-grow: 1 -.card-list - margin-inline: -16px - -.active-list-item - background-color: var(--primary-lighter) - color: var(--text-primary) - -.active-list-item:focus - background-color: var(--primary-lighter) +.template-tabs + display: block + margin-block-start: 2rem From d6608ee114a00bbc3566ce923469dc0f2ce9d597 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabian=20B=C3=BChler?= Date: Fri, 9 Aug 2024 13:20:14 +0200 Subject: [PATCH 05/14] Allow setting default ui template from template page --- .../ui-template/ui-template.component.html | 3 + .../ui-template/ui-template.component.ts | 22 ++++++- src/app/services/env.service.ts | 66 ++++++++++++++++--- 3 files changed, 82 insertions(+), 9 deletions(-) diff --git a/src/app/components-small/ui-template/ui-template.component.html b/src/app/components-small/ui-template/ui-template.component.html index 25c2021..26472c3 100644 --- a/src/app/components-small/ui-template/ui-template.component.html +++ b/src/app/components-small/ui-template/ui-template.component.html @@ -9,6 +9,9 @@
+
diff --git a/src/app/components-small/ui-template/ui-template.component.ts b/src/app/components-small/ui-template/ui-template.component.ts index 0d82687..adadede 100644 --- a/src/app/components-small/ui-template/ui-template.component.ts +++ b/src/app/components-small/ui-template/ui-template.component.ts @@ -6,6 +6,7 @@ import { MatChipInputEvent } from '@angular/material/chips'; import { Subscription } from 'rxjs'; import { MatDialog } from '@angular/material/dialog'; import { DeleteDialog } from 'src/app/dialogs/delete-dialog/delete-dialog.dialog'; +import { EnvService } from 'src/app/services/env.service'; @Component({ selector: 'qhana-ui-template', @@ -27,10 +28,13 @@ export class UiTemplateComponent implements OnChanges, OnInit, OnDestroy { currentTags: string[] | null = null; currentDescription: string | null = null; + currentEnvDefaultTemplate: string | null = null; + private tagsDirty: boolean = false; private updateSubscription: Subscription | null = null; + private defaultTemplateSubscription: Subscription | null = null; - constructor(private registry: PluginRegistryBaseService, private dialog: MatDialog) { } + constructor(private registry: PluginRegistryBaseService, private env: EnvService, private dialog: MatDialog) { } ngOnInit(): void { this.updateSubscription = this.registry.apiObjectSubject.subscribe((apiObject) => { @@ -45,10 +49,12 @@ export class UiTemplateComponent implements OnChanges, OnInit, OnDestroy { } } }); + this.defaultTemplateSubscription = this.env.uiTemplateId.subscribe(templateID => this.currentEnvDefaultTemplate = templateID); } ngOnDestroy(): void { this.updateSubscription?.unsubscribe(); + this.defaultTemplateSubscription?.unsubscribe(); } ngOnChanges(changes: SimpleChanges): void { @@ -140,4 +146,18 @@ export class UiTemplateComponent implements OnChanges, OnInit, OnDestroy { } } + async toggleEnvDefault() { + const templateId = this.templateLink?.resourceKey?.uiTemplateId; + if (templateId == null) { + return; + } + + if (this.currentEnvDefaultTemplate === templateId) { + // unset default + this.env.setDefaultUiTemplateEnvVar(null); + } else { + this.env.setDefaultUiTemplateEnvVar(templateId); + } + } + } diff --git a/src/app/services/env.service.ts b/src/app/services/env.service.ts index 8436d7a..2e62ead 100644 --- a/src/app/services/env.service.ts +++ b/src/app/services/env.service.ts @@ -16,7 +16,7 @@ import { Injectable } from '@angular/core'; import { BehaviorSubject, Observable, Subscription } from 'rxjs'; -import { ApiObject, isApiObject, isDeletedApiObject } from './api-data-types'; +import { ApiLink, ApiObject, CollectionApiObject, isApiObject, isDeletedApiObject } from './api-data-types'; import { PluginRegistryBaseService } from './registry.service'; @@ -53,6 +53,9 @@ export class EnvService { private urlMapSubject: BehaviorSubject | null> = new BehaviorSubject | null>(null); private uiTemplateSubject: BehaviorSubject = new BehaviorSubject(null); + private createEnvVarLink: ApiLink | null = null; + private currentUiTemplateEnvVar: ApiLink | null = null; + public get urlMap(): Observable | null> { return this.urlMapSubject.asObservable(); } @@ -65,7 +68,9 @@ export class EnvService { this.subscribe(); // just kick off get, updates are handled by subscription setup above this.registryService.getByRel([["env", "collection"]], new URLSearchParams([["name", "UI_URL_MAP"]]), true); - this.registryService.getByRel([["env", "collection"]], new URLSearchParams([["name", "DEFAULT_UI_TEMPLATE"]]), true); + this.registryService.getByRel([["env", "collection"]], new URLSearchParams([["name", "DEFAULT_UI_TEMPLATE"]]), true).then(response => { + this.createEnvVarLink = response?.links?.find?.(link => link.resourceType === "env" && link.rel.some(r => r === "create")) ?? null; + }); } private unsubscribe() { @@ -74,6 +79,11 @@ export class EnvService { private subscribe() { this.registrySubscription = this.registryService.apiObjectSubject.subscribe((apiObject => { + if (isDeletedApiObject(apiObject) && apiObject.deleted.resourceType === "env") { + if (apiObject.deleted.name === "DEFAULT_UI_TEMPLATE") { + this.uiTemplateSubject.next(null); + } + } if (apiObject.self.resourceType !== "env") { return; // only look at env api objects } @@ -82,12 +92,8 @@ export class EnvService { this.updateUrlMap(apiObject.value); } if (apiObject.name === "DEFAULT_UI_TEMPLATE") { - this.uiTemplateSubject.next(apiObject.value); - } - } - if (isDeletedApiObject(apiObject)) { - if (apiObject.deleted.name === "DEFAULT_UI_TEMPLATE") { - this.uiTemplateSubject.next(null); + this.currentUiTemplateEnvVar = apiObject.self; + this.uiTemplateSubject.next(apiObject.value ? apiObject.value : null); } } })); @@ -122,4 +128,48 @@ export class EnvService { }); return urlIn; } + + public async setDefaultUiTemplateEnvVar(templateId: string | null) { + if (this.currentUiTemplateEnvVar == null) { + // no env var available, create it + if (!templateId) { + return; // nothing to create + } + if (this.createEnvVarLink == null) { + console.warn("No create link found for env vars!"); + return; + } + this.registryService.submitByApiLink(this.createEnvVarLink, { + name: "DEFAULT_UI_TEMPLATE", + value: templateId, + }); + return; + } + if (this.currentUiTemplateEnvVar == null) { + return; + } + const response = await this.registryService.getByApiLink(this.currentUiTemplateEnvVar); + if (response == null) { + console.warn("Env var DEFAULT_UI_TEMPLATE not found!"); + return; + } + if (!templateId) { + const deleteLink = response.links.find(link => link.resourceType === "env" && link.rel.some(r => r === "delete")); + if (deleteLink == null) { + console.warn("No delete link found for env var DEFAULT_UI_TEMPLATE!"); + return; + } + this.registryService.submitByApiLink(deleteLink); + return; + } + const updateLink = response.links.find(link => link.resourceType === "env" && link.rel.some(r => r === "update")); + if (updateLink == null) { + console.warn("No update link found for env var DEFAULT_UI_TEMPLATE!"); + return; + } + this.registryService.submitByApiLink(updateLink, { + name: "DEFAULT_UI_TEMPLATE", + value: templateId, + }); + } } From e60ca1aeaec73a359c20bd756d41a98f64af642a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabian=20B=C3=BChler?= Date: Fri, 9 Aug 2024 14:26:24 +0200 Subject: [PATCH 06/14] Show template tab details --- src/app/app.module.ts | 4 ++ .../plugin-filter-view.component.html | 53 +++++++++++++++ .../plugin-filter-view.component.sass | 24 +++++++ .../plugin-filter-view.component.ts | 43 ++++++++++++ .../ui-template-tab-list.component.html | 12 ++-- .../ui-template-tab.component.html | 13 ++++ .../ui-template-tab.component.sass | 6 ++ .../ui-template-tab.component.ts | 65 +++++++++++++++++++ 8 files changed, 214 insertions(+), 6 deletions(-) create mode 100644 src/app/components-small/plugin-filter-view/plugin-filter-view.component.html create mode 100644 src/app/components-small/plugin-filter-view/plugin-filter-view.component.sass create mode 100644 src/app/components-small/plugin-filter-view/plugin-filter-view.component.ts create mode 100644 src/app/components-small/ui-template-tab/ui-template-tab.component.html create mode 100644 src/app/components-small/ui-template-tab/ui-template-tab.component.sass create mode 100644 src/app/components-small/ui-template-tab/ui-template-tab.component.ts diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 05839cc..a77fb44 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -89,6 +89,8 @@ import { ExportExperimentDialog } from './dialogs/export-experiment/export-exper import { MarkdownHelpDialog } from './dialogs/markdown-help/markdown-help.dialog'; import { UiTemplateComponent } from "./components-small/ui-template/ui-template.component"; 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'; @NgModule({ declarations: [ @@ -138,6 +140,8 @@ import { UiTemplateTabListComponent } from './components-small/ui-template-tab-l UiTemplatesPageComponent, UiTemplateComponent, UiTemplateTabListComponent, + UiTemplateTabComponent, + PluginFilterViewComponent, ], imports: [ BrowserModule, diff --git a/src/app/components-small/plugin-filter-view/plugin-filter-view.component.html b/src/app/components-small/plugin-filter-view/plugin-filter-view.component.html new file mode 100644 index 0000000..d1c30aa --- /dev/null +++ b/src/app/components-small/plugin-filter-view/plugin-filter-view.component.html @@ -0,0 +1,53 @@ +@if (filterType === "and") { +
+
+
+
AND
+
+
+
+ @for (f of filter.and; track $index) { + + } +
+
+} +@if (filterType === "or") { +
+
+
+
OR
+
+
+
+ @for (f of filter.or; track $index) { + + } +
+
+} +@if (filterType === "not") { +
+

NOT

+
+ +
+} +@if (filterType === "id") { +

Plugin ID: {{filter.id}}

+} +@if (filterType === "name") { +

Plugin name: {{filter.name}}

+} +@if (filterType === "version") { +

Plugin version: {{filter.version}}

+} +@if (filterType === "tag") { +

Plugin tag: {{filter.tag}}

+} +@if (filterType === "type") { +

Plugin type: {{filter.type}}

+} +@if (filterType === "empty") { +

No filter set.

+} diff --git a/src/app/components-small/plugin-filter-view/plugin-filter-view.component.sass b/src/app/components-small/plugin-filter-view/plugin-filter-view.component.sass new file mode 100644 index 0000000..1b1d4d8 --- /dev/null +++ b/src/app/components-small/plugin-filter-view/plugin-filter-view.component.sass @@ -0,0 +1,24 @@ +.filter-container + display: flex + gap: 1rem + align-items: stretch + +.filter-type + display: flex + flex-direction: column + align-items: center + + .line + flex-grow: 1 + width: 0px + border-inline-start: 1px solid var(--text-washed) + +.single-filter-container + display: flex + gap: 0.5rem + align-items: center + + .line + align-self: stretch + width: 0px + border-inline-start: 1px solid var(--text-washed) diff --git a/src/app/components-small/plugin-filter-view/plugin-filter-view.component.ts b/src/app/components-small/plugin-filter-view/plugin-filter-view.component.ts new file mode 100644 index 0000000..0f805a3 --- /dev/null +++ b/src/app/components-small/plugin-filter-view/plugin-filter-view.component.ts @@ -0,0 +1,43 @@ +import { Component, Input, OnChanges, SimpleChanges } from '@angular/core'; +import { CommonModule } from '@angular/common'; + +const ALLOWED_FILTER_KEYS: Set<'and' | 'or' | 'not' | 'id' | 'name' | 'tag' | 'version' | 'type'> = new Set(['and', 'or', 'not', 'id', 'name', 'tag', 'version', 'type']); + +function isAllowedFilter(filterType: string): filterType is 'and' | 'or' | 'not' | 'id' | 'name' | 'tag' | 'version' | 'type' { + return ALLOWED_FILTER_KEYS.has(filterType as any); +} + +@Component({ + selector: 'qhana-plugin-filter-view', + templateUrl: './plugin-filter-view.component.html', + styleUrl: './plugin-filter-view.component.sass' +}) +export class PluginFilterViewComponent implements OnChanges { + + @Input() filter: any = null; + + filterType: 'empty' | 'and' | 'or' | 'not' | 'id' | 'name' | 'tag' | 'version' | 'type' | null = null; + + ngOnChanges(changes: SimpleChanges): void { + if (this.filter == null) { + this.filterType = null; + return; + } + + const keys = Object.keys(this.filter); + if (keys.length !== 1) { + if (keys.length === 0) { + this.filterType = 'empty'; + } else { + this.filterType = null; + } + return; + } + const filterType = keys[0]; + if (!isAllowedFilter(filterType)) { + this.filterType = null; + return; + } + this.filterType = filterType; + } +} 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 182269f..0ca7c65 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 @@ -11,7 +11,7 @@

Navigation Tab Groups

(Group: {{navigationGroup.groupKey}}.{{hrefToTab.get(tab.href)?.groupKey}}) -

TODO: CONTENT

+ } @@ -33,7 +33,7 @@

(Group: {{group.groupKey}}.{{hrefToTab.get(tab.href)?.groupKey}}) -

TODO: CONTENT

+ } @@ -54,7 +54,7 @@

Experiment Workspace Tabs

{{tab.name}} -

TODO: CONTENT

+ } @@ -72,7 +72,7 @@

Experiment Navigation Tab Groups

(Group: {{experimentNavigationGroup.groupKey}}.{{hrefToTab.get(tab.href)?.groupKey}}) -

TODO: CONTENT

+ } @@ -94,7 +94,7 @@

(Group: {{group.groupKey}}.{{hrefToTab.get(tab.href)?.groupKey}}) -

TODO: CONTENT

+ } @@ -119,7 +119,7 @@

(Group: {{group.groupKey}}.{{hrefToTab.get(tab.href)?.groupKey}}) -

TODO: CONTENT

+ } 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 new file mode 100644 index 0000000..28e9091 --- /dev/null +++ b/src/app/components-small/ui-template-tab/ui-template-tab.component.html @@ -0,0 +1,13 @@ +
+
+ + +
+
+

Description: {{tabData?.description ? '' : '–'}}

+ +@if(!isEditing && !(tabData?.groupKey ?? false) && tabFilterData) { +

Plugin filter:

+ +} + 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 new file mode 100644 index 0000000..d493905 --- /dev/null +++ b/src/app/components-small/ui-template-tab/ui-template-tab.component.sass @@ -0,0 +1,6 @@ +.tab-header + display: flex + align-items: center + justify-content: flex-end + width: 100% + 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 new file mode 100644 index 0000000..ee8b7ba --- /dev/null +++ b/src/app/components-small/ui-template-tab/ui-template-tab.component.ts @@ -0,0 +1,65 @@ +import { Component, Input, OnChanges, OnDestroy, OnInit, SimpleChanges } from '@angular/core'; +import { Subscription } from 'rxjs'; +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'; + +@Component({ + selector: 'qhana-ui-template-tab', + templateUrl: './ui-template-tab.component.html', + styleUrl: './ui-template-tab.component.sass' +}) +export class UiTemplateTabComponent implements OnChanges, OnInit, OnDestroy { + + @Input() tabLink: ApiLink | null = null; + + tabData: TemplateTabApiObject | null = null; + tabFilterData: any = null; + templateUpdateLink: ApiLink | null = null; + templateDeleteLink: ApiLink | null = null; + + isEditing: boolean = false; + + private updateSubscription: Subscription | null = null; + + constructor(private registry: PluginRegistryBaseService) { } + + ngOnInit(): void { + this.updateSubscription = this.registry.apiObjectSubject.subscribe((apiObject) => { + if (apiObject.self.href !== this.tabLink?.href) { + return; + } + if (apiObject.self.resourceType === "ui-template-tab") { + this.tabData = apiObject as TemplateTabApiObject; + } + }); + } + + ngOnDestroy(): void { + this.updateSubscription?.unsubscribe(); + } + + ngOnChanges(changes: SimpleChanges): void { + this.loadTemplateTab(); + } + + private async loadTemplateTab() { + if (this.tabLink == null) { + this.tabData = null; + return; + } + const tabResponse = await this.registry.getByApiLink(this.tabLink, null, true); + this.tabData = tabResponse?.data ?? null; + + if (tabResponse?.data?.filterString) { + try { + this.tabFilterData = JSON.parse(tabResponse?.data?.filterString); + } catch { + // Ignore error + } + } + + 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; + } +} From 3eca7ef9e66a21dcd006c0883e2139875e06ffdb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabian=20B=C3=BChler?= Date: Wed, 14 Aug 2024 15:05:39 +0200 Subject: [PATCH 07/14] Fix dark mode contrast issues --- src/styles.sass | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/src/styles.sass b/src/styles.sass index f4cdbb6..ec08a47 100644 --- a/src/styles.sass +++ b/src/styles.sass @@ -52,7 +52,7 @@ $my-dark-theme: mat.define-dark-theme((color: (primary: $my-primary,accent: $my- // line is for milkdown --line: 150,150,150 -.mat-mdc-card, .color-scheme-card +.mat-mdc-card, .mat-expansion-panel, .color-scheme-card --background: #{mat.get-color-from-palette(mat.$light-theme-background-palette, 'card')} --background-card: #bbb --border-color: #ddd @@ -105,7 +105,23 @@ $my-dark-theme: mat.define-dark-theme((color: (primary: $my-primary,accent: $my- // line is for milkdown --line: 170,170,170 - .mat-mdc-card, .color-scheme-card + // material theme token overrides for better dark mode text contrast + --mat-option-selected-state-label-text-color: var(--primary-text) + --mdc-filled-text-field-caret-color: var(--primary-text) + --mdc-filled-text-field-focus-active-indicator-color: var(--primary-text) + --mdc-filled-text-field-focus-label-text-color: rgba(179, 136, 255, 0.87) + --mdc-outlined-text-field-caret-color: var(--primary-text) + --mdc-outlined-text-field-focus-outline-color: var(--primary-text) + --mdc-outlined-text-field-focus-label-text-color: rgba(179, 136, 255, 0.87) + --mat-form-field-focus-select-arrow-color: rgba(179, 136, 255, 0.87) + --mat-select-focused-arrow-color: rgba(179, 136, 255, 0.87) + --mdc-circular-progress-active-indicator-color: var(--primary-text) + --mat-badge-background-color: var(--primary-text) + --mat-stepper-header-selected-state-icon-background-color: var(--primary-text) + --mat-stepper-header-done-state-icon-background-color: var(--primary-text) + --mat-stepper-header-edit-state-icon-background-color: var(--primary-text) + + .mat-mdc-card, .mat-expansion-panel, .color-scheme-card --background: #{mat.get-color-from-palette(mat.$dark-theme-background-palette, 'card')} --background-card: #{mat.get-color-from-palette(mat.$dark-theme-background-palette, 'background')} --border-color: #888 @@ -131,6 +147,9 @@ $my-dark-theme: mat.define-dark-theme((color: (primary: $my-primary,accent: $my- box-shadow: none border: 2px solid var(--border-color) + :root .mat-primary .mat-pseudo-checkbox-checked.mat-pseudo-checkbox-minimal::after, :root .mat-primary .mat-pseudo-checkbox-indeterminate.mat-pseudo-checkbox-minimal::after + color: var(--primary-text) + :root // color contrast fixes: .mat-mdc-form-field.mat-mdc-primary From 182c352f0496634385e5096a1523050bf9d7d02c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabian=20B=C3=BChler?= Date: Thu, 15 Aug 2024 09:58:37 +0200 Subject: [PATCH 08/14] Fix styling issues in template component Make description more form field like Always display a box, even while loading the template data --- .../ui-template/ui-template.component.html | 9 ++++++++- .../ui-template/ui-template.component.sass | 5 ++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/app/components-small/ui-template/ui-template.component.html b/src/app/components-small/ui-template/ui-template.component.html index 26472c3..30feff9 100644 --- a/src/app/components-small/ui-template/ui-template.component.html +++ b/src/app/components-small/ui-template/ui-template.component.html @@ -1,3 +1,10 @@ + + + Loading... + + + +
@@ -40,6 +47,6 @@ /> - + diff --git a/src/app/components-small/ui-template/ui-template.component.sass b/src/app/components-small/ui-template/ui-template.component.sass index e4ce0ec..b9f671a 100644 --- a/src/app/components-small/ui-template/ui-template.component.sass +++ b/src/app/components-small/ui-template/ui-template.component.sass @@ -1,6 +1,6 @@ .header display: flex - align-items: center + align-items: flex-start justify-content: space-between gap: 1rem @@ -21,3 +21,6 @@ .tags-field width: 100% + +.qhana-form-input + border-width: 1px From 2076f7b7f568e9aca43aaaca143378192867d745 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabian=20B=C3=BChler?= Date: Thu, 15 Aug 2024 09:59:31 +0200 Subject: [PATCH 09/14] Add more resource types to delete dialog --- src/app/dialogs/delete-dialog/delete-dialog.dialog.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/app/dialogs/delete-dialog/delete-dialog.dialog.ts b/src/app/dialogs/delete-dialog/delete-dialog.dialog.ts index f30cfb4..197dac3 100644 --- a/src/app/dialogs/delete-dialog/delete-dialog.dialog.ts +++ b/src/app/dialogs/delete-dialog/delete-dialog.dialog.ts @@ -6,6 +6,8 @@ const RESOURCE_TYPE_TO_TEXT: { [props: string]: string } = { 'resource': 'resource', 'seed': 'seed url', 'service': 'service entry', + 'ui-template': 'UI template', + 'ui-template-tab': 'UI template tab', } @Component({ From a4170231be88858b5cdab34abd23ff413a2a72f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabian=20B=C3=BChler?= Date: Thu, 15 Aug 2024 10:01:33 +0200 Subject: [PATCH 10/14] Fix child filters having no gap in plugin filter view --- .../plugin-filter-view/plugin-filter-view.component.html | 4 ++-- .../plugin-filter-view/plugin-filter-view.component.sass | 5 +++++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/app/components-small/plugin-filter-view/plugin-filter-view.component.html b/src/app/components-small/plugin-filter-view/plugin-filter-view.component.html index d1c30aa..393080a 100644 --- a/src/app/components-small/plugin-filter-view/plugin-filter-view.component.html +++ b/src/app/components-small/plugin-filter-view/plugin-filter-view.component.html @@ -5,7 +5,7 @@
AND
-
+
@for (f of filter.and; track $index) { } @@ -19,7 +19,7 @@
OR
-
+
@for (f of filter.or; track $index) { } diff --git a/src/app/components-small/plugin-filter-view/plugin-filter-view.component.sass b/src/app/components-small/plugin-filter-view/plugin-filter-view.component.sass index 1b1d4d8..1a0bb8b 100644 --- a/src/app/components-small/plugin-filter-view/plugin-filter-view.component.sass +++ b/src/app/components-small/plugin-filter-view/plugin-filter-view.component.sass @@ -13,6 +13,11 @@ width: 0px border-inline-start: 1px solid var(--text-washed) +.child-filters + display: flex + flex-direction: column + gap: 0.5rem + .single-filter-container display: flex gap: 0.5rem From a51faf7ba42af101e3faae92804159fb3e005a02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabian=20B=C3=BChler?= Date: Thu, 15 Aug 2024 10:02:19 +0200 Subject: [PATCH 11/14] Add template tab form and use it to create/update tabs --- src/app/app.module.ts | 4 + .../plugin-filter-form.component.html | 90 +++++++ .../plugin-filter-form.component.sass | 67 +++++ .../plugin-filter-form.component.ts | 242 ++++++++++++++++++ .../ui-template-tab-form.component.html | 43 ++++ .../ui-template-tab-form.component.sass | 25 ++ .../ui-template-tab-form.component.ts | 186 ++++++++++++++ .../ui-template-tab-list.component.html | 32 ++- .../ui-template-tab-list.component.sass | 3 + .../ui-template-tab-list.component.ts | 33 ++- .../ui-template-tab.component.html | 18 +- .../ui-template-tab.component.sass | 6 + .../ui-template-tab.component.ts | 65 ++++- 13 files changed, 781 insertions(+), 33 deletions(-) create mode 100644 src/app/components-small/plugin-filter-form/plugin-filter-form.component.html create mode 100644 src/app/components-small/plugin-filter-form/plugin-filter-form.component.sass create mode 100644 src/app/components-small/plugin-filter-form/plugin-filter-form.component.ts create mode 100644 src/app/components-small/ui-template-tab-form/ui-template-tab-form.component.html create mode 100644 src/app/components-small/ui-template-tab-form/ui-template-tab-form.component.sass create mode 100644 src/app/components-small/ui-template-tab-form/ui-template-tab-form.component.ts 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 @@ +
+ + Name: + + Invalid tab name! + +
+

Description:

+ +
+
+

Show tab in:

+ + + {{location.value}} + + +
+ + Tab Group: + + The group key of the tab this tab should be grouped under. (Use '.' to separate group keys for nested groups.) + + + Sort Key: + + +
+ + Icon: + + + {{templateForm.value.icon || "extension"}} +
+ + Group Key: + + Establish this tab as its own tab group. (Cannot contain '.') + +

Group key cannot be used in experiment workspace tabs!

+ +
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 @@ -
+ + + Create New Tab + + + + + +

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}} - + }
-
+

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}}) - + } }

-
+

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); + } } } From e53ecbebf16cdb0f757dfca0fe73eec9a38c7798 Mon Sep 17 00:00:00 2001 From: Philipp Wundrack Date: Mon, 19 Aug 2024 12:18:42 +0200 Subject: [PATCH 12/14] fix "Cannot match any routes" --- src/app/app-routing.module.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts index e92a9d5..ce1992a 100644 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -91,7 +91,7 @@ const extraTabsMatcher: UrlMatcher = (segments: UrlSegment[], group, route): Url } // match: ./plugin/:pluginId - if (segments[index]?.path !== "plugins") { + if (segments[index]?.path !== "plugins" && segments[index]?.path !== "plugin") { return null; } consumed.push(segments[index]); From 3840396f9cfb3d873404806b639b42a7f2c5f108 Mon Sep 17 00:00:00 2001 From: Philipp Wundrack Date: Mon, 19 Aug 2024 12:20:37 +0200 Subject: [PATCH 13/14] remove debug output --- src/app/app-routing.module.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts index ce1992a..99860d3 100644 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -50,8 +50,6 @@ const extraTabsMatcher: UrlMatcher = (segments: UrlSegment[], group, route): Url } } - console.log(consumed, params) - // match: ./extra[/:path]/:templateTabId if (segments[index]?.path !== "extra") { return null; @@ -80,8 +78,6 @@ const extraTabsMatcher: UrlMatcher = (segments: UrlSegment[], group, route): Url } params.templateTabId = tabId; - console.log(consumed, params) - // found full match? if (index === segments.length) { return { @@ -102,8 +98,6 @@ const extraTabsMatcher: UrlMatcher = (segments: UrlSegment[], group, route): Url index += 1; } - console.log(consumed, params) - // found full match? if (index === segments.length && pluginId != null) { params.pluginId = pluginId; From 61113298045f71e25843ea7aeb37763f7cc1a3f4 Mon Sep 17 00:00:00 2001 From: Philipp Wundrack Date: Mon, 19 Aug 2024 12:23:27 +0200 Subject: [PATCH 14/14] fix form field name cut off --- .../change-ui-template/change-ui-template.dialog.html | 2 +- .../change-ui-template/change-ui-template.dialog.sass | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/app/dialogs/change-ui-template/change-ui-template.dialog.html b/src/app/dialogs/change-ui-template/change-ui-template.dialog.html index e033ca9..28aabe4 100644 --- a/src/app/dialogs/change-ui-template/change-ui-template.dialog.html +++ b/src/app/dialogs/change-ui-template/change-ui-template.dialog.html @@ -1,5 +1,5 @@

{{data.template ? 'Update' : 'Create'}} UI Template

-
+
Name diff --git a/src/app/dialogs/change-ui-template/change-ui-template.dialog.sass b/src/app/dialogs/change-ui-template/change-ui-template.dialog.sass index 10e7a6a..353639f 100644 --- a/src/app/dialogs/change-ui-template/change-ui-template.dialog.sass +++ b/src/app/dialogs/change-ui-template/change-ui-template.dialog.sass @@ -2,4 +2,7 @@ width: 100% .description-textarea - min-height: 20ex \ No newline at end of file + min-height: 20ex + +.dialog-content + padding-top: 0.5rem