diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts index ed98425..99860d3 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]+$/; @@ -49,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; @@ -79,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 { @@ -90,7 +87,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]); @@ -101,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; @@ -118,6 +113,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..b27e04d 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'; @@ -86,6 +87,12 @@ 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"; +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: [ @@ -132,6 +139,13 @@ import { MarkdownHelpDialog } from './dialogs/markdown-help/markdown-help.dialog PluginFilterEditorComponent, TabGroupListComponent, ChooseTemplateDialog, + UiTemplatesPageComponent, + 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:
+Or combine multiple filters with:
+Must have at least one plugin filter!
+ } + @for (f of filterValue; track $index) { +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 EventEmitterNOT
+ +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..1a0bb8b --- /dev/null +++ b/src/app/components-small/plugin-filter-view/plugin-filter-view.component.sass @@ -0,0 +1,29 @@ +.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) + +.child-filters + display: flex + flex-direction: column + gap: 0.5rem + +.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-form/ui-template-tab-form.component.html b/src/app/components-small/ui-template-tab-form/ui-template-tab-form.component.html new file mode 100644 index 0000000..f775fc5 --- /dev/null +++ b/src/app/components-small/ui-template-tab-form/ui-template-tab-form.component.html @@ -0,0 +1,43 @@ + diff --git a/src/app/components-small/ui-template-tab-form/ui-template-tab-form.component.sass b/src/app/components-small/ui-template-tab-form/ui-template-tab-form.component.sass new file mode 100644 index 0000000..2d17ff5 --- /dev/null +++ b/src/app/components-small/ui-template-tab-form/ui-template-tab-form.component.sass @@ -0,0 +1,25 @@ +.form-field + width: 100% + margin-block-end: 0.5rem + +.filter-example + resize: none + overflow: hidden + +.location-chooser + margin-block-end: 1rem + +.icon-field-wrapper + display: flex + gap: 1rem + align-items: baseline + +.icon-preview + font-size: 24px + margin-inline: 1rem + +.submit-button + margin-block-start: 2rem + +.form-error + color: var(--warn-text) diff --git a/src/app/components-small/ui-template-tab-form/ui-template-tab-form.component.ts b/src/app/components-small/ui-template-tab-form/ui-template-tab-form.component.ts new file mode 100644 index 0000000..675569d --- /dev/null +++ b/src/app/components-small/ui-template-tab-form/ui-template-tab-form.component.ts @@ -0,0 +1,186 @@ +import { Component, EventEmitter, Input, OnChanges, OnDestroy, OnInit, Output, SimpleChanges } from '@angular/core'; +import { TAB_GROUP_NAME_OVERRIDES, TemplateTabApiObject } from 'src/app/services/templates.service'; +import { FormBuilder, FormGroup, ValidationErrors, Validators, ValidatorFn, AbstractControl } from '@angular/forms'; +import { Subscription } from 'rxjs'; + +export function isInSetValidator(validValues: any[]): ValidatorFn { + return (control: AbstractControl): { [key: string]: any } | null => { + if (!validValues.includes(control.value)) { + return { invalidValue: true }; + } + return null; + }; +} + +@Component({ + selector: 'qhana-ui-template-tab-form', + templateUrl: './ui-template-tab-form.component.html', + styleUrl: './ui-template-tab-form.component.sass' +}) +export class UiTemplateTabFormComponent implements OnChanges, OnDestroy, OnInit { + + @Input() tabData: TemplateTabApiObject | null = null; + + @Output() isDirty: EventEmitterDescription: {{tabData?.description ? '' : '–'}}
+Plugin filter:
+