Warning, /firebird/packages/firebird-ng/display/src/lib/components/data-selector/data-selector.component.ts is written in an unsupported language. File is not indexed.
0001 import {
0002 ChangeDetectionStrategy,
0003 Component,
0004 Type,
0005 computed,
0006 effect,
0007 inject,
0008 input,
0009 output,
0010 signal,
0011 untracked,
0012 } from '@angular/core';
0013 import { NgComponentOutlet } from '@angular/common';
0014 import { MatButton } from '@angular/material/button';
0015 import { MatIcon } from '@angular/material/icon';
0016 import { resolveRegistry } from '@dexvis/app-features';
0017 import {
0018 DATA_SELECTOR_TABS,
0019 DataCatalogService,
0020 DataSelection,
0021 DataSelectionService,
0022 DataSelectorTabRegistration,
0023 } from '@dexvis/firebird-ng/api';
0024
0025 /**
0026 * The data selector: one control to choose what the display shows — a named
0027 * preset, a physics case picked by tags, or geometry and event files by URL
0028 * or from disk. The display's "open" toolbar panel and the config page both
0029 * embed it; they differ only in the button caption and in what happens after
0030 * Show (`applied`): the panel closes, the config page navigates to /display.
0031 *
0032 * Tabs come from DI (`withDataSelectorTab`), so an experiment pack can add,
0033 * replace (same tab id) or drop them (`withoutFeatures('data-selector-tab:<id>')`). A tab reads and writes `DataSelectionService.draft`;
0034 * this host owns the Show/Cancel buttons and applies the draft through the
0035 * one shared path (config keys, then a live display reloads).
0036 */
0037 @Component({
0038 selector: 'firebird-data-selector',
0039 templateUrl: './data-selector.component.html',
0040 styleUrl: './data-selector.component.scss',
0041 changeDetection: ChangeDetectionStrategy.OnPush,
0042 imports: [NgComponentOutlet, MatButton, MatIcon],
0043 })
0044 export class DataSelectorComponent {
0045 /** Caption of the apply button: 'Show' in the display panel, 'Display' on the config page. */
0046 readonly applyLabel = input('Show');
0047 /** When false, the apply button is enabled with an unchanged draft (the config page navigates anyway). */
0048 readonly requireChanges = input(true);
0049 readonly showCancel = input(true);
0050 /** The selection that was applied. */
0051 readonly applied = output<DataSelection>();
0052 readonly cancelled = output<void>();
0053
0054 readonly catalog = inject(DataCatalogService);
0055 readonly selection = inject(DataSelectionService);
0056 /** One tab per id: a later registration with the id of an earlier one replaces it (tokens.ts). */
0057 private readonly registeredTabs = resolveRegistry(inject(DATA_SELECTOR_TABS, { optional: true }) ?? [], tab => tab.id);
0058
0059 /** Registered tabs by `order` (ties: registration order), minus catalog-only tabs when there is no catalog. */
0060 readonly visibleTabs = computed<DataSelectorTabRegistration[]>(() => {
0061 const catalogIsEmpty = this.catalog.isEmpty();
0062 return this.registeredTabs
0063 .filter(tab => tab.needsCatalog === false || !catalogIsEmpty)
0064 .sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
0065 });
0066
0067 readonly activeTabId = signal<string | null>(null);
0068 readonly tabComponent = signal<Type<unknown> | null>(null);
0069 readonly tabError = signal<string | null>(null);
0070
0071 readonly applyEnabled = computed(() => !this.requireChanges() || this.selection.draftHasChanges());
0072
0073 constructor() {
0074 void this.catalog.ensureRemoteLoaded();
0075 // Keep a valid tab selected as the tab set changes (the remote catalog
0076 // arriving can reveal Presets/Physics). First choice: Presets when a
0077 // preset is what is loaded, otherwise Manual, which shows the URLs as is.
0078 effect(() => {
0079 const tabs = this.visibleTabs();
0080 const current = untracked(this.activeTabId);
0081 if (tabs.some(tab => tab.id === current)) return;
0082 const preferred = untracked(this.selection.activeEntry)
0083 ? tabs[0]
0084 : (tabs.find(tab => tab.id === 'manual') ?? tabs[0]);
0085 if (preferred) this.selectTab(preferred.id);
0086 });
0087 }
0088
0089 selectTab(id: string): void {
0090 const tab = this.visibleTabs().find(candidate => candidate.id === id);
0091 if (!tab || this.activeTabId() === id) return;
0092 // Each tab starts from the configured state; a half-made choice on
0093 // another tab must not leak into this one
0094 this.selection.clearDraft();
0095 this.activeTabId.set(id);
0096 this.tabComponent.set(null);
0097 this.tabError.set(null);
0098 tab.load().then(
0099 component => {
0100 if (this.activeTabId() === id) this.tabComponent.set(component);
0101 },
0102 error => {
0103 console.error(`[DataSelector] tab '${id}' failed to load`, error);
0104 this.tabError.set(`Tab '${tab.label}' failed to load: ${error instanceof Error ? error.message : error}`);
0105 },
0106 );
0107 }
0108
0109 apply(): void {
0110 const draft = this.selection.draft();
0111 this.selection.apply(draft);
0112 this.applied.emit(draft);
0113 }
0114
0115 cancel(): void {
0116 this.selection.clearDraft();
0117 this.cancelled.emit();
0118 }
0119 }