@noatgnu/cupcake-vanilla 1.2.8 → 1.2.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,16 +3,17 @@ import { Injectable, signal, computed, EventEmitter, inject, Output, Input, Comp
3
3
  import * as i3 from '@noatgnu/cupcake-core';
4
4
  import { BaseApiService, WebSocketService, NotificationService as NotificationService$1, LabGroupService, ToastService, ResourceVisibility } from '@noatgnu/cupcake-core';
5
5
  import { switchMap, map, tap, takeUntil, filter as filter$1 } from 'rxjs/operators';
6
- import { from, Observable, throwError, NEVER, debounceTime, distinctUntilChanged, filter, switchMap as switchMap$1, map as map$1, catchError, of, tap as tap$1, Subject, BehaviorSubject } from 'rxjs';
6
+ import { from, Observable, throwError, NEVER, debounceTime, distinctUntilChanged, filter, switchMap as switchMap$1, map as map$1, catchError, of, tap as tap$1, Subject, BehaviorSubject, firstValueFrom } from 'rxjs';
7
7
  import jsSHA from 'jssha';
8
8
  import * as i5 from '@angular/common';
9
9
  import { CommonModule } from '@angular/common';
10
10
  import * as i1 from '@angular/forms';
11
11
  import { FormBuilder, Validators, ReactiveFormsModule, FormsModule } from '@angular/forms';
12
12
  import * as i2 from '@ng-bootstrap/ng-bootstrap';
13
- import { NgbTypeahead, NgbModule, NgbNavModule, NgbActiveModal, NgbModal } from '@ng-bootstrap/ng-bootstrap';
13
+ import { NgbTypeahead, NgbModule, NgbNavModule, NgbActiveModal, NgbModal, NgbPaginationModule } from '@ng-bootstrap/ng-bootstrap';
14
14
  import * as i5$1 from '@angular/cdk/drag-drop';
15
15
  import { moveItemInArray, DragDropModule } from '@angular/cdk/drag-drop';
16
+ import { RouterModule } from '@angular/router';
16
17
 
17
18
  var OntologyType;
18
19
  (function (OntologyType) {
@@ -1526,10 +1527,63 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.3", ngImpor
1526
1527
  class OntologySearchService extends BaseApiService {
1527
1528
  /**
1528
1529
  * Get ontology suggestions across all sources for SDRF validation
1530
+ *
1531
+ * @param params Query parameters
1532
+ * @param params.q Search query (required, min 2 characters)
1533
+ * @param params.type Ontology type to search (optional)
1534
+ * @param params.match Match type - 'contains' or 'startswith' (default: 'contains')
1535
+ * @param params.limit Maximum number of results (default: 50)
1536
+ * @param params.customFilters Custom filters to apply (optional)
1537
+ * Example: {"organism": "Homo sapiens"} or {"organism": {"icontains": "human"}}
1529
1538
  */
1530
1539
  suggest(params) {
1531
- const httpParams = this.buildHttpParams(params);
1532
- return this.get(`${this.apiUrl}/ontology-search/suggest/`, { params: httpParams });
1540
+ const queryParams = {
1541
+ q: params.q,
1542
+ };
1543
+ if (params.type) {
1544
+ queryParams.type = params.type;
1545
+ }
1546
+ if (params.match) {
1547
+ queryParams.match = params.match;
1548
+ }
1549
+ if (params.limit) {
1550
+ queryParams.limit = params.limit;
1551
+ }
1552
+ if (params.customFilters) {
1553
+ queryParams.custom_filters = JSON.stringify(params.customFilters);
1554
+ }
1555
+ const httpParams = this.buildHttpParams(queryParams);
1556
+ return this.get(`${this.apiUrl}/ontology/search/suggest/`, { params: httpParams });
1557
+ }
1558
+ /**
1559
+ * Get ontology suggestions for a specific ontology type with custom filters
1560
+ */
1561
+ suggestWithFilters(query, ontologyType, customFilters, limit = 50) {
1562
+ return this.suggest({
1563
+ q: query,
1564
+ type: ontologyType,
1565
+ customFilters,
1566
+ limit
1567
+ });
1568
+ }
1569
+ /**
1570
+ * Search across multiple ontology types
1571
+ */
1572
+ searchAll(query, limit = 50) {
1573
+ return this.suggest({
1574
+ q: query,
1575
+ limit
1576
+ });
1577
+ }
1578
+ /**
1579
+ * Search specific ontology type
1580
+ */
1581
+ searchType(query, ontologyType, limit = 50) {
1582
+ return this.suggest({
1583
+ q: query,
1584
+ type: ontologyType,
1585
+ limit
1586
+ });
1533
1587
  }
1534
1588
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.3", ngImport: i0, type: OntologySearchService, deps: null, target: i0.ɵɵFactoryTarget.Injectable });
1535
1589
  static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.3", ngImport: i0, type: OntologySearchService, providedIn: 'root' });
@@ -3207,6 +3261,7 @@ class MetadataValueEditModal {
3207
3261
  metadataColumnService;
3208
3262
  metadataColumnTemplateService;
3209
3263
  labGroupService;
3264
+ ontologySearchService;
3210
3265
  config;
3211
3266
  valueSaved = new EventEmitter();
3212
3267
  editForm;
@@ -3227,7 +3282,7 @@ class MetadataValueEditModal {
3227
3282
  specialInputValue = signal('', ...(ngDevMode ? [{ debugName: "specialInputValue" }] : []));
3228
3283
  selectedSampleIndices = signal(new Set(), ...(ngDevMode ? [{ debugName: "selectedSampleIndices" }] : []));
3229
3284
  showSamplePanel = signal(false, ...(ngDevMode ? [{ debugName: "showSamplePanel" }] : []));
3230
- constructor(fb, activeModal, authService, favouriteMetadataOptionService, metadataColumnService, metadataColumnTemplateService, labGroupService) {
3285
+ constructor(fb, activeModal, authService, favouriteMetadataOptionService, metadataColumnService, metadataColumnTemplateService, labGroupService, ontologySearchService) {
3231
3286
  this.fb = fb;
3232
3287
  this.activeModal = activeModal;
3233
3288
  this.authService = authService;
@@ -3235,6 +3290,7 @@ class MetadataValueEditModal {
3235
3290
  this.metadataColumnService = metadataColumnService;
3236
3291
  this.metadataColumnTemplateService = metadataColumnTemplateService;
3237
3292
  this.labGroupService = labGroupService;
3293
+ this.ontologySearchService = ontologySearchService;
3238
3294
  this.editForm = this.fb.group({
3239
3295
  value: ['', [Validators.maxLength(500)]]
3240
3296
  });
@@ -3319,6 +3375,19 @@ class MetadataValueEditModal {
3319
3375
  return of([]);
3320
3376
  }), tap$1(() => this.isLoadingSuggestions.set(false)));
3321
3377
  }
3378
+ else if (this.config.ontologyType) {
3379
+ // Fallback: use OntologySearchService directly with custom filters
3380
+ return this.ontologySearchService.suggest({
3381
+ q: term,
3382
+ type: this.config.ontologyType,
3383
+ match: this.searchType() === 'icontains' ? 'contains' : 'startswith',
3384
+ customFilters: this.config.customOntologyFilters,
3385
+ limit: 10
3386
+ }).pipe(map$1(response => response.suggestions || []), catchError(error => {
3387
+ console.error('Error getting ontology suggestions:', error);
3388
+ return of([]);
3389
+ }), tap$1(() => this.isLoadingSuggestions.set(false)));
3390
+ }
3322
3391
  else {
3323
3392
  this.isLoadingSuggestions.set(false);
3324
3393
  return of([]);
@@ -3647,7 +3716,7 @@ class MetadataValueEditModal {
3647
3716
  }
3648
3717
  }
3649
3718
  }
3650
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.3", ngImport: i0, type: MetadataValueEditModal, deps: [{ token: i1.FormBuilder }, { token: i2.NgbActiveModal }, { token: i3.AuthService }, { token: FavouriteMetadataOptionService }, { token: MetadataColumnService }, { token: MetadataColumnTemplateService }, { token: i3.LabGroupService }], target: i0.ɵɵFactoryTarget.Component });
3719
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.3", ngImport: i0, type: MetadataValueEditModal, deps: [{ token: i1.FormBuilder }, { token: i2.NgbActiveModal }, { token: i3.AuthService }, { token: FavouriteMetadataOptionService }, { token: MetadataColumnService }, { token: MetadataColumnTemplateService }, { token: i3.LabGroupService }, { token: OntologySearchService }], target: i0.ɵɵFactoryTarget.Component });
3651
3720
  static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.3", type: MetadataValueEditModal, isStandalone: true, selector: "ccv-metadata-value-edit-modal", inputs: { config: "config" }, outputs: { valueSaved: "valueSaved" }, ngImport: i0, template: "<div class=\"modal-header\">\n <h4 class=\"modal-title\">\n <i class=\"bi bi-pencil-square me-2\"></i>{{ title }}\n </h4>\n <button type=\"button\" class=\"btn-close\" (click)=\"onCancel()\"></button>\n</div>\n\n<div class=\"modal-body\">\n <div class=\"row\">\n <!-- Main Content -->\n <div [class]=\"hasMultiSampleEdit ? 'col-md-8' : 'col-12'\">\n <form [formGroup]=\"editForm\" (ngSubmit)=\"onSubmit()\">\n\n <!-- Column Information -->\n <div class=\"mb-3\">\n <div class=\"row\">\n <div class=\"col-md-6\">\n <small class=\"text-muted\">Column Name:</small>\n <div class=\"fw-semibold\">{{ config.columnName }}</div>\n </div>\n <div class=\"col-md-6\">\n <small class=\"text-muted\">Column Type:</small>\n <div><span class=\"badge bg-light text-dark\">{{ config.columnType }}</span></div>\n </div>\n </div>\n @if (hasOntologyType) {\n <div class=\"mt-2\">\n <small class=\"text-muted\">Ontology Type:</small>\n <div><span class=\"badge bg-info\">{{ ontologyTypeLabel }}</span></div>\n </div>\n }\n </div>\n\n <hr class=\"mb-4\">\n\n <!-- Favorite Options Section -->\n @if (hasFavorites && !isLoadingFavorites()) {\n <div class=\"mb-4\">\n <h6 class=\"text-muted mb-3\">\n <i class=\"bi bi-heart-fill me-2 text-danger\"></i>Recommended Values\n </h6>\n\n <!-- Favorites Navigation Tabs -->\n <ul ngbNav #favoritesNav=\"ngbNav\" [(activeId)]=\"activeFavoritesTab\" class=\"nav-tabs\">\n @if (userFavorites().length > 0) {\n <li [ngbNavItem]=\"'user'\">\n <button ngbNavLink>\n <i class=\"bi bi-person-fill me-1\"></i>Your Favorites\n <span class=\"badge bg-primary ms-1\">{{ userFavorites().length }}</span>\n </button>\n <ng-template ngbNavContent>\n <div class=\"mt-3\">\n <select \n class=\"form-select\" \n size=\"5\" \n (change)=\"onFavoriteSelected($event, userFavorites())\">\n <option value=\"\">Select a favorite value...</option>\n @for (favorite of userFavorites(); track favorite.id) {\n <option [value]=\"favorite.id\">\n {{ getFavoriteDisplayValue(favorite) }}\n @if (hasCustomDisplayValue(favorite)) { (Custom Display) }\n </option>\n }\n </select>\n <div class=\"form-text\">\n <i class=\"bi bi-info-circle me-1\"></i>Your personal favorite values for this column\n </div>\n </div>\n </ng-template>\n </li>\n }\n\n @if (labGroupFavorites().length > 0) {\n <li [ngbNavItem]=\"'lab_group'\">\n <button ngbNavLink>\n <i class=\"bi bi-people-fill me-1\"></i>Lab Group\n <span class=\"badge bg-info ms-1\">{{ labGroupFavorites().length }}</span>\n </button>\n <ng-template ngbNavContent>\n <div class=\"mt-3\">\n <select \n class=\"form-select\" \n size=\"5\" \n (change)=\"onFavoriteSelected($event, labGroupFavorites())\">\n <option value=\"\">Select a favorite value...</option>\n @for (favorite of labGroupFavorites(); track favorite.id) {\n <option [value]=\"favorite.id\">\n {{ getFavoriteDisplayValue(favorite) }}\n @if (hasCustomDisplayValue(favorite)) { (Custom Display) }\n </option>\n }\n </select>\n <div class=\"form-text\">\n <i class=\"bi bi-info-circle me-1\"></i>Favorite values shared by your lab group\n </div>\n </div>\n </ng-template>\n </li>\n }\n\n @if (globalFavorites().length > 0) {\n <li [ngbNavItem]=\"'global'\">\n <button ngbNavLink>\n <i class=\"bi bi-globe me-1\"></i>Global\n <span class=\"badge bg-success ms-1\">{{ globalFavorites().length }}</span>\n </button>\n <ng-template ngbNavContent>\n <div class=\"mt-3\">\n <select \n class=\"form-select\" \n size=\"5\" \n (change)=\"onFavoriteSelected($event, globalFavorites())\">\n <option value=\"\">Select a favorite value...</option>\n @for (favorite of globalFavorites(); track favorite.id) {\n <option [value]=\"favorite.id\">\n {{ getFavoriteDisplayValue(favorite) }}\n @if (hasCustomDisplayValue(favorite)) { (Custom Display) }\n </option>\n }\n </select>\n <div class=\"form-text\">\n <i class=\"bi bi-info-circle me-1\"></i>Globally recommended values for this column\n </div>\n </div>\n </ng-template>\n </li>\n }\n </ul>\n\n <div [ngbNavOutlet]=\"favoritesNav\" class=\"mt-3\"></div>\n\n <hr class=\"my-3\">\n </div>\n }\n\n @if (isLoadingFavorites()) {\n <div class=\"text-center mb-4\">\n <div class=\"spinner-border spinner-border-sm text-primary me-2\"></div>\n <small class=\"text-muted\">Loading favorite options...</small>\n </div>\n <hr class=\"my-3\">\n }\n\n <!-- Value Input Section -->\n <div class=\"mb-3\">\n <div class=\"d-flex justify-content-between align-items-center mb-2\">\n <label for=\"metadataValue\" class=\"form-label mb-0\">\n Metadata Value\n @if (hasOntologyType) {\n <span class=\"badge bg-success ms-2\">\n <i class=\"bi bi-search me-1\"></i>Autocomplete Available\n </span>\n }\n @if (specialSyntaxType()) {\n <span class=\"badge bg-info ms-2\">\n <i class=\"bi bi-gear me-1\"></i>{{ getSpecialInputTitle() }}\n </span>\n }\n </label>\n\n @if (specialSyntaxType()) {\n <div class=\"form-check form-switch\">\n <input\n class=\"form-check-input\"\n type=\"checkbox\"\n id=\"specialInputToggle\"\n [checked]=\"showSpecialInput()\"\n (change)=\"toggleSpecialInput()\">\n <label class=\"form-check-label small text-muted\" for=\"specialInputToggle\">\n Enhanced Editor\n </label>\n </div>\n }\n </div>\n\n <!-- Special SDRF Syntax Input Components -->\n @if (showSpecialInput() && specialSyntaxType()) {\n <div class=\"border rounded p-3 mb-3 bg-body-secondary\">\n <div class=\"mb-2\">\n <small class=\"text-muted fw-semibold\">\n <i class=\"bi bi-gear me-1\"></i>{{ getSpecialInputTitle() }}\n </small>\n </div>\n\n @switch (specialSyntaxType()) {\n @case ('age') {\n <ccv-sdrf-age-input\n [value]=\"specialInputValue()\"\n (valueChange)=\"onSpecialInputChange($event)\">\n </ccv-sdrf-age-input>\n }\n @case ('modification') {\n <ccv-sdrf-modification-input\n [value]=\"specialInputValue()\"\n [columnId]=\"config.columnId ?? null\"\n [templateId]=\"config.templateId ?? null\"\n (valueChange)=\"onSpecialInputChange($event)\">\n </ccv-sdrf-modification-input>\n }\n @case ('cleavage') {\n <ccv-sdrf-cleavage-input\n [value]=\"specialInputValue()\"\n [columnId]=\"config.columnId ?? null\"\n [templateId]=\"config.templateId ?? null\"\n (valueChange)=\"onSpecialInputChange($event)\">\n </ccv-sdrf-cleavage-input>\n }\n @case ('spiked_compound') {\n <ccv-sdrf-spiked-compound-input\n [value]=\"specialInputValue()\"\n (valueChange)=\"onSpecialInputChange($event)\">\n </ccv-sdrf-spiked-compound-input>\n }\n }\n\n <div class=\"form-text mt-2\">\n <i class=\"bi bi-info-circle me-1\"></i>\n Use the enhanced editor for structured input, or toggle off for manual text entry.\n </div>\n </div>\n }\n\n @if (hasOntologyType && !showSpecialInput()) {\n <!-- Search type selection for ontology -->\n <div class=\"mb-2\">\n <small class=\"text-muted d-block mb-1\">Search Mode:</small>\n <div class=\"btn-group btn-group-sm\" role=\"group\">\n <button\n type=\"button\"\n class=\"btn\"\n [class]=\"searchType() === 'icontains' ? 'btn-primary' : 'btn-outline-primary'\"\n (click)=\"onSearchTypeChange('icontains')\">\n <i class=\"bi bi-search me-1\"></i>Contains\n </button>\n <button\n type=\"button\"\n class=\"btn\"\n [class]=\"searchType() === 'istartswith' ? 'btn-primary' : 'btn-outline-primary'\"\n (click)=\"onSearchTypeChange('istartswith')\">\n <i class=\"bi bi-arrow-right me-1\"></i>Starts with\n </button>\n </div>\n </div>\n\n <!-- Typeahead input for ontology suggestions -->\n <div class=\"input-group\">\n <input\n type=\"text\"\n class=\"form-control\"\n [class.border-success]=\"selectedFavorite() !== null\"\n id=\"metadataValue\"\n formControlName=\"value\"\n placeholder=\"Type to search ontology suggestions...\"\n [ngbTypeahead]=\"searchOntology\"\n [resultFormatter]=\"formatSuggestion\"\n [inputFormatter]=\"inputFormatter\"\n (selectItem)=\"onSuggestionSelected($event)\"\n [editable]=\"true\"\n [showHint]=\"true\"\n [focusFirst]=\"false\"\n container=\"body\">\n @if (isLoadingSuggestions()) {\n <span class=\"input-group-text\">\n <div class=\"spinner-border spinner-border-sm\" role=\"status\">\n <span class=\"visually-hidden\">Loading...</span>\n </div>\n </span>\n } @else {\n <span class=\"input-group-text\">\n <i class=\"bi bi-search text-muted\"></i>\n </span>\n }\n <button\n type=\"button\"\n class=\"btn btn-outline-secondary\"\n (click)=\"clearValue()\"\n title=\"Clear value\">\n <i class=\"bi bi-x\"></i>\n </button>\n </div>\n <div class=\"form-text\">\n <i class=\"bi bi-info-circle me-1\"></i>\n Search {{ searchType() === 'icontains' ? 'contains' : 'starts with' }} for {{ ontologyTypeLabel }} ontology\n </div>\n } @else if (!showSpecialInput()) {\n <!-- Regular input when no ontology type or special input is disabled -->\n <div class=\"input-group\">\n <input\n type=\"text\"\n class=\"form-control\"\n [class.border-success]=\"selectedFavorite() !== null\"\n id=\"metadataValue\"\n formControlName=\"value\"\n placeholder=\"Enter metadata value\">\n <button\n type=\"button\"\n class=\"btn btn-outline-secondary\"\n (click)=\"clearValue()\"\n title=\"Clear value\">\n <i class=\"bi bi-x\"></i>\n </button>\n </div>\n <div class=\"form-text text-muted\">\n @if (specialSyntaxType()) {\n Manual text entry mode. Use the Enhanced Editor toggle for structured {{ specialSyntaxType() }} input.\n } @else {\n Enter the value manually (no ontology suggestions available for this column type)\n }\n </div>\n }\n </div>\n\n <!-- Character count and value info -->\n <div class=\"mb-3\">\n <div class=\"d-flex justify-content-between align-items-center\">\n <small class=\"text-muted\">\n Characters: {{ editForm.get('value')?.value?.length || 0 }}/500\n </small>\n @if (getSelectedFavoriteInfo()) {\n <small class=\"text-info\">\n <i class=\"bi bi-info-circle me-1\"></i>{{ getSelectedFavoriteInfo() }}\n </small>\n }\n </div>\n </div>\n\n </form>\n </div>\n\n <!-- Sample Selection Panel -->\n @if (hasMultiSampleEdit) {\n <div class=\"col-md-4\">\n <div class=\"border-start ps-3\">\n <div class=\"d-flex justify-content-between align-items-center mb-3\">\n <h6 class=\"mb-0 text-muted\">\n <i class=\"bi bi-list-check me-1\"></i>Apply to Samples\n </h6>\n <span class=\"badge bg-primary\">\n {{ selectedSampleCount }}/{{ totalSampleCount }}\n </span>\n </div>\n\n <!-- Select All/Clear All Controls -->\n <div class=\"d-flex gap-1 mb-3\">\n <button \n type=\"button\" \n class=\"btn btn-outline-primary btn-sm flex-fill\"\n (click)=\"selectAllSamples()\"\n [disabled]=\"selectedSampleCount === totalSampleCount\">\n <i class=\"bi bi-check-all me-1\"></i>All\n </button>\n <button \n type=\"button\" \n class=\"btn btn-outline-secondary btn-sm flex-fill\"\n (click)=\"clearSampleSelection()\"\n [disabled]=\"selectedSampleCount === 0\">\n <i class=\"bi bi-x me-1\"></i>Clear\n </button>\n </div>\n\n <!-- Sample List -->\n <div class=\"sample-list\" style=\"max-height: 400px; overflow-y: auto;\">\n @for (sample of sampleData; track sample.index) {\n <div class=\"form-check d-flex align-items-center mb-2 p-2 rounded border-bottom\">\n <input \n class=\"form-check-input me-2\" \n type=\"checkbox\" \n [id]=\"'sample-' + sample.index\"\n [checked]=\"isSampleSelected(sample.index)\"\n (change)=\"toggleSampleSelection(sample.index)\">\n <label \n [for]=\"'sample-' + sample.index\" \n class=\"form-check-label flex-grow-1 cursor-pointer\">\n <div class=\"d-flex justify-content-between align-items-center\">\n <div>\n <div class=\"fw-semibold\">Sample {{ sample.index }}</div>\n @if (sample.sourceName) {\n <small class=\"text-muted\">{{ sample.sourceName }}</small>\n }\n </div>\n <div class=\"text-end\">\n <small class=\"text-muted\">Current:</small>\n <div class=\"small fw-mono\" [title]=\"sample.value || ''\">\n {{ (sample.value && sample.value.length > 15) ? (sample.value | slice:0:15) + '...' : (sample.value || '-') }}\n </div>\n </div>\n </div>\n </label>\n </div>\n }\n </div>\n\n @if (selectedSampleCount > 0) {\n <div class=\"alert alert-info mt-3 p-2\">\n <small>\n <i class=\"bi bi-info-circle me-1\"></i>\n The value will be applied to {{ selectedSampleCount }} selected sample{{ selectedSampleCount !== 1 ? 's' : '' }}.\n </small>\n </div>\n }\n </div>\n </div>\n }\n </div>\n</div>\n\n<div class=\"modal-footer\">\n <div class=\"me-auto\">\n @if (editForm.get('value')?.value && !isLoading() && !isFromFavoriteManagement) {\n <div class=\"btn-group dropup\" ngbDropdown>\n <button\n type=\"button\"\n class=\"btn btn-outline-warning btn-sm dropdown-toggle\"\n ngbDropdownToggle>\n <i class=\"bi bi-heart me-1\"></i>Add to Favorites\n </button>\n <div class=\"dropdown-menu\" ngbDropdownMenu>\n <button\n type=\"button\"\n class=\"dropdown-item\"\n (click)=\"addToFavorites('user')\">\n <i class=\"bi bi-person me-2\"></i>Personal Favorite\n </button>\n <button\n type=\"button\"\n class=\"dropdown-item\"\n (click)=\"addToFavorites('lab_group')\">\n <i class=\"bi bi-people me-2\"></i>Lab Group Favorite\n </button>\n <button\n type=\"button\"\n class=\"dropdown-item\"\n (click)=\"addToFavorites('global')\">\n <i class=\"bi bi-globe me-2\"></i>Global Recommendation\n </button>\n </div>\n </div>\n }\n </div>\n\n <div class=\"d-flex gap-2\">\n <button type=\"button\" class=\"btn btn-outline-secondary\" (click)=\"onCancel()\">\n <i class=\"bi bi-x-circle me-1\"></i>Cancel\n </button>\n <button\n type=\"button\"\n class=\"btn btn-primary\"\n (click)=\"onSubmit()\"\n [disabled]=\"editForm.invalid || isLoading()\">\n @if (isLoading()) {\n <span class=\"spinner-border spinner-border-sm me-2\"></span>\n }\n <i class=\"bi bi-check-circle me-1\"></i>\n Save Value\n </button>\n </div>\n</div>\n", styles: [".sample-list .form-check{border:1px solid transparent;transition:all .2s ease}.sample-list .form-check:hover{background-color:rgba(var(--bs-primary-rgb),.05);border-color:rgba(var(--bs-primary-rgb),.2)}.sample-list .form-check .form-check-input:checked~.form-check-label .fw-semibold{color:var(--bs-primary)}.cursor-pointer{cursor:pointer}.fw-mono{font-family:var(--bs-font-monospace)}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i1.FormControlName, selector: "[formControlName]", inputs: ["formControlName", "disabled", "ngModel"], outputs: ["ngModelChange"] }, { kind: "ngmodule", type: NgbModule }, { kind: "directive", type: i2.NgbDropdown, selector: "[ngbDropdown]", inputs: ["autoClose", "dropdownClass", "open", "placement", "popperOptions", "container", "display"], outputs: ["openChange"], exportAs: ["ngbDropdown"] }, { kind: "directive", type: i2.NgbDropdownToggle, selector: "[ngbDropdownToggle]" }, { kind: "directive", type: i2.NgbDropdownMenu, selector: "[ngbDropdownMenu]" }, { kind: "directive", type: i2.NgbNavContent, selector: "ng-template[ngbNavContent]" }, { kind: "directive", type: i2.NgbNav, selector: "[ngbNav]", inputs: ["activeId", "animation", "destroyOnHide", "orientation", "roles", "keyboard"], outputs: ["activeIdChange", "shown", "hidden", "navChange"], exportAs: ["ngbNav"] }, { kind: "directive", type: i2.NgbNavItem, selector: "[ngbNavItem]", inputs: ["destroyOnHide", "disabled", "domId", "ngbNavItem"], outputs: ["shown", "hidden"], exportAs: ["ngbNavItem"] }, { kind: "directive", type: i2.NgbNavItemRole, selector: "[ngbNavItem]:not(ng-container)" }, { kind: "directive", type: i2.NgbNavLinkButton, selector: "button[ngbNavLink]" }, { kind: "directive", type: i2.NgbNavLinkBase, selector: "[ngbNavLink]" }, { kind: "component", type: i2.NgbNavOutlet, selector: "[ngbNavOutlet]", inputs: ["paneRole", "ngbNavOutlet"] }, { kind: "directive", type: i2.NgbTypeahead, selector: "input[ngbTypeahead]", inputs: ["autocomplete", "container", "editable", "focusFirst", "inputFormatter", "ngbTypeahead", "resultFormatter", "resultTemplate", "selectOnExact", "showHint", "placement", "popperOptions", "popupClass"], outputs: ["selectItem"], exportAs: ["ngbTypeahead"] }, { kind: "ngmodule", type: NgbNavModule }, { kind: "component", type: SdrfAgeInput, selector: "ccv-sdrf-age-input", inputs: ["value"], outputs: ["valueChange"] }, { kind: "component", type: SdrfModificationInput, selector: "ccv-sdrf-modification-input", inputs: ["value", "templateId", "columnId"], outputs: ["valueChange"] }, { kind: "component", type: SdrfCleavageInput, selector: "ccv-sdrf-cleavage-input", inputs: ["value", "templateId", "columnId"], outputs: ["valueChange"] }, { kind: "component", type: SdrfSpikedCompoundInput, selector: "ccv-sdrf-spiked-compound-input", inputs: ["value"], outputs: ["valueChange"] }, { kind: "pipe", type: i5.SlicePipe, name: "slice" }] });
3652
3721
  }
3653
3722
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.3", ngImport: i0, type: MetadataValueEditModal, decorators: [{
@@ -3662,7 +3731,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.3", ngImpor
3662
3731
  SdrfCleavageInput,
3663
3732
  SdrfSpikedCompoundInput
3664
3733
  ], template: "<div class=\"modal-header\">\n <h4 class=\"modal-title\">\n <i class=\"bi bi-pencil-square me-2\"></i>{{ title }}\n </h4>\n <button type=\"button\" class=\"btn-close\" (click)=\"onCancel()\"></button>\n</div>\n\n<div class=\"modal-body\">\n <div class=\"row\">\n <!-- Main Content -->\n <div [class]=\"hasMultiSampleEdit ? 'col-md-8' : 'col-12'\">\n <form [formGroup]=\"editForm\" (ngSubmit)=\"onSubmit()\">\n\n <!-- Column Information -->\n <div class=\"mb-3\">\n <div class=\"row\">\n <div class=\"col-md-6\">\n <small class=\"text-muted\">Column Name:</small>\n <div class=\"fw-semibold\">{{ config.columnName }}</div>\n </div>\n <div class=\"col-md-6\">\n <small class=\"text-muted\">Column Type:</small>\n <div><span class=\"badge bg-light text-dark\">{{ config.columnType }}</span></div>\n </div>\n </div>\n @if (hasOntologyType) {\n <div class=\"mt-2\">\n <small class=\"text-muted\">Ontology Type:</small>\n <div><span class=\"badge bg-info\">{{ ontologyTypeLabel }}</span></div>\n </div>\n }\n </div>\n\n <hr class=\"mb-4\">\n\n <!-- Favorite Options Section -->\n @if (hasFavorites && !isLoadingFavorites()) {\n <div class=\"mb-4\">\n <h6 class=\"text-muted mb-3\">\n <i class=\"bi bi-heart-fill me-2 text-danger\"></i>Recommended Values\n </h6>\n\n <!-- Favorites Navigation Tabs -->\n <ul ngbNav #favoritesNav=\"ngbNav\" [(activeId)]=\"activeFavoritesTab\" class=\"nav-tabs\">\n @if (userFavorites().length > 0) {\n <li [ngbNavItem]=\"'user'\">\n <button ngbNavLink>\n <i class=\"bi bi-person-fill me-1\"></i>Your Favorites\n <span class=\"badge bg-primary ms-1\">{{ userFavorites().length }}</span>\n </button>\n <ng-template ngbNavContent>\n <div class=\"mt-3\">\n <select \n class=\"form-select\" \n size=\"5\" \n (change)=\"onFavoriteSelected($event, userFavorites())\">\n <option value=\"\">Select a favorite value...</option>\n @for (favorite of userFavorites(); track favorite.id) {\n <option [value]=\"favorite.id\">\n {{ getFavoriteDisplayValue(favorite) }}\n @if (hasCustomDisplayValue(favorite)) { (Custom Display) }\n </option>\n }\n </select>\n <div class=\"form-text\">\n <i class=\"bi bi-info-circle me-1\"></i>Your personal favorite values for this column\n </div>\n </div>\n </ng-template>\n </li>\n }\n\n @if (labGroupFavorites().length > 0) {\n <li [ngbNavItem]=\"'lab_group'\">\n <button ngbNavLink>\n <i class=\"bi bi-people-fill me-1\"></i>Lab Group\n <span class=\"badge bg-info ms-1\">{{ labGroupFavorites().length }}</span>\n </button>\n <ng-template ngbNavContent>\n <div class=\"mt-3\">\n <select \n class=\"form-select\" \n size=\"5\" \n (change)=\"onFavoriteSelected($event, labGroupFavorites())\">\n <option value=\"\">Select a favorite value...</option>\n @for (favorite of labGroupFavorites(); track favorite.id) {\n <option [value]=\"favorite.id\">\n {{ getFavoriteDisplayValue(favorite) }}\n @if (hasCustomDisplayValue(favorite)) { (Custom Display) }\n </option>\n }\n </select>\n <div class=\"form-text\">\n <i class=\"bi bi-info-circle me-1\"></i>Favorite values shared by your lab group\n </div>\n </div>\n </ng-template>\n </li>\n }\n\n @if (globalFavorites().length > 0) {\n <li [ngbNavItem]=\"'global'\">\n <button ngbNavLink>\n <i class=\"bi bi-globe me-1\"></i>Global\n <span class=\"badge bg-success ms-1\">{{ globalFavorites().length }}</span>\n </button>\n <ng-template ngbNavContent>\n <div class=\"mt-3\">\n <select \n class=\"form-select\" \n size=\"5\" \n (change)=\"onFavoriteSelected($event, globalFavorites())\">\n <option value=\"\">Select a favorite value...</option>\n @for (favorite of globalFavorites(); track favorite.id) {\n <option [value]=\"favorite.id\">\n {{ getFavoriteDisplayValue(favorite) }}\n @if (hasCustomDisplayValue(favorite)) { (Custom Display) }\n </option>\n }\n </select>\n <div class=\"form-text\">\n <i class=\"bi bi-info-circle me-1\"></i>Globally recommended values for this column\n </div>\n </div>\n </ng-template>\n </li>\n }\n </ul>\n\n <div [ngbNavOutlet]=\"favoritesNav\" class=\"mt-3\"></div>\n\n <hr class=\"my-3\">\n </div>\n }\n\n @if (isLoadingFavorites()) {\n <div class=\"text-center mb-4\">\n <div class=\"spinner-border spinner-border-sm text-primary me-2\"></div>\n <small class=\"text-muted\">Loading favorite options...</small>\n </div>\n <hr class=\"my-3\">\n }\n\n <!-- Value Input Section -->\n <div class=\"mb-3\">\n <div class=\"d-flex justify-content-between align-items-center mb-2\">\n <label for=\"metadataValue\" class=\"form-label mb-0\">\n Metadata Value\n @if (hasOntologyType) {\n <span class=\"badge bg-success ms-2\">\n <i class=\"bi bi-search me-1\"></i>Autocomplete Available\n </span>\n }\n @if (specialSyntaxType()) {\n <span class=\"badge bg-info ms-2\">\n <i class=\"bi bi-gear me-1\"></i>{{ getSpecialInputTitle() }}\n </span>\n }\n </label>\n\n @if (specialSyntaxType()) {\n <div class=\"form-check form-switch\">\n <input\n class=\"form-check-input\"\n type=\"checkbox\"\n id=\"specialInputToggle\"\n [checked]=\"showSpecialInput()\"\n (change)=\"toggleSpecialInput()\">\n <label class=\"form-check-label small text-muted\" for=\"specialInputToggle\">\n Enhanced Editor\n </label>\n </div>\n }\n </div>\n\n <!-- Special SDRF Syntax Input Components -->\n @if (showSpecialInput() && specialSyntaxType()) {\n <div class=\"border rounded p-3 mb-3 bg-body-secondary\">\n <div class=\"mb-2\">\n <small class=\"text-muted fw-semibold\">\n <i class=\"bi bi-gear me-1\"></i>{{ getSpecialInputTitle() }}\n </small>\n </div>\n\n @switch (specialSyntaxType()) {\n @case ('age') {\n <ccv-sdrf-age-input\n [value]=\"specialInputValue()\"\n (valueChange)=\"onSpecialInputChange($event)\">\n </ccv-sdrf-age-input>\n }\n @case ('modification') {\n <ccv-sdrf-modification-input\n [value]=\"specialInputValue()\"\n [columnId]=\"config.columnId ?? null\"\n [templateId]=\"config.templateId ?? null\"\n (valueChange)=\"onSpecialInputChange($event)\">\n </ccv-sdrf-modification-input>\n }\n @case ('cleavage') {\n <ccv-sdrf-cleavage-input\n [value]=\"specialInputValue()\"\n [columnId]=\"config.columnId ?? null\"\n [templateId]=\"config.templateId ?? null\"\n (valueChange)=\"onSpecialInputChange($event)\">\n </ccv-sdrf-cleavage-input>\n }\n @case ('spiked_compound') {\n <ccv-sdrf-spiked-compound-input\n [value]=\"specialInputValue()\"\n (valueChange)=\"onSpecialInputChange($event)\">\n </ccv-sdrf-spiked-compound-input>\n }\n }\n\n <div class=\"form-text mt-2\">\n <i class=\"bi bi-info-circle me-1\"></i>\n Use the enhanced editor for structured input, or toggle off for manual text entry.\n </div>\n </div>\n }\n\n @if (hasOntologyType && !showSpecialInput()) {\n <!-- Search type selection for ontology -->\n <div class=\"mb-2\">\n <small class=\"text-muted d-block mb-1\">Search Mode:</small>\n <div class=\"btn-group btn-group-sm\" role=\"group\">\n <button\n type=\"button\"\n class=\"btn\"\n [class]=\"searchType() === 'icontains' ? 'btn-primary' : 'btn-outline-primary'\"\n (click)=\"onSearchTypeChange('icontains')\">\n <i class=\"bi bi-search me-1\"></i>Contains\n </button>\n <button\n type=\"button\"\n class=\"btn\"\n [class]=\"searchType() === 'istartswith' ? 'btn-primary' : 'btn-outline-primary'\"\n (click)=\"onSearchTypeChange('istartswith')\">\n <i class=\"bi bi-arrow-right me-1\"></i>Starts with\n </button>\n </div>\n </div>\n\n <!-- Typeahead input for ontology suggestions -->\n <div class=\"input-group\">\n <input\n type=\"text\"\n class=\"form-control\"\n [class.border-success]=\"selectedFavorite() !== null\"\n id=\"metadataValue\"\n formControlName=\"value\"\n placeholder=\"Type to search ontology suggestions...\"\n [ngbTypeahead]=\"searchOntology\"\n [resultFormatter]=\"formatSuggestion\"\n [inputFormatter]=\"inputFormatter\"\n (selectItem)=\"onSuggestionSelected($event)\"\n [editable]=\"true\"\n [showHint]=\"true\"\n [focusFirst]=\"false\"\n container=\"body\">\n @if (isLoadingSuggestions()) {\n <span class=\"input-group-text\">\n <div class=\"spinner-border spinner-border-sm\" role=\"status\">\n <span class=\"visually-hidden\">Loading...</span>\n </div>\n </span>\n } @else {\n <span class=\"input-group-text\">\n <i class=\"bi bi-search text-muted\"></i>\n </span>\n }\n <button\n type=\"button\"\n class=\"btn btn-outline-secondary\"\n (click)=\"clearValue()\"\n title=\"Clear value\">\n <i class=\"bi bi-x\"></i>\n </button>\n </div>\n <div class=\"form-text\">\n <i class=\"bi bi-info-circle me-1\"></i>\n Search {{ searchType() === 'icontains' ? 'contains' : 'starts with' }} for {{ ontologyTypeLabel }} ontology\n </div>\n } @else if (!showSpecialInput()) {\n <!-- Regular input when no ontology type or special input is disabled -->\n <div class=\"input-group\">\n <input\n type=\"text\"\n class=\"form-control\"\n [class.border-success]=\"selectedFavorite() !== null\"\n id=\"metadataValue\"\n formControlName=\"value\"\n placeholder=\"Enter metadata value\">\n <button\n type=\"button\"\n class=\"btn btn-outline-secondary\"\n (click)=\"clearValue()\"\n title=\"Clear value\">\n <i class=\"bi bi-x\"></i>\n </button>\n </div>\n <div class=\"form-text text-muted\">\n @if (specialSyntaxType()) {\n Manual text entry mode. Use the Enhanced Editor toggle for structured {{ specialSyntaxType() }} input.\n } @else {\n Enter the value manually (no ontology suggestions available for this column type)\n }\n </div>\n }\n </div>\n\n <!-- Character count and value info -->\n <div class=\"mb-3\">\n <div class=\"d-flex justify-content-between align-items-center\">\n <small class=\"text-muted\">\n Characters: {{ editForm.get('value')?.value?.length || 0 }}/500\n </small>\n @if (getSelectedFavoriteInfo()) {\n <small class=\"text-info\">\n <i class=\"bi bi-info-circle me-1\"></i>{{ getSelectedFavoriteInfo() }}\n </small>\n }\n </div>\n </div>\n\n </form>\n </div>\n\n <!-- Sample Selection Panel -->\n @if (hasMultiSampleEdit) {\n <div class=\"col-md-4\">\n <div class=\"border-start ps-3\">\n <div class=\"d-flex justify-content-between align-items-center mb-3\">\n <h6 class=\"mb-0 text-muted\">\n <i class=\"bi bi-list-check me-1\"></i>Apply to Samples\n </h6>\n <span class=\"badge bg-primary\">\n {{ selectedSampleCount }}/{{ totalSampleCount }}\n </span>\n </div>\n\n <!-- Select All/Clear All Controls -->\n <div class=\"d-flex gap-1 mb-3\">\n <button \n type=\"button\" \n class=\"btn btn-outline-primary btn-sm flex-fill\"\n (click)=\"selectAllSamples()\"\n [disabled]=\"selectedSampleCount === totalSampleCount\">\n <i class=\"bi bi-check-all me-1\"></i>All\n </button>\n <button \n type=\"button\" \n class=\"btn btn-outline-secondary btn-sm flex-fill\"\n (click)=\"clearSampleSelection()\"\n [disabled]=\"selectedSampleCount === 0\">\n <i class=\"bi bi-x me-1\"></i>Clear\n </button>\n </div>\n\n <!-- Sample List -->\n <div class=\"sample-list\" style=\"max-height: 400px; overflow-y: auto;\">\n @for (sample of sampleData; track sample.index) {\n <div class=\"form-check d-flex align-items-center mb-2 p-2 rounded border-bottom\">\n <input \n class=\"form-check-input me-2\" \n type=\"checkbox\" \n [id]=\"'sample-' + sample.index\"\n [checked]=\"isSampleSelected(sample.index)\"\n (change)=\"toggleSampleSelection(sample.index)\">\n <label \n [for]=\"'sample-' + sample.index\" \n class=\"form-check-label flex-grow-1 cursor-pointer\">\n <div class=\"d-flex justify-content-between align-items-center\">\n <div>\n <div class=\"fw-semibold\">Sample {{ sample.index }}</div>\n @if (sample.sourceName) {\n <small class=\"text-muted\">{{ sample.sourceName }}</small>\n }\n </div>\n <div class=\"text-end\">\n <small class=\"text-muted\">Current:</small>\n <div class=\"small fw-mono\" [title]=\"sample.value || ''\">\n {{ (sample.value && sample.value.length > 15) ? (sample.value | slice:0:15) + '...' : (sample.value || '-') }}\n </div>\n </div>\n </div>\n </label>\n </div>\n }\n </div>\n\n @if (selectedSampleCount > 0) {\n <div class=\"alert alert-info mt-3 p-2\">\n <small>\n <i class=\"bi bi-info-circle me-1\"></i>\n The value will be applied to {{ selectedSampleCount }} selected sample{{ selectedSampleCount !== 1 ? 's' : '' }}.\n </small>\n </div>\n }\n </div>\n </div>\n }\n </div>\n</div>\n\n<div class=\"modal-footer\">\n <div class=\"me-auto\">\n @if (editForm.get('value')?.value && !isLoading() && !isFromFavoriteManagement) {\n <div class=\"btn-group dropup\" ngbDropdown>\n <button\n type=\"button\"\n class=\"btn btn-outline-warning btn-sm dropdown-toggle\"\n ngbDropdownToggle>\n <i class=\"bi bi-heart me-1\"></i>Add to Favorites\n </button>\n <div class=\"dropdown-menu\" ngbDropdownMenu>\n <button\n type=\"button\"\n class=\"dropdown-item\"\n (click)=\"addToFavorites('user')\">\n <i class=\"bi bi-person me-2\"></i>Personal Favorite\n </button>\n <button\n type=\"button\"\n class=\"dropdown-item\"\n (click)=\"addToFavorites('lab_group')\">\n <i class=\"bi bi-people me-2\"></i>Lab Group Favorite\n </button>\n <button\n type=\"button\"\n class=\"dropdown-item\"\n (click)=\"addToFavorites('global')\">\n <i class=\"bi bi-globe me-2\"></i>Global Recommendation\n </button>\n </div>\n </div>\n }\n </div>\n\n <div class=\"d-flex gap-2\">\n <button type=\"button\" class=\"btn btn-outline-secondary\" (click)=\"onCancel()\">\n <i class=\"bi bi-x-circle me-1\"></i>Cancel\n </button>\n <button\n type=\"button\"\n class=\"btn btn-primary\"\n (click)=\"onSubmit()\"\n [disabled]=\"editForm.invalid || isLoading()\">\n @if (isLoading()) {\n <span class=\"spinner-border spinner-border-sm me-2\"></span>\n }\n <i class=\"bi bi-check-circle me-1\"></i>\n Save Value\n </button>\n </div>\n</div>\n", styles: [".sample-list .form-check{border:1px solid transparent;transition:all .2s ease}.sample-list .form-check:hover{background-color:rgba(var(--bs-primary-rgb),.05);border-color:rgba(var(--bs-primary-rgb),.2)}.sample-list .form-check .form-check-input:checked~.form-check-label .fw-semibold{color:var(--bs-primary)}.cursor-pointer{cursor:pointer}.fw-mono{font-family:var(--bs-font-monospace)}\n"] }]
3665
- }], ctorParameters: () => [{ type: i1.FormBuilder }, { type: i2.NgbActiveModal }, { type: i3.AuthService }, { type: FavouriteMetadataOptionService }, { type: MetadataColumnService }, { type: MetadataColumnTemplateService }, { type: i3.LabGroupService }], propDecorators: { config: [{
3734
+ }], ctorParameters: () => [{ type: i1.FormBuilder }, { type: i2.NgbActiveModal }, { type: i3.AuthService }, { type: FavouriteMetadataOptionService }, { type: MetadataColumnService }, { type: MetadataColumnTemplateService }, { type: i3.LabGroupService }, { type: OntologySearchService }], propDecorators: { config: [{
3666
3735
  type: Input
3667
3736
  }], valueSaved: [{
3668
3737
  type: Output
@@ -4651,34 +4720,41 @@ class ColumnEditModal {
4651
4720
  return this.hasOntologyType && this.editForm.get('enableTypeahead')?.value === true;
4652
4721
  }
4653
4722
  openValueEditModal() {
4654
- // Get custom filters from selected ontology type or template
4655
- let customFilters = this.selectedTemplate?.customOntologyFilters;
4723
+ const ontologyTypeIndexStr = this.editForm.get('ontologyType')?.value;
4724
+ const ontologyTypeIndex = parseInt(ontologyTypeIndexStr || '0');
4725
+ const enableTypeahead = this.editForm.get('enableTypeahead')?.value || false;
4656
4726
  let ontologyType = '';
4657
- // If in advanced mode and an ontology type is selected, use its custom filters
4658
- if (this.showAdvancedMode() && this.selectedOntologyConfig) {
4659
- ontologyType = this.selectedOntologyConfig.value;
4660
- if (this.selectedOntologyConfig.customFilters) {
4661
- customFilters = this.selectedOntologyConfig.customFilters;
4662
- }
4727
+ if (ontologyTypeIndex > 0 && ontologyTypeIndex < this.ontologyTypes.length) {
4728
+ ontologyType = this.ontologyTypes[ontologyTypeIndex].value;
4663
4729
  }
4664
4730
  const config = {
4665
- columnId: this.column?.id,
4666
4731
  columnName: this.editForm.get('name')?.value || 'Column',
4667
4732
  columnType: this.editForm.get('type')?.value || '',
4668
4733
  ontologyType: ontologyType,
4669
- enableTypeahead: this.editForm.get('enableTypeahead')?.value || false,
4734
+ enableTypeahead: enableTypeahead,
4670
4735
  currentValue: this.editForm.get('value')?.value || '',
4671
- context: this.isEdit ? 'table' : 'template',
4672
- templateId: this.selectedTemplate?.id || this.templateId || undefined,
4673
- customOntologyFilters: customFilters
4736
+ context: this.isEdit ? 'table' : 'template'
4674
4737
  };
4738
+ const hasTemplate = this.selectedTemplate || this.column?.template || this.templateId;
4739
+ if (hasTemplate) {
4740
+ if (this.isEdit && this.column?.id) {
4741
+ config.columnId = this.column.id;
4742
+ }
4743
+ config.templateId = this.selectedTemplate?.id || this.column?.template || this.templateId || undefined;
4744
+ }
4745
+ else {
4746
+ const columnName = this.editForm.get('name')?.value;
4747
+ if (columnName) {
4748
+ const ontologyConfig = this.getOntologyConfigForColumn(columnName);
4749
+ config.customOntologyFilters = ontologyConfig.customFilters;
4750
+ }
4751
+ }
4675
4752
  const modalRef = this.modalService.open(MetadataValueEditModal, {
4676
4753
  size: 'lg',
4677
4754
  backdrop: 'static'
4678
4755
  });
4679
4756
  modalRef.componentInstance.config = config;
4680
4757
  modalRef.componentInstance.valueSaved.subscribe((result) => {
4681
- // Extract the actual value from the result
4682
4758
  const newValue = typeof result === 'string' ? result : result.value;
4683
4759
  this.editForm.get('value')?.setValue(newValue);
4684
4760
  this.editForm.get('value')?.markAsTouched();
@@ -4795,16 +4871,14 @@ class ColumnEditModal {
4795
4871
  // Enable controls to update values, then disable again
4796
4872
  this.editForm.get('name')?.enable();
4797
4873
  this.editForm.get('type')?.enable();
4798
- const ontologyTypeValue = this.getDefaultOntologyTypeForColumn(column.name);
4799
- const ontologyTypeIndex = ontologyTypeValue
4800
- ? this.ontologyTypes.findIndex(o => o.value === ontologyTypeValue)
4801
- : 0;
4874
+ const ontologyConfig = this.getOntologyConfigForColumn(column.name);
4875
+ const ontologyTypeIndex = ontologyConfig.index;
4802
4876
  this.editForm.patchValue({
4803
4877
  name: column.name,
4804
4878
  type: column.type,
4805
4879
  value: '',
4806
- ontologyType: ontologyTypeIndex >= 0 ? ontologyTypeIndex.toString() : '0',
4807
- enableTypeahead: this.shouldEnableTypeaheadForColumn(column.name),
4880
+ ontologyType: ontologyTypeIndex.toString(),
4881
+ enableTypeahead: ontologyTypeIndex > 0,
4808
4882
  mandatory: this.isColumnMandatory(column.name),
4809
4883
  hidden: false,
4810
4884
  readonly: false,
@@ -4825,41 +4899,89 @@ class ColumnEditModal {
4825
4899
  return !this.templates.some(template => template.columnName.toLowerCase() === officialCol.name.toLowerCase());
4826
4900
  });
4827
4901
  }
4828
- getDefaultOntologyTypeForColumn(columnName) {
4902
+ getOntologyConfigForColumn(columnName) {
4829
4903
  const lowerName = columnName.toLowerCase();
4830
- // Based on backend configure_ontology_options method - use exact matches
4904
+ const MSTermType = {
4905
+ INSTRUMENT: 'instrument',
4906
+ MASS_ANALYZER_TYPE: 'mass analyzer type',
4907
+ CLEAVAGE_AGENT: 'cleavage agent',
4908
+ SAMPLE_ATTRIBUTE: 'sample attribute',
4909
+ ANCESTRAL_CATEGORY: 'ancestral category',
4910
+ SEX: 'sex',
4911
+ DEVELOPMENTAL_STAGE: 'developmental stage'
4912
+ };
4913
+ let ontologyType = '';
4914
+ let customFilters = undefined;
4831
4915
  switch (lowerName) {
4832
4916
  case 'characteristics[organism]':
4833
- return 'species';
4917
+ ontologyType = 'species';
4918
+ break;
4834
4919
  case 'characteristics[disease]':
4835
- return 'human_disease';
4920
+ ontologyType = 'human_disease';
4921
+ break;
4836
4922
  case 'characteristics[organism part]':
4837
- return 'tissue';
4923
+ ontologyType = 'tissue';
4924
+ break;
4838
4925
  case 'characteristics[cell type]':
4839
- return 'cell_ontology';
4926
+ ontologyType = 'cell_ontology';
4927
+ break;
4840
4928
  case 'comment[instrument]':
4841
- return 'ms_unique_vocabularies';
4929
+ ontologyType = 'ms_unique_vocabularies';
4930
+ customFilters = { ms_unique_vocabularies: { term_type: MSTermType.INSTRUMENT } };
4931
+ break;
4842
4932
  case 'comment[modification parameters]':
4843
- return 'unimod';
4933
+ ontologyType = 'unimod';
4934
+ break;
4844
4935
  case 'comment[ms2 analyzer type]':
4845
- return 'ms_unique_vocabularies';
4936
+ ontologyType = 'ms_unique_vocabularies';
4937
+ customFilters = { ms_unique_vocabularies: { term_type: MSTermType.MASS_ANALYZER_TYPE } };
4938
+ break;
4846
4939
  case 'comment[cleavage agent details]':
4847
- return 'ms_unique_vocabularies';
4940
+ ontologyType = 'ms_unique_vocabularies';
4941
+ customFilters = { ms_unique_vocabularies: { term_type: MSTermType.CLEAVAGE_AGENT } };
4942
+ break;
4848
4943
  case 'characteristics[ancestry category]':
4849
- return 'ms_unique_vocabularies';
4944
+ ontologyType = 'ms_unique_vocabularies';
4945
+ customFilters = { ms_unique_vocabularies: { term_type: MSTermType.ANCESTRAL_CATEGORY } };
4946
+ break;
4850
4947
  case 'characteristics[sex]':
4851
- return 'ms_unique_vocabularies';
4948
+ ontologyType = 'ms_unique_vocabularies';
4949
+ customFilters = { ms_unique_vocabularies: { term_type: MSTermType.SEX } };
4950
+ break;
4852
4951
  case 'characteristics[developmental stage]':
4853
- return 'ms_unique_vocabularies';
4952
+ ontologyType = 'ms_unique_vocabularies';
4953
+ customFilters = { ms_unique_vocabularies: { term_type: MSTermType.DEVELOPMENTAL_STAGE } };
4954
+ break;
4854
4955
  case 'comment[label]':
4855
- return 'ms_unique_vocabularies';
4856
- default:
4857
- return '';
4956
+ ontologyType = 'ms_unique_vocabularies';
4957
+ customFilters = { ms_unique_vocabularies: { term_type: MSTermType.SAMPLE_ATTRIBUTE } };
4958
+ break;
4858
4959
  }
4859
- }
4860
- shouldEnableTypeaheadForColumn(columnName) {
4861
- const ontologyType = this.getDefaultOntologyTypeForColumn(columnName);
4862
- return ontologyType.length > 0;
4960
+ if (!ontologyType) {
4961
+ return { index: 0, ontologyType: '', customFilters: undefined };
4962
+ }
4963
+ // Find the matching ontology type config with the correct custom filters
4964
+ const index = this.ontologyTypes.findIndex(config => {
4965
+ if (config.value !== ontologyType)
4966
+ return false;
4967
+ if (!customFilters)
4968
+ return !config.customFilters || Object.keys(config.customFilters).length === 0;
4969
+ // Match custom filters
4970
+ if (!config.customFilters)
4971
+ return false;
4972
+ const configFilterKey = Object.keys(config.customFilters)[0];
4973
+ const customFilterKey = Object.keys(customFilters)[0];
4974
+ if (configFilterKey !== customFilterKey)
4975
+ return false;
4976
+ const configTermType = config.customFilters[configFilterKey]?.['term_type'];
4977
+ const customTermType = customFilters[customFilterKey]?.['term_type'];
4978
+ return configTermType === customTermType;
4979
+ });
4980
+ return {
4981
+ index: index >= 0 ? index : 0,
4982
+ ontologyType,
4983
+ customFilters
4984
+ };
4863
4985
  }
4864
4986
  isColumnMandatory(columnName) {
4865
4987
  const lowerName = columnName.toLowerCase();
@@ -6044,6 +6166,1245 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.3", ngImpor
6044
6166
  args: [{ selector: 'ccv-metadata-table-templates', imports: [CommonModule, ReactiveFormsModule, NgbModule], template: "<div class=\"metadata-table-templates-container\">\n <!-- Header Section -->\n <nav class=\"navbar navbar-expand-lg bg-body-tertiary shadow-sm\">\n <div class=\"container-fluid\">\n <div class=\"d-flex align-items-center justify-content-between w-100\">\n <div class=\"d-flex align-items-center\">\n <h4 class=\"navbar-brand m-0 fw-bold\">\n <i class=\"bi bi-file-earmark-text me-2\"></i>Table Templates\n </h4>\n @if (selectedLabGroup()) {\n <span class=\"badge bg-primary ms-3\">{{ selectedLabGroup()!.name }}</span>\n }\n </div>\n <div class=\"d-flex gap-2\">\n <div class=\"btn-group\" ngbDropdown>\n <button type=\"button\" class=\"btn btn-primary btn-sm dropdown-toggle\" ngbDropdownToggle>\n <i class=\"bi bi-plus me-1\"></i>New Template\n </button>\n <div class=\"dropdown-menu dropdown-menu-end\" ngbDropdownMenu>\n <a class=\"dropdown-item\" href=\"#\" (click)=\"createTemplateWithCustomSchemas(); $event.preventDefault()\"\n title=\"Select one or multiple schemas to create template\">\n <i class=\"bi bi-file-earmark-text me-2\"></i>\n <strong>Create from Schemas</strong>\n </a>\n <div class=\"dropdown-divider\"></div>\n <a class=\"dropdown-item\" href=\"#\" (click)=\"createTemplate(); $event.preventDefault()\"\n title=\"Create from scratch\">\n <i class=\"bi bi-wrench me-2\"></i>\n <strong>Manual Configuration</strong>\n </a>\n </div>\n </div>\n </div>\n </div>\n </div>\n </nav>\n\n <div class=\"templates-content p-4\">\n <!-- Search and Filters Section -->\n <div class=\"card mb-4\">\n <div class=\"card-header\">\n <h5 class=\"mb-0\">\n <i class=\"bi bi-search me-2\"></i>Search & Filters\n </h5>\n </div>\n <div class=\"card-body\">\n <form [formGroup]=\"searchForm\" class=\"row g-3\">\n <div class=\"col-md-3\">\n <div class=\"form-floating\">\n <input\n type=\"search\"\n class=\"form-control\"\n id=\"templateSearch\"\n formControlName=\"search\"\n placeholder=\"Search templates...\">\n <label for=\"templateSearch\">\n <i class=\"bi bi-search me-1\"></i>Search Templates\n </label>\n </div>\n </div>\n <div class=\"col-md-3\">\n <div class=\"form-floating\">\n <select\n class=\"form-select\"\n id=\"labGroupSelect\"\n formControlName=\"labGroupId\"\n (change)=\"onLabGroupChange($event)\">\n <option [ngValue]=\"null\">All Lab Groups</option>\n @if (hasLabGroups()) {\n @for (group of labGroupsData().results; track group.id) {\n <option [ngValue]=\"group.id\">{{ group.name }}</option>\n }\n }\n </select>\n <label for=\"labGroupSelect\">Lab Group</label>\n </div>\n\n <!-- Lab Groups Pagination -->\n @if (showLabGroupsPagination()) {\n <div class=\"d-flex justify-content-center mt-2\">\n <ngb-pagination\n [collectionSize]=\"labGroupsData().count\"\n [page]=\"labGroupsCurrentPage()\"\n [pageSize]=\"labGroupsPageSize()\"\n [maxSize]=\"3\"\n [boundaryLinks]=\"false\"\n [rotate]=\"true\"\n (pageChange)=\"onLabGroupsPageChange($event)\"\n class=\"pagination-sm\">\n </ngb-pagination>\n </div>\n }\n </div>\n <div class=\"col-md-3\">\n <div class=\"form-check form-switch mt-3\">\n <input\n class=\"form-check-input\"\n type=\"checkbox\"\n id=\"defaultTemplates\"\n formControlName=\"isDefault\">\n <label class=\"form-check-label\" for=\"defaultTemplates\">\n <i class=\"bi bi-star me-1\"></i>Default Templates Only\n </label>\n </div>\n </div>\n </form>\n </div>\n </div>\n\n <!-- Templates Table -->\n <div class=\"card\">\n <div class=\"card-header d-flex justify-content-between align-items-center\">\n <h5 class=\"mb-0\">\n <i class=\"bi bi-file-earmark-text me-2\"></i>Table Templates\n <span class=\"badge bg-primary ms-2\">{{ templatesData().count }}</span>\n </h5>\n @if (isLoading()) {\n <div class=\"spinner-border spinner-border-sm text-primary\" role=\"status\">\n <span class=\"visually-hidden\">Loading...</span>\n </div>\n }\n </div>\n <div class=\"card-body p-0\">\n @if (hasTemplates()) {\n <div class=\"table-responsive\">\n <table class=\"table table-hover mb-0\">\n <thead>\n <tr>\n <th><i class=\"bi bi-tag me-1\"></i>Template Name</th>\n <th><i class=\"bi bi-person me-1\"></i>Creator</th>\n <th><i class=\"bi bi-folder me-1\"></i>Lab Group</th>\n <th><i class=\"bi bi-list me-1\"></i>Columns</th>\n <th><i class=\"bi bi-eye me-1\"></i>Visibility</th>\n <th><i class=\"bi bi-star me-1\"></i>Default</th>\n <th><i class=\"bi bi-gear me-1\"></i>Actions</th>\n </tr>\n </thead>\n <tbody>\n @for (template of templatesData().results; track template.id) {\n <tr>\n <td>\n <strong>{{ template.name }}</strong>\n @if (template.description) {\n <br><small class=\"text-muted\">{{ template.description }}</small>\n }\n </td>\n <td>{{ template.ownerUsername || 'System' }}</td>\n <td>\n @if (template.labGroup) {\n <span class=\"badge bg-info\">{{ getLabGroupName(template.labGroup) }}</span>\n } @else {\n <span class=\"text-muted\">-</span>\n }\n </td>\n <td>\n <span class=\"badge bg-secondary\">{{ template.columnCount || 0 }}</span>\n </td>\n <td>\n @switch (template.visibility) {\n @case ('public') {\n <span class=\"badge bg-success\">Public</span>\n }\n @case ('group') {\n <span class=\"badge bg-info\">Lab Group</span>\n }\n @case ('private') {\n <span class=\"badge bg-warning\">Private</span>\n }\n @default {\n <span class=\"badge bg-secondary\">{{ template.visibility }}</span>\n }\n }\n </td>\n <td>\n @if (template.isDefault) {\n <i class=\"bi bi-star-fill text-warning\"></i>\n } @else {\n <span class=\"text-muted\">-</span>\n }\n </td>\n <td>\n <div class=\"btn-group btn-group-sm\" role=\"group\">\n <button\n class=\"btn btn-outline-success\"\n (click)=\"createTableFromTemplate(template)\"\n title=\"Create metadata table from this template\">\n <i class=\"bi bi-table\"></i>\n </button>\n <button\n class=\"btn btn-outline-primary\"\n (click)=\"editTemplate(template)\"\n title=\"Edit template\">\n <i class=\"bi bi-pencil\"></i>\n </button>\n <button\n class=\"btn btn-outline-info\"\n (click)=\"duplicateTemplate(template)\"\n title=\"Duplicate template\">\n <i class=\"bi bi-copy\"></i>\n </button>\n <button\n class=\"btn btn-outline-warning\"\n (click)=\"reorderTemplateColumns(template)\"\n title=\"Reorder columns by schema\"\n [disabled]=\"!template.columnCount || template.columnCount === 0\">\n <i class=\"bi bi-arrows-move\"></i>\n </button>\n <button\n class=\"btn btn-outline-danger\"\n (click)=\"deleteTemplate(template)\"\n title=\"Delete template\">\n <i class=\"bi bi-trash\"></i>\n </button>\n </div>\n </td>\n </tr>\n }\n </tbody>\n </table>\n </div>\n\n <!-- Pagination -->\n @if (showTemplatesPagination()) {\n <div class=\"d-flex justify-content-center mt-3 p-3\">\n <ngb-pagination\n [collectionSize]=\"templatesData().count\"\n [page]=\"currentPage()\"\n [pageSize]=\"pageSize()\"\n [maxSize]=\"5\"\n [boundaryLinks]=\"true\"\n [rotate]=\"true\"\n (pageChange)=\"onPageChange($event)\"\n class=\"pagination-sm\">\n </ngb-pagination>\n </div>\n }\n } @else {\n <div class=\"empty-state text-center py-5\">\n <i class=\"bi bi-file-earmark-text fs-1 text-muted mb-3\"></i>\n <h5 class=\"text-muted\">No Table Templates Found</h5>\n <p class=\"text-muted\">\n @if (searchForm.get('search')?.value || searchForm.get('labGroupId')?.value) {\n Try adjusting your search criteria or create a new template.\n } @else {\n Get started by creating your first table template.\n }\n </p>\n <button class=\"btn btn-primary\" (click)=\"createTemplate()\">\n <i class=\"bi bi-plus me-1\"></i>Create First Template\n </button>\n </div>\n }\n </div>\n </div>\n </div>\n</div>\n" }]
6045
6167
  }], ctorParameters: () => [{ type: i1.FormBuilder }, { type: MetadataTableTemplateService }, { type: i3.LabGroupService }, { type: i2.NgbModal }, { type: i3.ToastService }, { type: AsyncTaskUIService }] });
6046
6168
 
6169
+ class ColumnTemplateEditModal {
6170
+ fb;
6171
+ activeModal;
6172
+ template = null;
6173
+ labGroups = [];
6174
+ isEdit = false;
6175
+ templateSaved = new EventEmitter();
6176
+ editForm;
6177
+ isLoading = signal(false, ...(ngDevMode ? [{ debugName: "isLoading" }] : []));
6178
+ // Make Object available in template
6179
+ Object = Object;
6180
+ // Available options for dropdowns
6181
+ columnTypes = COLUMN_TYPE_CONFIGS;
6182
+ visibilityOptions = [
6183
+ { value: 'private', label: 'Private' },
6184
+ { value: 'labGroup', label: 'Lab Group' },
6185
+ { value: 'public', label: 'Public' },
6186
+ { value: 'global', label: 'Global' }
6187
+ ];
6188
+ ontologyTypes = ONTOLOGY_TYPE_CONFIGS;
6189
+ constructor(fb, activeModal) {
6190
+ this.fb = fb;
6191
+ this.activeModal = activeModal;
6192
+ this.editForm = this.fb.group({
6193
+ name: ['', [Validators.required, Validators.minLength(3)]],
6194
+ description: [''],
6195
+ columnName: ['', [Validators.required, Validators.pattern(/^[a-zA-Z_][a-zA-Z0-9_]*$/)]],
6196
+ columnType: ['text', Validators.required],
6197
+ defaultValue: [''],
6198
+ defaultPosition: [0, [Validators.min(0)]],
6199
+ ontologyType: [''],
6200
+ customOntologyFilters: [{}],
6201
+ visibility: ['private', Validators.required],
6202
+ labGroup: [null],
6203
+ enableTypeahead: [false],
6204
+ excelValidation: [false],
6205
+ isActive: [true],
6206
+ tags: [''],
6207
+ category: ['']
6208
+ });
6209
+ }
6210
+ ngOnInit() {
6211
+ // Set initial disabled state for labGroup field
6212
+ this.onVisibilityChange();
6213
+ if (this.template && this.isEdit) {
6214
+ this.populateForm();
6215
+ // Reapply visibility logic after populating form
6216
+ this.onVisibilityChange();
6217
+ // Apply ontology filters if ontology type is set
6218
+ this.onOntologyTypeChange();
6219
+ }
6220
+ }
6221
+ populateForm() {
6222
+ if (this.template) {
6223
+ this.editForm.patchValue({
6224
+ name: this.template.name,
6225
+ description: this.template.description || '',
6226
+ columnName: this.template.columnName,
6227
+ columnType: this.template.columnType,
6228
+ defaultValue: this.template.defaultValue || '',
6229
+ defaultPosition: this.template.defaultPosition || 0,
6230
+ ontologyType: this.template.ontologyType || '',
6231
+ customOntologyFilters: this.template.customOntologyFilters || {},
6232
+ visibility: this.template.visibility || 'private',
6233
+ labGroup: this.template.labGroup || null,
6234
+ enableTypeahead: this.template.enableTypeahead || false,
6235
+ excelValidation: this.template.excelValidation || false,
6236
+ isActive: this.template.isActive !== false,
6237
+ tags: this.template.tags || '',
6238
+ category: this.template.category || ''
6239
+ });
6240
+ }
6241
+ }
6242
+ onVisibilityChange() {
6243
+ const visibility = this.editForm.get('visibility')?.value;
6244
+ const labGroupControl = this.editForm.get('labGroup');
6245
+ if (visibility === 'labGroup') {
6246
+ labGroupControl?.setValidators([Validators.required]);
6247
+ labGroupControl?.enable();
6248
+ }
6249
+ else {
6250
+ labGroupControl?.clearValidators();
6251
+ labGroupControl?.setValue(null);
6252
+ labGroupControl?.disable();
6253
+ }
6254
+ labGroupControl?.updateValueAndValidity();
6255
+ }
6256
+ onOntologyTypeChange() {
6257
+ const ontologyType = this.editForm.get('ontologyType')?.value;
6258
+ const customFiltersControl = this.editForm.get('customOntologyFilters');
6259
+ // Find the selected ontology type configuration
6260
+ const selectedOntology = this.ontologyTypes.find(ont => ont.value === ontologyType);
6261
+ if (selectedOntology && selectedOntology.customFilters) {
6262
+ // Apply the custom filter from the ontology type
6263
+ customFiltersControl?.setValue(selectedOntology.customFilters);
6264
+ }
6265
+ else {
6266
+ // Clear custom filters if no ontology type selected or no custom filter defined
6267
+ customFiltersControl?.setValue({});
6268
+ }
6269
+ customFiltersControl?.markAsDirty();
6270
+ }
6271
+ onSubmit() {
6272
+ if (this.editForm.valid) {
6273
+ this.isLoading.set(true);
6274
+ const formValue = this.editForm.value;
6275
+ // Clean up form data
6276
+ const templateData = {
6277
+ ...formValue,
6278
+ labGroup: formValue.visibility === 'labGroup' ? formValue.labGroup : null,
6279
+ defaultPosition: Number(formValue.defaultPosition) || 0
6280
+ };
6281
+ this.templateSaved.emit(templateData);
6282
+ }
6283
+ else {
6284
+ this.markFormGroupTouched();
6285
+ }
6286
+ }
6287
+ markFormGroupTouched() {
6288
+ Object.keys(this.editForm.controls).forEach(key => {
6289
+ const control = this.editForm.get(key);
6290
+ control?.markAsTouched();
6291
+ });
6292
+ }
6293
+ onCancel() {
6294
+ this.activeModal.dismiss();
6295
+ }
6296
+ onClose() {
6297
+ this.isLoading.set(false);
6298
+ this.activeModal.close();
6299
+ }
6300
+ getFieldError(fieldName) {
6301
+ const control = this.editForm.get(fieldName);
6302
+ if (control?.errors && control.touched) {
6303
+ if (control.errors['required'])
6304
+ return `${fieldName} is required`;
6305
+ if (control.errors['minlength'])
6306
+ return `${fieldName} must be at least ${control.errors['minlength'].requiredLength} characters`;
6307
+ if (control.errors['pattern'])
6308
+ return `${fieldName} must be a valid identifier (letters, numbers, underscore)`;
6309
+ if (control.errors['min'])
6310
+ return `${fieldName} must be at least ${control.errors['min'].min}`;
6311
+ }
6312
+ return '';
6313
+ }
6314
+ get title() {
6315
+ return this.isEdit ? 'Edit Column Template' : 'Create Column Template';
6316
+ }
6317
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.3", ngImport: i0, type: ColumnTemplateEditModal, deps: [{ token: i1.FormBuilder }, { token: i2.NgbActiveModal }], target: i0.ɵɵFactoryTarget.Component });
6318
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.3", type: ColumnTemplateEditModal, isStandalone: true, selector: "ccv-column-template-edit-modal", inputs: { template: "template", labGroups: "labGroups", isEdit: "isEdit" }, outputs: { templateSaved: "templateSaved" }, ngImport: i0, template: "<div class=\"modal-header\">\n <h4 class=\"modal-title\">\n <i class=\"bi bi-clipboard-list me-2\"></i>{{ title }}\n </h4>\n <button type=\"button\" class=\"btn-close\" (click)=\"onCancel()\"></button>\n</div>\n\n<div class=\"modal-body\">\n <form [formGroup]=\"editForm\" (ngSubmit)=\"onSubmit()\">\n <div class=\"row\">\n <!-- Basic Information -->\n <div class=\"col-12 mb-3\">\n <h6 class=\"text-muted border-bottom pb-2\">\n <i class=\"bi bi-info-circle me-2\"></i>Basic Information\n </h6>\n </div>\n\n <div class=\"col-md-6 mb-3\">\n <label for=\"templateName\" class=\"form-label\">Template Name *</label>\n <input\n type=\"text\"\n class=\"form-control\"\n id=\"templateName\"\n formControlName=\"name\"\n [class.is-invalid]=\"editForm.get('name')?.invalid && editForm.get('name')?.touched\"\n placeholder=\"Enter template name\">\n <div class=\"invalid-feedback\">\n {{ getFieldError('name') }}\n </div>\n </div>\n\n <div class=\"col-md-6 mb-3\">\n <label for=\"columnName\" class=\"form-label\">Column Name *</label>\n <input\n type=\"text\"\n class=\"form-control\"\n id=\"columnName\"\n formControlName=\"columnName\"\n [class.is-invalid]=\"editForm.get('columnName')?.invalid && editForm.get('columnName')?.touched\"\n placeholder=\"columnName\">\n <div class=\"invalid-feedback\">\n {{ getFieldError('columnName') }}\n </div>\n <small class=\"form-text text-muted\">Column name based on column type and template name. Example: characteristics[biological replicate]</small>\n </div>\n\n <div class=\"col-12 mb-3\">\n <label for=\"description\" class=\"form-label\">Description</label>\n <textarea\n class=\"form-control\"\n id=\"description\"\n formControlName=\"description\"\n rows=\"2\"\n placeholder=\"Optional description\"></textarea>\n </div>\n\n <!-- Column Configuration -->\n <div class=\"col-12 mb-3\">\n <h6 class=\"text-muted border-bottom pb-2\">\n <i class=\"bi bi-gear me-2\"></i>Column Configuration\n </h6>\n </div>\n\n <div class=\"col-md-6 mb-3\">\n <label for=\"columnType\" class=\"form-label\">Column Type *</label>\n <select\n class=\"form-select\"\n id=\"columnType\"\n formControlName=\"columnType\"\n [class.is-invalid]=\"editForm.get('columnType')?.invalid && editForm.get('columnType')?.touched\">\n @for (type of columnTypes; track type.value) {\n <option [value]=\"type.value\">{{ type.label }}</option>\n }\n </select>\n <div class=\"invalid-feedback\">\n {{ getFieldError('columnType') }}\n </div>\n </div>\n\n <div class=\"col-md-6 mb-3\">\n <label for=\"defaultPosition\" class=\"form-label\">Default Position</label>\n <input\n type=\"number\"\n class=\"form-control\"\n id=\"defaultPosition\"\n formControlName=\"defaultPosition\"\n [class.is-invalid]=\"editForm.get('defaultPosition')?.invalid && editForm.get('defaultPosition')?.touched\"\n min=\"0\"\n placeholder=\"0\">\n <div class=\"invalid-feedback\">\n {{ getFieldError('defaultPosition') }}\n </div>\n </div>\n\n <div class=\"col-md-6 mb-3\">\n <label for=\"defaultValue\" class=\"form-label\">Default Value</label>\n <input\n type=\"text\"\n class=\"form-control\"\n id=\"defaultValue\"\n formControlName=\"defaultValue\"\n placeholder=\"Optional default value\">\n </div>\n\n <div class=\"col-md-6 mb-3\">\n <label for=\"ontologyType\" class=\"form-label\">Ontology Type</label>\n <select\n class=\"form-select\"\n id=\"ontologyType\"\n formControlName=\"ontologyType\"\n (change)=\"onOntologyTypeChange()\">\n @for (ontology of ontologyTypes; track $index) {\n <option [value]=\"ontology.value\">{{ ontology.label }}</option>\n }\n </select>\n </div>\n\n @if (editForm.get('ontologyType')?.value && editForm.get('customOntologyFilters')?.value && Object.keys(editForm.get('customOntologyFilters')?.value || {}).length > 0) {\n <div class=\"col-12 mb-3\">\n <div class=\"alert alert-info\">\n <h6 class=\"alert-heading\">\n <i class=\"bi bi-filter me-2\"></i>Custom Ontology Filters Applied\n </h6>\n <small class=\"mb-0\">\n The following custom filters will be automatically applied for this ontology type:\n <code class=\"ms-1\">{{ editForm.get('customOntologyFilters')?.value | json }}</code>\n </small>\n </div>\n </div>\n }\n\n <!-- Visibility and Access -->\n <div class=\"col-12 mb-3\">\n <h6 class=\"text-muted border-bottom pb-2\">\n <i class=\"bi bi-eye me-2\"></i>Visibility and Access\n </h6>\n </div>\n\n <div class=\"col-md-6 mb-3\">\n <label for=\"visibility\" class=\"form-label\">Visibility *</label>\n <select\n class=\"form-select\"\n id=\"visibility\"\n formControlName=\"visibility\"\n (change)=\"onVisibilityChange()\"\n [class.is-invalid]=\"editForm.get('visibility')?.invalid && editForm.get('visibility')?.touched\">\n @for (option of visibilityOptions; track option.value) {\n <option [value]=\"option.value\">{{ option.label }}</option>\n }\n </select>\n <div class=\"invalid-feedback\">\n {{ getFieldError('visibility') }}\n </div>\n </div>\n\n <div class=\"col-md-6 mb-3\">\n <label for=\"labGroup\" class=\"form-label\">Lab Group</label>\n <select\n class=\"form-select\"\n id=\"labGroup\"\n formControlName=\"labGroup\"\n [class.is-invalid]=\"editForm.get('labGroup')?.invalid && editForm.get('labGroup')?.touched\">\n <option [ngValue]=\"null\">Select Lab Group</option>\n @for (group of labGroups; track group.id) {\n <option [ngValue]=\"group.id\">{{ group.name }}</option>\n }\n </select>\n <div class=\"invalid-feedback\">\n {{ getFieldError('labGroup') }}\n </div>\n </div>\n\n <!-- Additional Options -->\n <div class=\"col-12 mb-3\">\n <h6 class=\"text-muted border-bottom pb-2\">\n <i class=\"bi bi-sliders me-2\"></i>Additional Options\n </h6>\n </div>\n\n <div class=\"col-md-6 mb-3\">\n <label for=\"category\" class=\"form-label\">Category</label>\n <input\n type=\"text\"\n class=\"form-control\"\n id=\"category\"\n formControlName=\"category\"\n placeholder=\"Template category\">\n </div>\n\n <div class=\"col-md-6 mb-3\">\n <label for=\"tags\" class=\"form-label\">Tags</label>\n <input\n type=\"text\"\n class=\"form-control\"\n id=\"tags\"\n formControlName=\"tags\"\n placeholder=\"Comma-separated tags\">\n </div>\n\n <div class=\"col-12 mb-3\">\n <div class=\"row\">\n <div class=\"col-md-3\">\n <div class=\"form-check form-switch\">\n <input\n class=\"form-check-input\"\n type=\"checkbox\"\n id=\"enableTypeahead\"\n formControlName=\"enableTypeahead\">\n <label class=\"form-check-label\" for=\"enableTypeahead\">\n Enable Typeahead\n </label>\n </div>\n </div>\n <div class=\"col-md-3\">\n <div class=\"form-check form-switch\">\n <input\n class=\"form-check-input\"\n type=\"checkbox\"\n id=\"excelValidation\"\n formControlName=\"excelValidation\">\n <label class=\"form-check-label\" for=\"excelValidation\">\n Excel Validation\n </label>\n </div>\n </div>\n <div class=\"col-md-3\">\n <div class=\"form-check form-switch\">\n <input\n class=\"form-check-input\"\n type=\"checkbox\"\n id=\"isActive\"\n formControlName=\"isActive\">\n <label class=\"form-check-label\" for=\"isActive\">\n Active\n </label>\n </div>\n </div>\n </div>\n </div>\n </div>\n </form>\n</div>\n\n<div class=\"modal-footer\">\n <button type=\"button\" class=\"btn btn-outline-secondary\" (click)=\"onCancel()\">\n <i class=\"bi bi-x-circle me-1\"></i>Cancel\n </button>\n <button\n type=\"button\"\n class=\"btn btn-primary\"\n (click)=\"onSubmit()\"\n [disabled]=\"editForm.invalid || isLoading()\">\n @if (isLoading()) {\n <span class=\"spinner-border spinner-border-sm me-2\"></span>\n }\n <i class=\"bi bi-check-circle me-1\"></i>\n {{ isEdit ? 'Update' : 'Create' }} Template\n </button>\n</div>\n", styles: [""], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { kind: "directive", type: i1.CheckboxControlValueAccessor, selector: "input[type=checkbox][formControlName],input[type=checkbox][formControl],input[type=checkbox][ngModel]" }, { kind: "directive", type: i1.SelectControlValueAccessor, selector: "select:not([multiple])[formControlName],select:not([multiple])[formControl],select:not([multiple])[ngModel]", inputs: ["compareWith"] }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1.MinValidator, selector: "input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]", inputs: ["min"] }, { kind: "directive", type: i1.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i1.FormControlName, selector: "[formControlName]", inputs: ["formControlName", "disabled", "ngModel"], outputs: ["ngModelChange"] }, { kind: "ngmodule", type: NgbModule }, { kind: "pipe", type: i5.JsonPipe, name: "json" }] });
6319
+ }
6320
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.3", ngImport: i0, type: ColumnTemplateEditModal, decorators: [{
6321
+ type: Component,
6322
+ args: [{ selector: 'ccv-column-template-edit-modal', standalone: true, imports: [CommonModule, ReactiveFormsModule, NgbModule], template: "<div class=\"modal-header\">\n <h4 class=\"modal-title\">\n <i class=\"bi bi-clipboard-list me-2\"></i>{{ title }}\n </h4>\n <button type=\"button\" class=\"btn-close\" (click)=\"onCancel()\"></button>\n</div>\n\n<div class=\"modal-body\">\n <form [formGroup]=\"editForm\" (ngSubmit)=\"onSubmit()\">\n <div class=\"row\">\n <!-- Basic Information -->\n <div class=\"col-12 mb-3\">\n <h6 class=\"text-muted border-bottom pb-2\">\n <i class=\"bi bi-info-circle me-2\"></i>Basic Information\n </h6>\n </div>\n\n <div class=\"col-md-6 mb-3\">\n <label for=\"templateName\" class=\"form-label\">Template Name *</label>\n <input\n type=\"text\"\n class=\"form-control\"\n id=\"templateName\"\n formControlName=\"name\"\n [class.is-invalid]=\"editForm.get('name')?.invalid && editForm.get('name')?.touched\"\n placeholder=\"Enter template name\">\n <div class=\"invalid-feedback\">\n {{ getFieldError('name') }}\n </div>\n </div>\n\n <div class=\"col-md-6 mb-3\">\n <label for=\"columnName\" class=\"form-label\">Column Name *</label>\n <input\n type=\"text\"\n class=\"form-control\"\n id=\"columnName\"\n formControlName=\"columnName\"\n [class.is-invalid]=\"editForm.get('columnName')?.invalid && editForm.get('columnName')?.touched\"\n placeholder=\"columnName\">\n <div class=\"invalid-feedback\">\n {{ getFieldError('columnName') }}\n </div>\n <small class=\"form-text text-muted\">Column name based on column type and template name. Example: characteristics[biological replicate]</small>\n </div>\n\n <div class=\"col-12 mb-3\">\n <label for=\"description\" class=\"form-label\">Description</label>\n <textarea\n class=\"form-control\"\n id=\"description\"\n formControlName=\"description\"\n rows=\"2\"\n placeholder=\"Optional description\"></textarea>\n </div>\n\n <!-- Column Configuration -->\n <div class=\"col-12 mb-3\">\n <h6 class=\"text-muted border-bottom pb-2\">\n <i class=\"bi bi-gear me-2\"></i>Column Configuration\n </h6>\n </div>\n\n <div class=\"col-md-6 mb-3\">\n <label for=\"columnType\" class=\"form-label\">Column Type *</label>\n <select\n class=\"form-select\"\n id=\"columnType\"\n formControlName=\"columnType\"\n [class.is-invalid]=\"editForm.get('columnType')?.invalid && editForm.get('columnType')?.touched\">\n @for (type of columnTypes; track type.value) {\n <option [value]=\"type.value\">{{ type.label }}</option>\n }\n </select>\n <div class=\"invalid-feedback\">\n {{ getFieldError('columnType') }}\n </div>\n </div>\n\n <div class=\"col-md-6 mb-3\">\n <label for=\"defaultPosition\" class=\"form-label\">Default Position</label>\n <input\n type=\"number\"\n class=\"form-control\"\n id=\"defaultPosition\"\n formControlName=\"defaultPosition\"\n [class.is-invalid]=\"editForm.get('defaultPosition')?.invalid && editForm.get('defaultPosition')?.touched\"\n min=\"0\"\n placeholder=\"0\">\n <div class=\"invalid-feedback\">\n {{ getFieldError('defaultPosition') }}\n </div>\n </div>\n\n <div class=\"col-md-6 mb-3\">\n <label for=\"defaultValue\" class=\"form-label\">Default Value</label>\n <input\n type=\"text\"\n class=\"form-control\"\n id=\"defaultValue\"\n formControlName=\"defaultValue\"\n placeholder=\"Optional default value\">\n </div>\n\n <div class=\"col-md-6 mb-3\">\n <label for=\"ontologyType\" class=\"form-label\">Ontology Type</label>\n <select\n class=\"form-select\"\n id=\"ontologyType\"\n formControlName=\"ontologyType\"\n (change)=\"onOntologyTypeChange()\">\n @for (ontology of ontologyTypes; track $index) {\n <option [value]=\"ontology.value\">{{ ontology.label }}</option>\n }\n </select>\n </div>\n\n @if (editForm.get('ontologyType')?.value && editForm.get('customOntologyFilters')?.value && Object.keys(editForm.get('customOntologyFilters')?.value || {}).length > 0) {\n <div class=\"col-12 mb-3\">\n <div class=\"alert alert-info\">\n <h6 class=\"alert-heading\">\n <i class=\"bi bi-filter me-2\"></i>Custom Ontology Filters Applied\n </h6>\n <small class=\"mb-0\">\n The following custom filters will be automatically applied for this ontology type:\n <code class=\"ms-1\">{{ editForm.get('customOntologyFilters')?.value | json }}</code>\n </small>\n </div>\n </div>\n }\n\n <!-- Visibility and Access -->\n <div class=\"col-12 mb-3\">\n <h6 class=\"text-muted border-bottom pb-2\">\n <i class=\"bi bi-eye me-2\"></i>Visibility and Access\n </h6>\n </div>\n\n <div class=\"col-md-6 mb-3\">\n <label for=\"visibility\" class=\"form-label\">Visibility *</label>\n <select\n class=\"form-select\"\n id=\"visibility\"\n formControlName=\"visibility\"\n (change)=\"onVisibilityChange()\"\n [class.is-invalid]=\"editForm.get('visibility')?.invalid && editForm.get('visibility')?.touched\">\n @for (option of visibilityOptions; track option.value) {\n <option [value]=\"option.value\">{{ option.label }}</option>\n }\n </select>\n <div class=\"invalid-feedback\">\n {{ getFieldError('visibility') }}\n </div>\n </div>\n\n <div class=\"col-md-6 mb-3\">\n <label for=\"labGroup\" class=\"form-label\">Lab Group</label>\n <select\n class=\"form-select\"\n id=\"labGroup\"\n formControlName=\"labGroup\"\n [class.is-invalid]=\"editForm.get('labGroup')?.invalid && editForm.get('labGroup')?.touched\">\n <option [ngValue]=\"null\">Select Lab Group</option>\n @for (group of labGroups; track group.id) {\n <option [ngValue]=\"group.id\">{{ group.name }}</option>\n }\n </select>\n <div class=\"invalid-feedback\">\n {{ getFieldError('labGroup') }}\n </div>\n </div>\n\n <!-- Additional Options -->\n <div class=\"col-12 mb-3\">\n <h6 class=\"text-muted border-bottom pb-2\">\n <i class=\"bi bi-sliders me-2\"></i>Additional Options\n </h6>\n </div>\n\n <div class=\"col-md-6 mb-3\">\n <label for=\"category\" class=\"form-label\">Category</label>\n <input\n type=\"text\"\n class=\"form-control\"\n id=\"category\"\n formControlName=\"category\"\n placeholder=\"Template category\">\n </div>\n\n <div class=\"col-md-6 mb-3\">\n <label for=\"tags\" class=\"form-label\">Tags</label>\n <input\n type=\"text\"\n class=\"form-control\"\n id=\"tags\"\n formControlName=\"tags\"\n placeholder=\"Comma-separated tags\">\n </div>\n\n <div class=\"col-12 mb-3\">\n <div class=\"row\">\n <div class=\"col-md-3\">\n <div class=\"form-check form-switch\">\n <input\n class=\"form-check-input\"\n type=\"checkbox\"\n id=\"enableTypeahead\"\n formControlName=\"enableTypeahead\">\n <label class=\"form-check-label\" for=\"enableTypeahead\">\n Enable Typeahead\n </label>\n </div>\n </div>\n <div class=\"col-md-3\">\n <div class=\"form-check form-switch\">\n <input\n class=\"form-check-input\"\n type=\"checkbox\"\n id=\"excelValidation\"\n formControlName=\"excelValidation\">\n <label class=\"form-check-label\" for=\"excelValidation\">\n Excel Validation\n </label>\n </div>\n </div>\n <div class=\"col-md-3\">\n <div class=\"form-check form-switch\">\n <input\n class=\"form-check-input\"\n type=\"checkbox\"\n id=\"isActive\"\n formControlName=\"isActive\">\n <label class=\"form-check-label\" for=\"isActive\">\n Active\n </label>\n </div>\n </div>\n </div>\n </div>\n </div>\n </form>\n</div>\n\n<div class=\"modal-footer\">\n <button type=\"button\" class=\"btn btn-outline-secondary\" (click)=\"onCancel()\">\n <i class=\"bi bi-x-circle me-1\"></i>Cancel\n </button>\n <button\n type=\"button\"\n class=\"btn btn-primary\"\n (click)=\"onSubmit()\"\n [disabled]=\"editForm.invalid || isLoading()\">\n @if (isLoading()) {\n <span class=\"spinner-border spinner-border-sm me-2\"></span>\n }\n <i class=\"bi bi-check-circle me-1\"></i>\n {{ isEdit ? 'Update' : 'Create' }} Template\n </button>\n</div>\n" }]
6323
+ }], ctorParameters: () => [{ type: i1.FormBuilder }, { type: i2.NgbActiveModal }], propDecorators: { template: [{
6324
+ type: Input
6325
+ }], labGroups: [{
6326
+ type: Input
6327
+ }], isEdit: [{
6328
+ type: Input
6329
+ }], templateSaved: [{
6330
+ type: Output
6331
+ }] } });
6332
+
6333
+ class ColumnTemplates {
6334
+ fb;
6335
+ metadataColumnTemplateService;
6336
+ labGroupService;
6337
+ modalService;
6338
+ searchForm;
6339
+ // Signals for reactive state management
6340
+ searchParams = signal({
6341
+ search: '',
6342
+ labGroupId: null,
6343
+ visibility: null,
6344
+ limit: 10,
6345
+ offset: 0
6346
+ }, ...(ngDevMode ? [{ debugName: "searchParams" }] : []));
6347
+ labGroupParams = signal({
6348
+ limit: 10,
6349
+ offset: 0
6350
+ }, ...(ngDevMode ? [{ debugName: "labGroupParams" }] : []));
6351
+ // Computed signals for better performance
6352
+ isLoading = signal(false, ...(ngDevMode ? [{ debugName: "isLoading" }] : []));
6353
+ selectedLabGroup = signal(null, ...(ngDevMode ? [{ debugName: "selectedLabGroup" }] : []));
6354
+ currentPage = signal(1, ...(ngDevMode ? [{ debugName: "currentPage" }] : []));
6355
+ pageSize = signal(10, ...(ngDevMode ? [{ debugName: "pageSize" }] : []));
6356
+ totalItems = signal(0, ...(ngDevMode ? [{ debugName: "totalItems" }] : []));
6357
+ labGroupsCurrentPage = signal(1, ...(ngDevMode ? [{ debugName: "labGroupsCurrentPage" }] : []));
6358
+ labGroupsPageSize = signal(10, ...(ngDevMode ? [{ debugName: "labGroupsPageSize" }] : []));
6359
+ labGroupsTotalItems = signal(0, ...(ngDevMode ? [{ debugName: "labGroupsTotalItems" }] : []));
6360
+ // Column filter
6361
+ columnFilter = signal('', ...(ngDevMode ? [{ debugName: "columnFilter" }] : []));
6362
+ // Sorting
6363
+ sortField = signal('', ...(ngDevMode ? [{ debugName: "sortField" }] : []));
6364
+ sortDirection = signal('asc', ...(ngDevMode ? [{ debugName: "sortDirection" }] : []));
6365
+ // Direct signal-based data management
6366
+ templatesData = signal({ count: 0, results: [] }, ...(ngDevMode ? [{ debugName: "templatesData" }] : []));
6367
+ labGroupsData = signal({ count: 0, results: [] }, ...(ngDevMode ? [{ debugName: "labGroupsData" }] : []));
6368
+ // Computed values for better performance
6369
+ hasTemplates = computed(() => this.templatesData().results.length > 0, ...(ngDevMode ? [{ debugName: "hasTemplates" }] : []));
6370
+ hasLabGroups = computed(() => this.labGroupsData().results.length > 0, ...(ngDevMode ? [{ debugName: "hasLabGroups" }] : []));
6371
+ showTemplatesPagination = computed(() => this.templatesData().count > this.pageSize(), ...(ngDevMode ? [{ debugName: "showTemplatesPagination" }] : []));
6372
+ showLabGroupsPagination = computed(() => this.labGroupsData().count > this.labGroupsPageSize(), ...(ngDevMode ? [{ debugName: "showLabGroupsPagination" }] : []));
6373
+ // Filtered and sorted templates
6374
+ filteredTemplates = computed(() => {
6375
+ const templates = this.templatesData().results;
6376
+ const filter = this.columnFilter().toLowerCase();
6377
+ const sortField = this.sortField();
6378
+ const sortDirection = this.sortDirection();
6379
+ // Apply filter
6380
+ let filtered = filter ?
6381
+ templates.filter(template => template.columnName.toLowerCase().includes(filter) ||
6382
+ template.name.toLowerCase().includes(filter)) : templates;
6383
+ // Apply sorting
6384
+ if (sortField) {
6385
+ filtered = [...filtered].sort((a, b) => {
6386
+ let aValue = '';
6387
+ let bValue = '';
6388
+ switch (sortField) {
6389
+ case 'name':
6390
+ aValue = a.name?.toLowerCase() || '';
6391
+ bValue = b.name?.toLowerCase() || '';
6392
+ break;
6393
+ case 'columnName':
6394
+ aValue = a.columnName?.toLowerCase() || '';
6395
+ bValue = b.columnName?.toLowerCase() || '';
6396
+ break;
6397
+ case 'columnType':
6398
+ aValue = a.columnType?.toLowerCase() || '';
6399
+ bValue = b.columnType?.toLowerCase() || '';
6400
+ break;
6401
+ case 'ontologyType':
6402
+ aValue = a.ontologyType?.toLowerCase() || '';
6403
+ bValue = b.ontologyType?.toLowerCase() || '';
6404
+ break;
6405
+ case 'visibility':
6406
+ aValue = a.visibility?.toLowerCase() || '';
6407
+ bValue = b.visibility?.toLowerCase() || '';
6408
+ break;
6409
+ default:
6410
+ return 0;
6411
+ }
6412
+ if (aValue < bValue)
6413
+ return sortDirection === 'asc' ? -1 : 1;
6414
+ if (aValue > bValue)
6415
+ return sortDirection === 'asc' ? 1 : -1;
6416
+ return 0;
6417
+ });
6418
+ }
6419
+ return filtered;
6420
+ }, ...(ngDevMode ? [{ debugName: "filteredTemplates" }] : []));
6421
+ constructor(fb, metadataColumnTemplateService, labGroupService, modalService) {
6422
+ this.fb = fb;
6423
+ this.metadataColumnTemplateService = metadataColumnTemplateService;
6424
+ this.labGroupService = labGroupService;
6425
+ this.modalService = modalService;
6426
+ this.searchForm = this.fb.group({
6427
+ search: [''],
6428
+ labGroupId: [null],
6429
+ visibility: [null]
6430
+ });
6431
+ // Effect to automatically reload templates when search params change
6432
+ effect(() => {
6433
+ const params = this.searchParams();
6434
+ this.loadTemplatesWithParams(params);
6435
+ });
6436
+ // Effect to automatically reload lab groups when params change
6437
+ effect(() => {
6438
+ const params = this.labGroupParams();
6439
+ this.loadLabGroupsWithParams(params);
6440
+ });
6441
+ }
6442
+ ngOnInit() {
6443
+ this.setupSearch();
6444
+ this.loadInitialData();
6445
+ }
6446
+ loadInitialData() {
6447
+ // Initial load - signals will handle subsequent updates via effects
6448
+ this.labGroupParams.set({
6449
+ limit: this.labGroupsPageSize(),
6450
+ offset: 0
6451
+ });
6452
+ this.searchParams.set({
6453
+ search: '',
6454
+ labGroupId: null,
6455
+ visibility: null,
6456
+ limit: this.pageSize(),
6457
+ offset: 0
6458
+ });
6459
+ }
6460
+ loadLabGroupsWithParams(params) {
6461
+ this.labGroupService.getMyLabGroups({ limit: params.limit, offset: params.offset }).subscribe({
6462
+ next: (response) => {
6463
+ this.labGroupsTotalItems.set(response.count);
6464
+ this.labGroupsData.set(response);
6465
+ },
6466
+ error: (error) => {
6467
+ console.error('Error loading lab groups:', error);
6468
+ this.labGroupsData.set({ count: 0, results: [] });
6469
+ }
6470
+ });
6471
+ }
6472
+ setupSearch() {
6473
+ // Subscribe to form changes and update search params signal
6474
+ this.searchForm.valueChanges.pipe(debounceTime(300), distinctUntilChanged()).subscribe(formValue => {
6475
+ // Reset to first page when searching
6476
+ this.currentPage.set(1);
6477
+ // Update search params signal - this will trigger the effect
6478
+ this.searchParams.set({
6479
+ search: formValue.search || '',
6480
+ labGroupId: formValue.labGroupId || null,
6481
+ visibility: formValue.visibility || null,
6482
+ limit: this.pageSize(),
6483
+ offset: 0 // Always start from first page when searching
6484
+ });
6485
+ });
6486
+ }
6487
+ loadTemplatesWithParams(params) {
6488
+ this.isLoading.set(true);
6489
+ this.metadataColumnTemplateService.getMetadataColumnTemplates({
6490
+ search: params.search || undefined,
6491
+ labGroupId: params.labGroupId || undefined,
6492
+ visibility: params.visibility || undefined,
6493
+ limit: params.limit,
6494
+ offset: params.offset
6495
+ }).subscribe({
6496
+ next: (response) => {
6497
+ this.isLoading.set(false);
6498
+ this.totalItems.set(response.count);
6499
+ this.templatesData.set(response);
6500
+ },
6501
+ error: (error) => {
6502
+ this.isLoading.set(false);
6503
+ console.error('Error loading column templates:', error);
6504
+ this.templatesData.set({ count: 0, results: [] });
6505
+ }
6506
+ });
6507
+ }
6508
+ onLabGroupChange(event) {
6509
+ const target = event.target;
6510
+ const value = target.value;
6511
+ const id = value === 'null' ? null : Number(value);
6512
+ const labGroup = id ? this.findLabGroupById(id) : null;
6513
+ this.selectedLabGroup.set(labGroup);
6514
+ this.currentPage.set(1);
6515
+ // Update search params - effect will trigger reload
6516
+ this.searchParams.update(params => ({
6517
+ ...params,
6518
+ labGroupId: id,
6519
+ offset: 0
6520
+ }));
6521
+ }
6522
+ findLabGroupById(id) {
6523
+ const labGroups = this.labGroupsData();
6524
+ return labGroups.results.find((group) => group.id === id) || null;
6525
+ }
6526
+ onPageChange(page) {
6527
+ this.currentPage.set(page);
6528
+ this.searchParams.update(params => ({
6529
+ ...params,
6530
+ offset: (page - 1) * this.pageSize()
6531
+ }));
6532
+ }
6533
+ onLabGroupsPageChange(page) {
6534
+ this.labGroupsCurrentPage.set(page);
6535
+ this.labGroupParams.set({
6536
+ limit: this.labGroupsPageSize(),
6537
+ offset: (page - 1) * this.labGroupsPageSize()
6538
+ });
6539
+ }
6540
+ // Column filter methods
6541
+ onColumnFilterChange(value) {
6542
+ this.columnFilter.set(value);
6543
+ }
6544
+ clearColumnFilter() {
6545
+ this.columnFilter.set('');
6546
+ }
6547
+ get filteredTemplatesCount() {
6548
+ return this.filteredTemplates().length;
6549
+ }
6550
+ get totalTemplatesCount() {
6551
+ return this.templatesData().results.length;
6552
+ }
6553
+ // Sorting methods
6554
+ sortBy(field) {
6555
+ const currentField = this.sortField();
6556
+ const currentDirection = this.sortDirection();
6557
+ if (currentField === field) {
6558
+ // Toggle direction if same field
6559
+ this.sortDirection.set(currentDirection === 'asc' ? 'desc' : 'asc');
6560
+ }
6561
+ else {
6562
+ // New field, default to ascending
6563
+ this.sortField.set(field);
6564
+ this.sortDirection.set('asc');
6565
+ }
6566
+ }
6567
+ getSortIcon(field) {
6568
+ const currentField = this.sortField();
6569
+ const currentDirection = this.sortDirection();
6570
+ if (currentField !== field) {
6571
+ return 'bi-arrow-down-up'; // Default unsorted icon
6572
+ }
6573
+ return currentDirection === 'asc' ? 'bi-sort-alpha-down' : 'bi-sort-alpha-up';
6574
+ }
6575
+ isSorted(field) {
6576
+ return this.sortField() === field;
6577
+ }
6578
+ createTemplate() {
6579
+ this.openTemplateModal(null, false);
6580
+ }
6581
+ editTemplate(template) {
6582
+ this.openTemplateModal(template, true);
6583
+ }
6584
+ openTemplateModal(template, isEdit) {
6585
+ const modalRef = this.modalService.open(ColumnTemplateEditModal, {
6586
+ size: 'lg',
6587
+ backdrop: 'static',
6588
+ keyboard: false
6589
+ });
6590
+ modalRef.componentInstance.template = template;
6591
+ modalRef.componentInstance.labGroups = this.labGroupsData().results;
6592
+ modalRef.componentInstance.isEdit = isEdit;
6593
+ modalRef.componentInstance.templateSaved.subscribe((templateData) => {
6594
+ this.handleTemplateSave(templateData, template?.id || null, isEdit);
6595
+ modalRef.componentInstance.onClose();
6596
+ });
6597
+ }
6598
+ handleTemplateSave(templateData, templateId, isEdit) {
6599
+ this.isLoading.set(true);
6600
+ const apiCall = isEdit && templateId
6601
+ ? this.metadataColumnTemplateService.updateMetadataColumnTemplate(templateId, templateData)
6602
+ : this.metadataColumnTemplateService.createMetadataColumnTemplate(templateData);
6603
+ apiCall.subscribe({
6604
+ next: (template) => {
6605
+ this.isLoading.set(false);
6606
+ console.log(`Column template ${isEdit ? 'updated' : 'created'}:`, template);
6607
+ this.refreshTemplates();
6608
+ },
6609
+ error: (error) => {
6610
+ this.isLoading.set(false);
6611
+ console.error(`Error ${isEdit ? 'updating' : 'creating'} column template:`, error);
6612
+ alert(`Failed to ${isEdit ? 'update' : 'create'} template. Please try again.`);
6613
+ }
6614
+ });
6615
+ }
6616
+ deleteTemplate(template) {
6617
+ // Enhanced delete confirmation with better UX
6618
+ const confirmMessage = `Are you sure you want to delete the column template "${template.name}"?\n\nThis action cannot be undone.`;
6619
+ if (confirm(confirmMessage)) {
6620
+ this.isLoading.set(true);
6621
+ this.metadataColumnTemplateService.deleteMetadataColumnTemplate(template.id).subscribe({
6622
+ next: () => {
6623
+ this.isLoading.set(false);
6624
+ console.log('Column template deleted successfully');
6625
+ this.refreshTemplates();
6626
+ },
6627
+ error: (error) => {
6628
+ this.isLoading.set(false);
6629
+ console.error('Error deleting column template:', error);
6630
+ alert('Failed to delete template. Please try again.');
6631
+ }
6632
+ });
6633
+ }
6634
+ }
6635
+ duplicateTemplate(template) {
6636
+ const duplicatedTemplate = {
6637
+ name: `${template.name} (Copy)`,
6638
+ description: template.description,
6639
+ columnName: `${template.columnName}_copy`,
6640
+ columnType: template.columnType,
6641
+ defaultValue: template.defaultValue,
6642
+ ontologyType: template.ontologyType,
6643
+ visibility: ResourceVisibility.PRIVATE, // Always create as private
6644
+ labGroup: template.labGroup || this.selectedLabGroup()?.id || undefined,
6645
+ isActive: true
6646
+ };
6647
+ this.isLoading.set(true);
6648
+ this.metadataColumnTemplateService.createMetadataColumnTemplate(duplicatedTemplate).subscribe({
6649
+ next: (newTemplate) => {
6650
+ this.isLoading.set(false);
6651
+ console.log('Column template duplicated:', newTemplate);
6652
+ this.refreshTemplates();
6653
+ },
6654
+ error: (error) => {
6655
+ this.isLoading.set(false);
6656
+ console.error('Error duplicating column template:', error);
6657
+ alert('Failed to duplicate template. Please try again.');
6658
+ }
6659
+ });
6660
+ }
6661
+ refreshTemplates() {
6662
+ // Trigger a refresh by updating the search params
6663
+ this.searchParams.update(params => ({ ...params }));
6664
+ }
6665
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.3", ngImport: i0, type: ColumnTemplates, deps: [{ token: i1.FormBuilder }, { token: MetadataColumnTemplateService }, { token: i3.LabGroupService }, { token: i2.NgbModal }], target: i0.ɵɵFactoryTarget.Component });
6666
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.3", type: ColumnTemplates, isStandalone: true, selector: "ccv-column-templates", ngImport: i0, template: "<div class=\"column-templates-container\">\n <!-- Header Section -->\n <nav class=\"navbar navbar-expand-lg bg-body-tertiary shadow-sm\">\n <div class=\"container-fluid\">\n <div class=\"d-flex align-items-center justify-content-between w-100\">\n <div class=\"d-flex align-items-center\">\n <h4 class=\"navbar-brand m-0 fw-bold\">\n <i class=\"bi bi-clipboard-list me-2\"></i>Column Templates\n </h4>\n @if (selectedLabGroup()) {\n <span class=\"badge bg-primary ms-3\">{{ selectedLabGroup()!.name }}</span>\n }\n </div>\n <div class=\"d-flex gap-2\">\n <button class=\"btn btn-primary btn-sm\" (click)=\"createTemplate()\">\n <i class=\"bi bi-plus me-1\"></i>New Template\n </button>\n </div>\n </div>\n </div>\n </nav>\n\n <div class=\"templates-content p-4\">\n <!-- Search and Filters Section -->\n <div class=\"card mb-4\">\n <div class=\"card-header\">\n <h5 class=\"mb-0\">\n <i class=\"bi bi-search me-2\"></i>Search & Filters\n </h5>\n </div>\n <div class=\"card-body\">\n <form [formGroup]=\"searchForm\" class=\"row g-3\">\n <div class=\"col-md-4\">\n <div class=\"form-floating\">\n <input\n type=\"search\"\n class=\"form-control\"\n id=\"templateSearch\"\n formControlName=\"search\"\n placeholder=\"Search templates...\">\n <label for=\"templateSearch\">\n <i class=\"bi bi-search me-1\"></i>Search Templates\n </label>\n </div>\n </div>\n <div class=\"col-md-4\">\n <div class=\"form-floating\">\n <select\n class=\"form-select\"\n id=\"labGroupSelect\"\n formControlName=\"labGroupId\"\n (change)=\"onLabGroupChange($event)\">\n <option [ngValue]=\"null\">All Lab Groups</option>\n @if (hasLabGroups()) {\n @for (group of labGroupsData().results; track group.id) {\n <option [ngValue]=\"group.id\">{{ group.name }}</option>\n }\n }\n </select>\n <label for=\"labGroupSelect\">Lab Group</label>\n </div>\n\n <!-- Lab Groups Pagination -->\n @if (showLabGroupsPagination()) {\n <div class=\"d-flex justify-content-center mt-2\">\n <ngb-pagination\n [collectionSize]=\"labGroupsData().count\"\n [page]=\"labGroupsCurrentPage()\"\n [pageSize]=\"labGroupsPageSize()\"\n [maxSize]=\"3\"\n [boundaryLinks]=\"false\"\n [rotate]=\"true\"\n (pageChange)=\"onLabGroupsPageChange($event)\"\n class=\"pagination-sm\">\n </ngb-pagination>\n </div>\n }\n </div>\n <div class=\"col-md-4\">\n <div class=\"form-check form-switch mt-3\">\n <input\n class=\"form-check-input\"\n type=\"checkbox\"\n id=\"globalTemplates\"\n formControlName=\"visibility\">\n <label class=\"form-check-label\" for=\"globalTemplates\">\n <i class=\"bi bi-globe me-1\"></i>Global Templates Only\n </label>\n </div>\n </div>\n </form>\n </div>\n </div>\n\n <!-- Templates Table -->\n <div class=\"card\">\n <div class=\"card-header\">\n <div class=\"d-flex justify-content-between align-items-center mb-3\">\n <h5 class=\"mb-0\">\n <i class=\"bi bi-table me-2\"></i>Column Templates\n <span class=\"badge bg-primary ms-2\">{{ templatesData().count }}</span>\n </h5>\n @if (isLoading()) {\n <div class=\"spinner-border spinner-border-sm text-primary\" role=\"status\">\n <span class=\"visually-hidden\">Loading...</span>\n </div>\n }\n </div>\n \n <!-- Column Filter -->\n @if (hasTemplates()) {\n <div class=\"d-flex align-items-center\">\n <div class=\"input-group input-group-sm me-3\" style=\"width: 300px;\">\n <span class=\"input-group-text\">\n <i class=\"bi bi-funnel text-muted\"></i>\n </span>\n <input\n type=\"text\"\n class=\"form-control\"\n placeholder=\"Filter by column name or template...\"\n [value]=\"columnFilter()\"\n (input)=\"onColumnFilterChange($any($event.target).value)\">\n @if (columnFilter()) {\n <button\n type=\"button\"\n class=\"btn btn-outline-secondary\"\n (click)=\"clearColumnFilter()\"\n title=\"Clear filter\">\n <i class=\"bi bi-x\"></i>\n </button>\n }\n </div>\n @if (columnFilter()) {\n <small class=\"text-muted\">\n {{ filteredTemplatesCount }}/{{ totalTemplatesCount }} templates\n </small>\n }\n </div>\n }\n </div>\n <div class=\"card-body p-0\">\n @if (hasTemplates()) {\n <div class=\"table-responsive\">\n <table class=\"table table-hover mb-0\">\n <thead>\n <tr>\n <th class=\"sortable\" \n (click)=\"sortBy('name')\"\n [class.sorted]=\"isSorted('name')\">\n <div class=\"d-flex align-items-center justify-content-between\">\n <span><i class=\"bi bi-tag me-1\"></i>Template Name</span>\n <i class=\"bi ms-1\" [class]=\"getSortIcon('name')\"></i>\n </div>\n </th>\n <th class=\"sortable\" \n (click)=\"sortBy('columnName')\"\n [class.sorted]=\"isSorted('columnName')\">\n <div class=\"d-flex align-items-center justify-content-between\">\n <span><i class=\"bi bi-list me-1\"></i>Column Name</span>\n <i class=\"bi ms-1\" [class]=\"getSortIcon('columnName')\"></i>\n </div>\n </th>\n <th class=\"sortable\" \n (click)=\"sortBy('columnType')\"\n [class.sorted]=\"isSorted('columnType')\">\n <div class=\"d-flex align-items-center justify-content-between\">\n <span><i class=\"bi bi-layers me-1\"></i>Type</span>\n <i class=\"bi ms-1\" [class]=\"getSortIcon('columnType')\"></i>\n </div>\n </th>\n <th class=\"sortable\" \n (click)=\"sortBy('ontologyType')\"\n [class.sorted]=\"isSorted('ontologyType')\">\n <div class=\"d-flex align-items-center justify-content-between\">\n <span><i class=\"bi bi-diagram-3 me-1\"></i>Ontology</span>\n <i class=\"bi ms-1\" [class]=\"getSortIcon('ontologyType')\"></i>\n </div>\n </th>\n <th class=\"sortable\" \n (click)=\"sortBy('visibility')\"\n [class.sorted]=\"isSorted('visibility')\">\n <div class=\"d-flex align-items-center justify-content-between\">\n <span><i class=\"bi bi-globe me-1\"></i>Visibility</span>\n <i class=\"bi ms-1\" [class]=\"getSortIcon('visibility')\"></i>\n </div>\n </th>\n <th><i class=\"bi bi-folder me-1\"></i>Category</th>\n <th><i class=\"bi bi-gear me-1\"></i>Actions</th>\n </tr>\n </thead>\n <tbody>\n @for (template of filteredTemplates(); track template.id) {\n <tr>\n <td>\n <strong>{{ template.name }}</strong>\n @if (template.isSystemTemplate) {\n <i class=\"bi bi-shield-check text-success ms-2\" title=\"System template\"></i>\n }\n </td>\n <td>{{ template.columnName }}</td>\n <td>\n <span class=\"badge bg-secondary\">{{ template.columnType }}</span>\n </td>\n <td>\n @if (template.ontologyType) {\n <span class=\"badge bg-success\">{{ template.ontologyType }}</span>\n } @else {\n <span class=\"text-muted\">-</span>\n }\n </td>\n <td>\n @switch (template.visibility) {\n @case ('public') {\n <span class=\"badge bg-success\">Public</span>\n }\n @case ('group') {\n <span class=\"badge bg-warning\">Lab Group</span>\n }\n @case ('private') {\n <span class=\"badge bg-secondary\">Private</span>\n }\n @default {\n <span class=\"badge bg-secondary\">{{ template.visibility }}</span>\n }\n }\n </td>\n <td>\n @if (template.category) {\n <span class=\"badge bg-secondary\">{{ template.category }}</span>\n } @else {\n <span class=\"text-muted\">-</span>\n }\n </td>\n <td>\n <div class=\"btn-group btn-group-sm\" role=\"group\">\n <button\n class=\"btn btn-outline-primary\"\n (click)=\"editTemplate(template)\"\n title=\"Edit template\">\n <i class=\"bi bi-pencil\"></i>\n </button>\n <button\n class=\"btn btn-outline-info\"\n (click)=\"duplicateTemplate(template)\"\n title=\"Duplicate template\">\n <i class=\"bi bi-copy\"></i>\n </button>\n <button\n class=\"btn btn-outline-danger\"\n (click)=\"deleteTemplate(template)\"\n title=\"Delete template\">\n <i class=\"bi bi-trash\"></i>\n </button>\n </div>\n </td>\n </tr>\n }\n </tbody>\n </table>\n </div>\n\n <!-- Pagination -->\n @if (showTemplatesPagination()) {\n <div class=\"d-flex justify-content-center mt-3 p-3\">\n <ngb-pagination\n [collectionSize]=\"templatesData().count\"\n [page]=\"currentPage()\"\n [pageSize]=\"pageSize()\"\n [maxSize]=\"5\"\n [boundaryLinks]=\"true\"\n [rotate]=\"true\"\n (pageChange)=\"onPageChange($event)\"\n class=\"pagination-sm\">\n </ngb-pagination>\n </div>\n }\n } @else {\n <div class=\"empty-state text-center py-5\">\n <i class=\"bi bi-clipboard-list fs-1 text-muted mb-3\"></i>\n <h5 class=\"text-muted\">No Column Templates Found</h5>\n <p class=\"text-muted\">\n @if (searchForm.get('search')?.value || searchForm.get('labGroupId')?.value) {\n Try adjusting your search criteria or create a new template.\n } @else {\n Get started by creating your first column template.\n }\n </p>\n <button class=\"btn btn-primary\" (click)=\"createTemplate()\">\n <i class=\"bi bi-plus me-1\"></i>Create First Template\n </button>\n </div>\n }\n </div>\n </div>\n </div>\n</div>\n", styles: [".sortable{cursor:pointer;user-select:none;transition:background-color .2s ease}.sortable:hover{background-color:rgba(var(--bs-primary-rgb),.1)}.sortable.sorted{background-color:rgba(var(--bs-primary-rgb),.15)}.sortable.sorted .bi{color:var(--bs-primary)}.sortable .bi{opacity:.5;transition:all .2s ease;font-size:.75em}.sortable:hover .bi{opacity:.8}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.CheckboxControlValueAccessor, selector: "input[type=checkbox][formControlName],input[type=checkbox][formControl],input[type=checkbox][ngModel]" }, { kind: "directive", type: i1.SelectControlValueAccessor, selector: "select:not([multiple])[formControlName],select:not([multiple])[formControl],select:not([multiple])[ngModel]", inputs: ["compareWith"] }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i1.FormControlName, selector: "[formControlName]", inputs: ["formControlName", "disabled", "ngModel"], outputs: ["ngModelChange"] }, { kind: "ngmodule", type: NgbModule }, { kind: "component", type: i2.NgbPagination, selector: "ngb-pagination", inputs: ["disabled", "boundaryLinks", "directionLinks", "ellipses", "rotate", "collectionSize", "maxSize", "page", "pageSize", "size"], outputs: ["pageChange"] }, { kind: "ngmodule", type: NgbPaginationModule }] });
6667
+ }
6668
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.3", ngImport: i0, type: ColumnTemplates, decorators: [{
6669
+ type: Component,
6670
+ args: [{ selector: 'ccv-column-templates', standalone: true, imports: [CommonModule, ReactiveFormsModule, NgbModule, NgbPaginationModule], template: "<div class=\"column-templates-container\">\n <!-- Header Section -->\n <nav class=\"navbar navbar-expand-lg bg-body-tertiary shadow-sm\">\n <div class=\"container-fluid\">\n <div class=\"d-flex align-items-center justify-content-between w-100\">\n <div class=\"d-flex align-items-center\">\n <h4 class=\"navbar-brand m-0 fw-bold\">\n <i class=\"bi bi-clipboard-list me-2\"></i>Column Templates\n </h4>\n @if (selectedLabGroup()) {\n <span class=\"badge bg-primary ms-3\">{{ selectedLabGroup()!.name }}</span>\n }\n </div>\n <div class=\"d-flex gap-2\">\n <button class=\"btn btn-primary btn-sm\" (click)=\"createTemplate()\">\n <i class=\"bi bi-plus me-1\"></i>New Template\n </button>\n </div>\n </div>\n </div>\n </nav>\n\n <div class=\"templates-content p-4\">\n <!-- Search and Filters Section -->\n <div class=\"card mb-4\">\n <div class=\"card-header\">\n <h5 class=\"mb-0\">\n <i class=\"bi bi-search me-2\"></i>Search & Filters\n </h5>\n </div>\n <div class=\"card-body\">\n <form [formGroup]=\"searchForm\" class=\"row g-3\">\n <div class=\"col-md-4\">\n <div class=\"form-floating\">\n <input\n type=\"search\"\n class=\"form-control\"\n id=\"templateSearch\"\n formControlName=\"search\"\n placeholder=\"Search templates...\">\n <label for=\"templateSearch\">\n <i class=\"bi bi-search me-1\"></i>Search Templates\n </label>\n </div>\n </div>\n <div class=\"col-md-4\">\n <div class=\"form-floating\">\n <select\n class=\"form-select\"\n id=\"labGroupSelect\"\n formControlName=\"labGroupId\"\n (change)=\"onLabGroupChange($event)\">\n <option [ngValue]=\"null\">All Lab Groups</option>\n @if (hasLabGroups()) {\n @for (group of labGroupsData().results; track group.id) {\n <option [ngValue]=\"group.id\">{{ group.name }}</option>\n }\n }\n </select>\n <label for=\"labGroupSelect\">Lab Group</label>\n </div>\n\n <!-- Lab Groups Pagination -->\n @if (showLabGroupsPagination()) {\n <div class=\"d-flex justify-content-center mt-2\">\n <ngb-pagination\n [collectionSize]=\"labGroupsData().count\"\n [page]=\"labGroupsCurrentPage()\"\n [pageSize]=\"labGroupsPageSize()\"\n [maxSize]=\"3\"\n [boundaryLinks]=\"false\"\n [rotate]=\"true\"\n (pageChange)=\"onLabGroupsPageChange($event)\"\n class=\"pagination-sm\">\n </ngb-pagination>\n </div>\n }\n </div>\n <div class=\"col-md-4\">\n <div class=\"form-check form-switch mt-3\">\n <input\n class=\"form-check-input\"\n type=\"checkbox\"\n id=\"globalTemplates\"\n formControlName=\"visibility\">\n <label class=\"form-check-label\" for=\"globalTemplates\">\n <i class=\"bi bi-globe me-1\"></i>Global Templates Only\n </label>\n </div>\n </div>\n </form>\n </div>\n </div>\n\n <!-- Templates Table -->\n <div class=\"card\">\n <div class=\"card-header\">\n <div class=\"d-flex justify-content-between align-items-center mb-3\">\n <h5 class=\"mb-0\">\n <i class=\"bi bi-table me-2\"></i>Column Templates\n <span class=\"badge bg-primary ms-2\">{{ templatesData().count }}</span>\n </h5>\n @if (isLoading()) {\n <div class=\"spinner-border spinner-border-sm text-primary\" role=\"status\">\n <span class=\"visually-hidden\">Loading...</span>\n </div>\n }\n </div>\n \n <!-- Column Filter -->\n @if (hasTemplates()) {\n <div class=\"d-flex align-items-center\">\n <div class=\"input-group input-group-sm me-3\" style=\"width: 300px;\">\n <span class=\"input-group-text\">\n <i class=\"bi bi-funnel text-muted\"></i>\n </span>\n <input\n type=\"text\"\n class=\"form-control\"\n placeholder=\"Filter by column name or template...\"\n [value]=\"columnFilter()\"\n (input)=\"onColumnFilterChange($any($event.target).value)\">\n @if (columnFilter()) {\n <button\n type=\"button\"\n class=\"btn btn-outline-secondary\"\n (click)=\"clearColumnFilter()\"\n title=\"Clear filter\">\n <i class=\"bi bi-x\"></i>\n </button>\n }\n </div>\n @if (columnFilter()) {\n <small class=\"text-muted\">\n {{ filteredTemplatesCount }}/{{ totalTemplatesCount }} templates\n </small>\n }\n </div>\n }\n </div>\n <div class=\"card-body p-0\">\n @if (hasTemplates()) {\n <div class=\"table-responsive\">\n <table class=\"table table-hover mb-0\">\n <thead>\n <tr>\n <th class=\"sortable\" \n (click)=\"sortBy('name')\"\n [class.sorted]=\"isSorted('name')\">\n <div class=\"d-flex align-items-center justify-content-between\">\n <span><i class=\"bi bi-tag me-1\"></i>Template Name</span>\n <i class=\"bi ms-1\" [class]=\"getSortIcon('name')\"></i>\n </div>\n </th>\n <th class=\"sortable\" \n (click)=\"sortBy('columnName')\"\n [class.sorted]=\"isSorted('columnName')\">\n <div class=\"d-flex align-items-center justify-content-between\">\n <span><i class=\"bi bi-list me-1\"></i>Column Name</span>\n <i class=\"bi ms-1\" [class]=\"getSortIcon('columnName')\"></i>\n </div>\n </th>\n <th class=\"sortable\" \n (click)=\"sortBy('columnType')\"\n [class.sorted]=\"isSorted('columnType')\">\n <div class=\"d-flex align-items-center justify-content-between\">\n <span><i class=\"bi bi-layers me-1\"></i>Type</span>\n <i class=\"bi ms-1\" [class]=\"getSortIcon('columnType')\"></i>\n </div>\n </th>\n <th class=\"sortable\" \n (click)=\"sortBy('ontologyType')\"\n [class.sorted]=\"isSorted('ontologyType')\">\n <div class=\"d-flex align-items-center justify-content-between\">\n <span><i class=\"bi bi-diagram-3 me-1\"></i>Ontology</span>\n <i class=\"bi ms-1\" [class]=\"getSortIcon('ontologyType')\"></i>\n </div>\n </th>\n <th class=\"sortable\" \n (click)=\"sortBy('visibility')\"\n [class.sorted]=\"isSorted('visibility')\">\n <div class=\"d-flex align-items-center justify-content-between\">\n <span><i class=\"bi bi-globe me-1\"></i>Visibility</span>\n <i class=\"bi ms-1\" [class]=\"getSortIcon('visibility')\"></i>\n </div>\n </th>\n <th><i class=\"bi bi-folder me-1\"></i>Category</th>\n <th><i class=\"bi bi-gear me-1\"></i>Actions</th>\n </tr>\n </thead>\n <tbody>\n @for (template of filteredTemplates(); track template.id) {\n <tr>\n <td>\n <strong>{{ template.name }}</strong>\n @if (template.isSystemTemplate) {\n <i class=\"bi bi-shield-check text-success ms-2\" title=\"System template\"></i>\n }\n </td>\n <td>{{ template.columnName }}</td>\n <td>\n <span class=\"badge bg-secondary\">{{ template.columnType }}</span>\n </td>\n <td>\n @if (template.ontologyType) {\n <span class=\"badge bg-success\">{{ template.ontologyType }}</span>\n } @else {\n <span class=\"text-muted\">-</span>\n }\n </td>\n <td>\n @switch (template.visibility) {\n @case ('public') {\n <span class=\"badge bg-success\">Public</span>\n }\n @case ('group') {\n <span class=\"badge bg-warning\">Lab Group</span>\n }\n @case ('private') {\n <span class=\"badge bg-secondary\">Private</span>\n }\n @default {\n <span class=\"badge bg-secondary\">{{ template.visibility }}</span>\n }\n }\n </td>\n <td>\n @if (template.category) {\n <span class=\"badge bg-secondary\">{{ template.category }}</span>\n } @else {\n <span class=\"text-muted\">-</span>\n }\n </td>\n <td>\n <div class=\"btn-group btn-group-sm\" role=\"group\">\n <button\n class=\"btn btn-outline-primary\"\n (click)=\"editTemplate(template)\"\n title=\"Edit template\">\n <i class=\"bi bi-pencil\"></i>\n </button>\n <button\n class=\"btn btn-outline-info\"\n (click)=\"duplicateTemplate(template)\"\n title=\"Duplicate template\">\n <i class=\"bi bi-copy\"></i>\n </button>\n <button\n class=\"btn btn-outline-danger\"\n (click)=\"deleteTemplate(template)\"\n title=\"Delete template\">\n <i class=\"bi bi-trash\"></i>\n </button>\n </div>\n </td>\n </tr>\n }\n </tbody>\n </table>\n </div>\n\n <!-- Pagination -->\n @if (showTemplatesPagination()) {\n <div class=\"d-flex justify-content-center mt-3 p-3\">\n <ngb-pagination\n [collectionSize]=\"templatesData().count\"\n [page]=\"currentPage()\"\n [pageSize]=\"pageSize()\"\n [maxSize]=\"5\"\n [boundaryLinks]=\"true\"\n [rotate]=\"true\"\n (pageChange)=\"onPageChange($event)\"\n class=\"pagination-sm\">\n </ngb-pagination>\n </div>\n }\n } @else {\n <div class=\"empty-state text-center py-5\">\n <i class=\"bi bi-clipboard-list fs-1 text-muted mb-3\"></i>\n <h5 class=\"text-muted\">No Column Templates Found</h5>\n <p class=\"text-muted\">\n @if (searchForm.get('search')?.value || searchForm.get('labGroupId')?.value) {\n Try adjusting your search criteria or create a new template.\n } @else {\n Get started by creating your first column template.\n }\n </p>\n <button class=\"btn btn-primary\" (click)=\"createTemplate()\">\n <i class=\"bi bi-plus me-1\"></i>Create First Template\n </button>\n </div>\n }\n </div>\n </div>\n </div>\n</div>\n", styles: [".sortable{cursor:pointer;user-select:none;transition:background-color .2s ease}.sortable:hover{background-color:rgba(var(--bs-primary-rgb),.1)}.sortable.sorted{background-color:rgba(var(--bs-primary-rgb),.15)}.sortable.sorted .bi{color:var(--bs-primary)}.sortable .bi{opacity:.5;transition:all .2s ease;font-size:.75em}.sortable:hover .bi{opacity:.8}\n"] }]
6671
+ }], ctorParameters: () => [{ type: i1.FormBuilder }, { type: MetadataColumnTemplateService }, { type: i3.LabGroupService }, { type: i2.NgbModal }] });
6672
+
6673
+ class FavoriteManagement {
6674
+ fb;
6675
+ favouriteMetadataOptionService;
6676
+ metadataColumnTemplateService;
6677
+ labGroupService;
6678
+ toastService;
6679
+ modalService;
6680
+ authService;
6681
+ Math = Math;
6682
+ isLoading = signal(false, ...(ngDevMode ? [{ debugName: "isLoading" }] : []));
6683
+ favorites = signal([], ...(ngDevMode ? [{ debugName: "favorites" }] : []));
6684
+ totalFavoritesCount = signal(0, ...(ngDevMode ? [{ debugName: "totalFavoritesCount" }] : []));
6685
+ searchTerm = signal('', ...(ngDevMode ? [{ debugName: "searchTerm" }] : []));
6686
+ scopeFilter = signal('', ...(ngDevMode ? [{ debugName: "scopeFilter" }] : []));
6687
+ labGroupFilter = signal('', ...(ngDevMode ? [{ debugName: "labGroupFilter" }] : []));
6688
+ columnTypeFilter = signal('', ...(ngDevMode ? [{ debugName: "columnTypeFilter" }] : []));
6689
+ filteredFavorites = computed(() => {
6690
+ const searchTerm = this.searchTerm().toLowerCase();
6691
+ const scopeFilter = this.scopeFilter();
6692
+ const labGroupFilter = this.labGroupFilter();
6693
+ const columnTypeFilter = this.columnTypeFilter();
6694
+ return this.favorites().filter(fav => {
6695
+ const matchesSearch = !searchTerm ||
6696
+ fav.name.toLowerCase().includes(searchTerm) ||
6697
+ fav.value.toLowerCase().includes(searchTerm) ||
6698
+ (fav.displayValue?.toLowerCase().includes(searchTerm) ?? false);
6699
+ let matchesScope = true;
6700
+ if (scopeFilter) {
6701
+ if (scopeFilter === 'personal') {
6702
+ matchesScope = !!(fav.user && !fav.isGlobal && !fav.labGroup);
6703
+ }
6704
+ else if (scopeFilter === 'labGroup') {
6705
+ matchesScope = !!(fav.labGroup && !fav.isGlobal);
6706
+ }
6707
+ else if (scopeFilter === 'global') {
6708
+ matchesScope = !!fav.isGlobal;
6709
+ }
6710
+ }
6711
+ const matchesLabGroup = !labGroupFilter ||
6712
+ (fav.labGroup && fav.labGroup.toString() === labGroupFilter);
6713
+ const matchesType = !columnTypeFilter || fav.type.includes(columnTypeFilter);
6714
+ return matchesSearch && matchesScope && matchesLabGroup && matchesType;
6715
+ });
6716
+ }, ...(ngDevMode ? [{ debugName: "filteredFavorites" }] : []));
6717
+ currentPage = signal(1, ...(ngDevMode ? [{ debugName: "currentPage" }] : []));
6718
+ pageSize = signal(10, ...(ngDevMode ? [{ debugName: "pageSize" }] : []));
6719
+ paginatedFavorites = computed(() => this.filteredFavorites(), ...(ngDevMode ? [{ debugName: "paginatedFavorites" }] : []));
6720
+ totalPages = computed(() => {
6721
+ const totalCount = this.totalFavoritesCount();
6722
+ const size = this.pageSize();
6723
+ return Math.ceil(totalCount / size);
6724
+ }, ...(ngDevMode ? [{ debugName: "totalPages" }] : []));
6725
+ searchForm;
6726
+ editForm;
6727
+ isEditMode = signal(false, ...(ngDevMode ? [{ debugName: "isEditMode" }] : []));
6728
+ editingFavorite = signal(null, ...(ngDevMode ? [{ debugName: "editingFavorite" }] : []));
6729
+ showEditModal = signal(false, ...(ngDevMode ? [{ debugName: "showEditModal" }] : []));
6730
+ sdrfSyntaxService = inject(SdrfSyntaxService);
6731
+ specialSyntaxType = signal(null, ...(ngDevMode ? [{ debugName: "specialSyntaxType" }] : []));
6732
+ showSpecialInput = signal(false, ...(ngDevMode ? [{ debugName: "showSpecialInput" }] : []));
6733
+ specialInputValue = signal('', ...(ngDevMode ? [{ debugName: "specialInputValue" }] : []));
6734
+ currentUser = signal(null, ...(ngDevMode ? [{ debugName: "currentUser" }] : []));
6735
+ isAdmin = computed(() => {
6736
+ const user = this.currentUser();
6737
+ return user?.isStaff || user?.isSuperuser || false;
6738
+ }, ...(ngDevMode ? [{ debugName: "isAdmin" }] : []));
6739
+ currentUserId = computed(() => this.currentUser()?.id || null, ...(ngDevMode ? [{ debugName: "currentUserId" }] : []));
6740
+ userLabGroups = signal([], ...(ngDevMode ? [{ debugName: "userLabGroups" }] : []));
6741
+ userLabGroupsLoaded = signal(false, ...(ngDevMode ? [{ debugName: "userLabGroupsLoaded" }] : []));
6742
+ selectedOntologyValue = null;
6743
+ availableLabGroups = signal([], ...(ngDevMode ? [{ debugName: "availableLabGroups" }] : []));
6744
+ allLabGroups = signal([], ...(ngDevMode ? [{ debugName: "allLabGroups" }] : []));
6745
+ availableColumnTemplates = signal([], ...(ngDevMode ? [{ debugName: "availableColumnTemplates" }] : []));
6746
+ columnTemplatesLoaded = signal(false, ...(ngDevMode ? [{ debugName: "columnTemplatesLoaded" }] : []));
6747
+ columnTypes = [
6748
+ { value: '', label: 'All Types' },
6749
+ { value: ColumnType.CHARACTERISTICS, label: 'Characteristics' },
6750
+ { value: ColumnType.FACTOR_VALUE, label: 'Factor Value' },
6751
+ { value: ColumnType.COMMENT, label: 'Comment' },
6752
+ { value: ColumnType.SOURCE_NAME, label: 'Source Name' },
6753
+ { value: ColumnType.SPECIAL, label: 'Special' }
6754
+ ];
6755
+ constructor(fb, favouriteMetadataOptionService, metadataColumnTemplateService, labGroupService, toastService, modalService, authService) {
6756
+ this.fb = fb;
6757
+ this.favouriteMetadataOptionService = favouriteMetadataOptionService;
6758
+ this.metadataColumnTemplateService = metadataColumnTemplateService;
6759
+ this.labGroupService = labGroupService;
6760
+ this.toastService = toastService;
6761
+ this.modalService = modalService;
6762
+ this.authService = authService;
6763
+ this.searchForm = this.fb.group({
6764
+ search: [''],
6765
+ scope: [''],
6766
+ labGroup: [''],
6767
+ columnType: ['']
6768
+ });
6769
+ this.editForm = this.fb.group({
6770
+ name: ['', [Validators.required, Validators.maxLength(255)]],
6771
+ type: ['', [Validators.required, Validators.maxLength(255)]],
6772
+ value: ['', [Validators.maxLength(500)]],
6773
+ displayValue: ['', [Validators.maxLength(500)]],
6774
+ scope: ['personal', Validators.required],
6775
+ labGroupId: [null],
6776
+ templateId: [null],
6777
+ ontologyType: [''],
6778
+ enableTypeahead: [false]
6779
+ });
6780
+ this.editForm.get('value')?.valueChanges.subscribe(value => {
6781
+ if (typeof value === 'string' && value !== this.selectedOntologyValue) {
6782
+ console.log('Manual input detected, clearing stored ontology value');
6783
+ this.selectedOntologyValue = null;
6784
+ }
6785
+ });
6786
+ }
6787
+ ngOnInit() {
6788
+ this.currentUser.set(this.authService.getCurrentUser());
6789
+ this.authService.currentUser$.subscribe(user => {
6790
+ this.currentUser.set(user);
6791
+ if (user) {
6792
+ this.loadUserLabGroups();
6793
+ }
6794
+ else {
6795
+ this.loadFavorites();
6796
+ }
6797
+ });
6798
+ if (this.currentUser()) {
6799
+ this.loadUserLabGroups();
6800
+ }
6801
+ else {
6802
+ this.loadFavorites();
6803
+ }
6804
+ this.loadAvailableColumnTemplates();
6805
+ this.searchForm.valueChanges.subscribe(formValues => {
6806
+ this.searchTerm.set(formValues.search || '');
6807
+ this.scopeFilter.set(formValues.scope || '');
6808
+ this.labGroupFilter.set(formValues.labGroup || '');
6809
+ this.columnTypeFilter.set(formValues.columnType || '');
6810
+ this.currentPage.set(1);
6811
+ });
6812
+ }
6813
+ loadFavorites() {
6814
+ this.isLoading.set(true);
6815
+ const page = this.currentPage();
6816
+ const size = this.pageSize();
6817
+ const offset = (page - 1) * size;
6818
+ this.favouriteMetadataOptionService.getFavouriteMetadataOptions({
6819
+ limit: size,
6820
+ offset: offset
6821
+ }).subscribe({
6822
+ next: (response) => {
6823
+ this.favorites.set(response.results);
6824
+ this.totalFavoritesCount.set(response.count || response.results.length);
6825
+ this.isLoading.set(false);
6826
+ },
6827
+ error: (error) => {
6828
+ console.error('Error loading favorites:', error);
6829
+ this.toastService.error('Failed to load favorites');
6830
+ this.isLoading.set(false);
6831
+ }
6832
+ });
6833
+ }
6834
+ canEditFavorite(favorite) {
6835
+ const userId = this.currentUserId();
6836
+ if (favorite.user && userId && Number(favorite.user) === Number(userId)) {
6837
+ return true;
6838
+ }
6839
+ if (favorite.isGlobal && this.isAdmin()) {
6840
+ return true;
6841
+ }
6842
+ return false;
6843
+ }
6844
+ canDeleteFavorite(favorite) {
6845
+ return this.canEditFavorite(favorite);
6846
+ }
6847
+ openCreateModal() {
6848
+ this.isEditMode.set(false);
6849
+ this.editingFavorite.set(null);
6850
+ this.showEditModal.set(true);
6851
+ this.selectedOntologyValue = null;
6852
+ this.editForm.reset({
6853
+ name: '',
6854
+ type: '',
6855
+ value: '',
6856
+ displayValue: '',
6857
+ scope: 'personal',
6858
+ templateId: null
6859
+ });
6860
+ }
6861
+ openEditModal(favorite) {
6862
+ if (!this.canEditFavorite(favorite))
6863
+ return;
6864
+ this.isEditMode.set(true);
6865
+ this.editingFavorite.set(favorite);
6866
+ this.showEditModal.set(true);
6867
+ this.selectedOntologyValue = favorite.value; // Store the original value
6868
+ let scope = 'personal';
6869
+ if (favorite.isGlobal)
6870
+ scope = 'global';
6871
+ else if (favorite.labGroup)
6872
+ scope = 'labGroup';
6873
+ let templateId = favorite.columnTemplate || null;
6874
+ this.editForm.patchValue({
6875
+ name: favorite.name,
6876
+ type: favorite.type,
6877
+ value: favorite.value,
6878
+ displayValue: favorite.displayValue || '',
6879
+ scope: scope,
6880
+ labGroupId: favorite.labGroup || null,
6881
+ templateId: templateId,
6882
+ ontologyType: '',
6883
+ enableTypeahead: false
6884
+ });
6885
+ const syntaxType = this.sdrfSyntaxService.detectSpecialSyntax(favorite.name, favorite.type);
6886
+ if (syntaxType) {
6887
+ this.specialSyntaxType.set(syntaxType);
6888
+ this.showSpecialInput.set(true);
6889
+ this.specialInputValue.set(favorite.value || '');
6890
+ }
6891
+ else {
6892
+ this.specialSyntaxType.set(null);
6893
+ this.showSpecialInput.set(false);
6894
+ this.specialInputValue.set('');
6895
+ }
6896
+ if (!templateId) {
6897
+ this.findTemplatesForColumnName(favorite.name).then(templates => {
6898
+ const matchingTemplate = templates.find(t => t.columnName === favorite.name && t.columnType === favorite.type);
6899
+ if (matchingTemplate?.id) {
6900
+ this.editForm.patchValue({
6901
+ templateId: matchingTemplate.id,
6902
+ ontologyType: matchingTemplate.ontologyType || '',
6903
+ enableTypeahead: matchingTemplate.enableTypeahead || false
6904
+ });
6905
+ }
6906
+ });
6907
+ }
6908
+ }
6909
+ saveFavorite() {
6910
+ if (this.editForm.invalid)
6911
+ return;
6912
+ const formValue = this.editForm.value;
6913
+ let value = this.selectedOntologyValue || formValue.value;
6914
+ if (typeof value === 'object' && value !== null) {
6915
+ value = value.value || value.displayName || String(value);
6916
+ }
6917
+ const favoriteData = {
6918
+ name: formValue.name,
6919
+ type: formValue.type,
6920
+ columnTemplate: formValue.templateId || undefined,
6921
+ value: value,
6922
+ displayValue: formValue.displayValue || value,
6923
+ isGlobal: formValue.scope === 'global',
6924
+ user: formValue.scope === 'personal' ? this.currentUserId() || undefined : undefined,
6925
+ labGroup: formValue.scope === 'labGroup' ? formValue.labGroupId || this.userLabGroups()[0]?.id : undefined
6926
+ };
6927
+ const operation = this.isEditMode()
6928
+ ? this.favouriteMetadataOptionService.updateFavouriteMetadataOption(this.editingFavorite().id, favoriteData)
6929
+ : this.favouriteMetadataOptionService.createFavouriteMetadataOption(favoriteData);
6930
+ operation.subscribe({
6931
+ next: (favorite) => {
6932
+ const action = this.isEditMode() ? 'updated' : 'created';
6933
+ this.toastService.success(`Favorite ${action} successfully!`);
6934
+ this.loadFavorites();
6935
+ this.closeEditModal();
6936
+ },
6937
+ error: (error) => {
6938
+ console.error('Error saving favorite:', error);
6939
+ const action = this.isEditMode() ? 'updating' : 'creating';
6940
+ this.toastService.error(`Failed ${action} favorite`);
6941
+ }
6942
+ });
6943
+ }
6944
+ deleteFavorite(favorite) {
6945
+ if (!this.canDeleteFavorite(favorite))
6946
+ return;
6947
+ const confirmMessage = `Are you sure you want to delete the favorite "${favorite.displayValue || favorite.value}"?`;
6948
+ if (confirm(confirmMessage)) {
6949
+ this.favouriteMetadataOptionService.deleteFavouriteMetadataOption(favorite.id).subscribe({
6950
+ next: () => {
6951
+ this.toastService.success('Favorite deleted successfully!');
6952
+ this.loadFavorites();
6953
+ },
6954
+ error: (error) => {
6955
+ console.error('Error deleting favorite:', error);
6956
+ this.toastService.error('Failed to delete favorite');
6957
+ }
6958
+ });
6959
+ }
6960
+ }
6961
+ closeEditModal() {
6962
+ this.isEditMode.set(false);
6963
+ this.editingFavorite.set(null);
6964
+ this.showEditModal.set(false);
6965
+ this.selectedOntologyValue = null; // Clear stored ontology value
6966
+ }
6967
+ onPageChange(page) {
6968
+ this.currentPage.set(page);
6969
+ this.loadFavorites();
6970
+ }
6971
+ onPageSizeChange(size) {
6972
+ this.pageSize.set(size);
6973
+ this.currentPage.set(1);
6974
+ this.loadFavorites();
6975
+ }
6976
+ clearFilters() {
6977
+ this.searchForm.reset();
6978
+ this.searchTerm.set('');
6979
+ this.scopeFilter.set('');
6980
+ this.labGroupFilter.set('');
6981
+ this.columnTypeFilter.set('');
6982
+ this.currentPage.set(1);
6983
+ }
6984
+ getScopeDisplay(favorite) {
6985
+ if (favorite.isGlobal)
6986
+ return 'Global';
6987
+ if (favorite.labGroup)
6988
+ return `Lab Group: ${favorite.labGroupName || favorite.labGroup}`;
6989
+ if (favorite.user)
6990
+ return `Personal (${favorite.userUsername || favorite.user})`;
6991
+ return 'Unknown';
6992
+ }
6993
+ getScopeClass(favorite) {
6994
+ if (favorite.isGlobal)
6995
+ return 'badge bg-success';
6996
+ if (favorite.labGroup)
6997
+ return 'badge bg-info';
6998
+ if (favorite.user)
6999
+ return 'badge bg-primary';
7000
+ return 'badge bg-secondary';
7001
+ }
7002
+ get editModalTitle() {
7003
+ return this.isEditMode() ? 'Edit Favorite' : 'Create New Favorite';
7004
+ }
7005
+ loadUserLabGroups() {
7006
+ this.labGroupService.getMyLabGroups({ limit: 10 }).subscribe({
7007
+ next: (response) => {
7008
+ this.userLabGroups.set(response.results);
7009
+ const labGroups = response.results.map(group => ({
7010
+ id: group.id,
7011
+ name: group.name
7012
+ }));
7013
+ this.availableLabGroups.set(labGroups);
7014
+ this.userLabGroupsLoaded.set(true);
7015
+ this.loadFavorites();
7016
+ },
7017
+ error: (error) => {
7018
+ console.error('Error loading user lab groups:', error);
7019
+ this.userLabGroups.set([]);
7020
+ this.availableLabGroups.set([]);
7021
+ this.userLabGroupsLoaded.set(true);
7022
+ this.loadFavorites();
7023
+ }
7024
+ });
7025
+ this.labGroupService.getLabGroups({ limit: 10 }).subscribe({
7026
+ next: (response) => {
7027
+ const allLabGroups = response.results.map(group => ({
7028
+ id: group.id,
7029
+ name: group.name
7030
+ }));
7031
+ this.allLabGroups.set(allLabGroups);
7032
+ if (this.isAdmin()) {
7033
+ this.availableLabGroups.set(allLabGroups);
7034
+ }
7035
+ },
7036
+ error: (error) => {
7037
+ console.error('Error loading all lab groups:', error);
7038
+ }
7039
+ });
7040
+ }
7041
+ loadAvailableColumnTemplates() {
7042
+ this.metadataColumnTemplateService.getMetadataColumnTemplates({ limit: 10 }).subscribe({
7043
+ next: (response) => {
7044
+ this.availableColumnTemplates.set(response.results);
7045
+ this.columnTemplatesLoaded.set(true);
7046
+ },
7047
+ error: (error) => {
7048
+ console.error('Error loading column templates:', error);
7049
+ this.availableColumnTemplates.set([]);
7050
+ this.columnTemplatesLoaded.set(true);
7051
+ }
7052
+ });
7053
+ }
7054
+ searchColumnNames = (text$) => text$.pipe(debounceTime(200), distinctUntilChanged(), switchMap$1(term => {
7055
+ if (term.length === 0) {
7056
+ return of(this.getColumnNameSuggestions(''));
7057
+ }
7058
+ else if (term.length >= 1) {
7059
+ return this.metadataColumnTemplateService.getMetadataColumnTemplates({
7060
+ search: term,
7061
+ limit: 10
7062
+ }).pipe(map$1(response => {
7063
+ const uniqueNames = [...new Set(response.results.map((template) => template.columnName))];
7064
+ return uniqueNames.sort();
7065
+ }), catchError(() => {
7066
+ return of(this.getColumnNameSuggestions(term));
7067
+ }));
7068
+ }
7069
+ else {
7070
+ return of([]);
7071
+ }
7072
+ }));
7073
+ getColumnNameSuggestions(term) {
7074
+ const templates = this.availableColumnTemplates();
7075
+ const uniqueNames = [...new Set(templates.map(template => template.columnName))];
7076
+ if (!term) {
7077
+ return uniqueNames.sort().slice(0, 10);
7078
+ }
7079
+ return uniqueNames
7080
+ .filter(name => name.toLowerCase().includes(term.toLowerCase()))
7081
+ .sort()
7082
+ .slice(0, 10);
7083
+ }
7084
+ searchColumnTypes = (text$) => text$.pipe(debounceTime(200), distinctUntilChanged(), filter(term => term.length >= 1), map$1(term => this.getColumnTypeSuggestions(term)));
7085
+ getColumnTypeSuggestions(term) {
7086
+ const selectedName = this.editForm.get('name')?.value;
7087
+ const templates = this.availableColumnTemplates();
7088
+ let availableTypes;
7089
+ if (selectedName) {
7090
+ availableTypes = templates
7091
+ .filter(template => template.columnName === selectedName)
7092
+ .map(template => template.columnType);
7093
+ }
7094
+ else {
7095
+ availableTypes = [...new Set(templates.map(template => template.columnType))];
7096
+ }
7097
+ return availableTypes
7098
+ .filter(type => type.toLowerCase().includes(term.toLowerCase()))
7099
+ .sort()
7100
+ .slice(0, 10);
7101
+ }
7102
+ onColumnNameSelected(event) {
7103
+ const selectedName = event.item || event;
7104
+ if (typeof selectedName === 'string') {
7105
+ this.editForm.patchValue({ name: selectedName });
7106
+ this.findTemplatesForColumnName(selectedName).then(templatesForName => {
7107
+ if (templatesForName.length > 0) {
7108
+ const currentType = this.editForm.get('type')?.value;
7109
+ const typesForName = templatesForName.map(template => template.columnType);
7110
+ if (!currentType || !typesForName.includes(currentType)) {
7111
+ const firstTemplate = templatesForName[0];
7112
+ this.editForm.patchValue({
7113
+ type: firstTemplate.columnType,
7114
+ templateId: firstTemplate.id,
7115
+ ontologyType: firstTemplate.ontologyType || '',
7116
+ enableTypeahead: firstTemplate.enableTypeahead || false
7117
+ });
7118
+ }
7119
+ else {
7120
+ const matchingTemplate = templatesForName.find(t => t.columnType === currentType);
7121
+ if (matchingTemplate) {
7122
+ this.editForm.patchValue({
7123
+ templateId: matchingTemplate.id,
7124
+ ontologyType: matchingTemplate.ontologyType || '',
7125
+ enableTypeahead: matchingTemplate.enableTypeahead || false
7126
+ });
7127
+ }
7128
+ else {
7129
+ this.editForm.patchValue({
7130
+ templateId: null,
7131
+ ontologyType: '',
7132
+ enableTypeahead: false
7133
+ });
7134
+ }
7135
+ }
7136
+ }
7137
+ else {
7138
+ this.editForm.patchValue({
7139
+ templateId: null,
7140
+ ontologyType: '',
7141
+ enableTypeahead: false
7142
+ });
7143
+ }
7144
+ this.checkForSpecialSyntax();
7145
+ });
7146
+ }
7147
+ }
7148
+ async findTemplatesForColumnName(columnName) {
7149
+ const cached = this.availableColumnTemplates();
7150
+ const cachedMatches = cached.filter(template => template.columnName === columnName);
7151
+ if (cachedMatches.length > 0) {
7152
+ return cachedMatches;
7153
+ }
7154
+ try {
7155
+ const response = await firstValueFrom(this.metadataColumnTemplateService.getMetadataColumnTemplates({
7156
+ search: columnName,
7157
+ limit: 10
7158
+ }));
7159
+ if (response) {
7160
+ const exactMatches = response.results.filter((template) => template.columnName === columnName);
7161
+ return exactMatches;
7162
+ }
7163
+ }
7164
+ catch (error) {
7165
+ console.warn('Failed to fetch templates for column name:', columnName, error);
7166
+ }
7167
+ return [];
7168
+ }
7169
+ onColumnTypeSelected(event) {
7170
+ const selectedType = event.item || event;
7171
+ if (typeof selectedType === 'string') {
7172
+ this.editForm.patchValue({ type: selectedType });
7173
+ const selectedName = this.editForm.get('name')?.value;
7174
+ if (selectedName) {
7175
+ const templates = this.availableColumnTemplates();
7176
+ const matchingTemplate = templates.find(template => template.columnName === selectedName && template.columnType === selectedType);
7177
+ if (matchingTemplate && matchingTemplate.id) {
7178
+ this.editForm.patchValue({
7179
+ templateId: matchingTemplate.id,
7180
+ ontologyType: matchingTemplate.ontologyType || '',
7181
+ enableTypeahead: matchingTemplate.enableTypeahead || false
7182
+ });
7183
+ }
7184
+ else {
7185
+ this.editForm.patchValue({
7186
+ templateId: null,
7187
+ ontologyType: '',
7188
+ enableTypeahead: false
7189
+ });
7190
+ }
7191
+ }
7192
+ this.checkForSpecialSyntax();
7193
+ }
7194
+ }
7195
+ openValueEditModal() {
7196
+ const formValue = this.editForm.value;
7197
+ const config = {
7198
+ columnName: formValue.name || 'Column',
7199
+ columnType: formValue.type || '',
7200
+ ontologyType: formValue.ontologyType || '',
7201
+ enableTypeahead: formValue.enableTypeahead || false,
7202
+ currentValue: formValue.value || '',
7203
+ context: 'favorite_management',
7204
+ templateId: formValue.templateId || undefined
7205
+ };
7206
+ const modalRef = this.modalService.open(MetadataValueEditModal, {
7207
+ size: 'lg',
7208
+ backdrop: 'static'
7209
+ });
7210
+ modalRef.componentInstance.config = config;
7211
+ modalRef.componentInstance.valueSaved.subscribe((result) => {
7212
+ const newValue = typeof result === 'string' ? result : result.value;
7213
+ this.editForm.get('value')?.setValue(newValue);
7214
+ this.editForm.get('value')?.markAsTouched();
7215
+ this.selectedOntologyValue = newValue;
7216
+ modalRef.componentInstance.onClose();
7217
+ });
7218
+ }
7219
+ searchLabGroups = (text$) => text$.pipe(debounceTime(200), distinctUntilChanged(), switchMap$1(term => {
7220
+ if (term.length === 0) {
7221
+ return of(this.availableLabGroups().slice(0, 10));
7222
+ }
7223
+ else if (term.length >= 1) {
7224
+ return this.labGroupService.getLabGroups({
7225
+ search: term,
7226
+ limit: 10
7227
+ }).pipe(map$1(response => response.results.map(group => ({
7228
+ id: group.id,
7229
+ name: group.name
7230
+ }))), catchError(() => {
7231
+ return of(this.getLabGroupSuggestions(term));
7232
+ }));
7233
+ }
7234
+ else {
7235
+ return of([]);
7236
+ }
7237
+ }));
7238
+ getLabGroupSuggestions(term) {
7239
+ const allGroups = this.allLabGroups();
7240
+ if (!term) {
7241
+ return allGroups.slice(0, 10);
7242
+ }
7243
+ return allGroups
7244
+ .filter(group => group.name.toLowerCase().includes(term.toLowerCase()))
7245
+ .slice(0, 10);
7246
+ }
7247
+ formatLabGroup = (group) => {
7248
+ return group.name;
7249
+ };
7250
+ inputLabGroupFormatter = (group) => {
7251
+ return group.name;
7252
+ };
7253
+ onLabGroupSelected = (event) => {
7254
+ const selectedGroup = event.item;
7255
+ if (selectedGroup && selectedGroup.id) {
7256
+ this.searchForm.patchValue({ labGroup: selectedGroup.id.toString() });
7257
+ }
7258
+ };
7259
+ getCleanColumnName(name) {
7260
+ if (!name)
7261
+ return '';
7262
+ const bracketMatch = name.match(/\[(.+?)\]/);
7263
+ if (bracketMatch) {
7264
+ return bracketMatch[1];
7265
+ }
7266
+ return name;
7267
+ }
7268
+ getCleanColumnType(type) {
7269
+ if (!type)
7270
+ return '';
7271
+ const cleanType = type.replace(/\[.*?\]/g, '').trim();
7272
+ return cleanType.charAt(0).toUpperCase() + cleanType.slice(1);
7273
+ }
7274
+ isComplexValue(value) {
7275
+ if (!value)
7276
+ return false;
7277
+ const sdrfPattern = /^[A-Z]{2,3}=[^;]+(?:;[A-Z]{2,3}=[^;]+)+$/;
7278
+ const keyValuePattern = /\w+[:=][^;,]+[;,]\w+[:=]/;
7279
+ return sdrfPattern.test(value) || keyValuePattern.test(value);
7280
+ }
7281
+ parseComplexValue(value) {
7282
+ if (!value)
7283
+ return [];
7284
+ const pairs = [];
7285
+ if (value.includes('=') && value.includes(';')) {
7286
+ const parts = value.split(';');
7287
+ parts.forEach(part => {
7288
+ const [key, val] = part.split('=', 2);
7289
+ if (key && val) {
7290
+ pairs.push({
7291
+ key: key.trim(),
7292
+ value: val.trim()
7293
+ });
7294
+ }
7295
+ });
7296
+ }
7297
+ else if (value.includes(':') && (value.includes(';') || value.includes(','))) {
7298
+ const separator = value.includes(';') ? ';' : ',';
7299
+ const parts = value.split(separator);
7300
+ parts.forEach(part => {
7301
+ const [key, val] = part.split(':', 2);
7302
+ if (key && val) {
7303
+ pairs.push({
7304
+ key: key.trim(),
7305
+ value: val.trim()
7306
+ });
7307
+ }
7308
+ });
7309
+ }
7310
+ return pairs;
7311
+ }
7312
+ checkForSpecialSyntax() {
7313
+ const columnName = this.editForm.get('name')?.value;
7314
+ const columnType = this.editForm.get('type')?.value;
7315
+ if (columnName && columnType) {
7316
+ const syntaxType = this.sdrfSyntaxService.detectSpecialSyntax(columnName, columnType);
7317
+ this.specialSyntaxType.set(syntaxType);
7318
+ if (syntaxType) {
7319
+ this.showSpecialInput.set(true);
7320
+ const currentValue = this.editForm.get('value')?.value;
7321
+ this.specialInputValue.set(currentValue || '');
7322
+ // Clear ontology value when switching to enhanced editor
7323
+ this.selectedOntologyValue = null;
7324
+ }
7325
+ else {
7326
+ this.showSpecialInput.set(false);
7327
+ this.specialInputValue.set('');
7328
+ }
7329
+ }
7330
+ else {
7331
+ this.specialSyntaxType.set(null);
7332
+ this.showSpecialInput.set(false);
7333
+ this.specialInputValue.set('');
7334
+ }
7335
+ }
7336
+ onSpecialInputChange(value) {
7337
+ this.specialInputValue.set(value);
7338
+ // When enhanced editor provides a value, clear the ontology value storage
7339
+ // because the enhanced editor is providing the final string value
7340
+ this.selectedOntologyValue = null;
7341
+ this.editForm.patchValue({ value });
7342
+ }
7343
+ toggleSpecialInput() {
7344
+ this.showSpecialInput.set(!this.showSpecialInput());
7345
+ if (!this.showSpecialInput()) {
7346
+ // Switching to normal input, sync the current value
7347
+ // Clear ontology value when switching back to regular input
7348
+ this.selectedOntologyValue = null;
7349
+ this.editForm.patchValue({ value: this.specialInputValue() });
7350
+ }
7351
+ else {
7352
+ // Switching to special input, clear ontology value
7353
+ this.selectedOntologyValue = null;
7354
+ }
7355
+ }
7356
+ getSpecialInputTitle() {
7357
+ const syntaxType = this.specialSyntaxType();
7358
+ switch (syntaxType) {
7359
+ case 'age': return 'Age Format Editor';
7360
+ case 'modification': return 'Modification Parameters Editor';
7361
+ case 'cleavage': return 'Cleavage Agent Editor';
7362
+ case 'spiked_compound': return 'Spiked Compound Editor';
7363
+ default: return 'Special Format Editor';
7364
+ }
7365
+ }
7366
+ getColumnTypeClass(favouriteOption) {
7367
+ const type = favouriteOption.type?.toLowerCase() || '';
7368
+ if (type.includes(ColumnType.CHARACTERISTICS))
7369
+ return 'text-primary';
7370
+ if (type.includes(ColumnType.FACTOR_VALUE.replace('_', ' ')))
7371
+ return 'text-success';
7372
+ if (type.includes(ColumnType.COMMENT))
7373
+ return 'text-info';
7374
+ if (type.includes(ColumnType.SOURCE_NAME.replace('_', ' ')))
7375
+ return 'text-warning';
7376
+ if (type === ColumnType.SPECIAL)
7377
+ return 'text-danger';
7378
+ return 'text-muted';
7379
+ }
7380
+ getColumnTypeIcon(favouriteMetadataOption) {
7381
+ const type = favouriteMetadataOption.type?.toLowerCase() || '';
7382
+ if (type.includes(ColumnType.CHARACTERISTICS))
7383
+ return 'bi-tags';
7384
+ if (type.includes(ColumnType.FACTOR_VALUE.replace('_', ' ')))
7385
+ return 'bi-sliders';
7386
+ if (type.includes(ColumnType.COMMENT))
7387
+ return 'bi-chat-left-text';
7388
+ if (type.includes(ColumnType.SOURCE_NAME.replace('_', ' ')))
7389
+ return 'bi-diagram-3';
7390
+ if (type === ColumnType.SPECIAL)
7391
+ return 'bi-star';
7392
+ return 'bi-circle';
7393
+ }
7394
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.3", ngImport: i0, type: FavoriteManagement, deps: [{ token: i1.FormBuilder }, { token: FavouriteMetadataOptionService }, { token: MetadataColumnTemplateService }, { token: i3.LabGroupService }, { token: i3.ToastService }, { token: i2.NgbModal }, { token: i3.AuthService }], target: i0.ɵɵFactoryTarget.Component });
7395
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.3", type: FavoriteManagement, isStandalone: true, selector: "ccv-favorite-management", ngImport: i0, template: "<div class=\"favorite-management-container\">\n <!-- Header Section -->\n <nav class=\"navbar navbar-expand-lg bg-body-tertiary shadow-sm\">\n <div class=\"container-fluid\">\n <div class=\"d-flex align-items-center justify-content-between w-100\">\n <div class=\"d-flex align-items-center\">\n <h4 class=\"mb-0\">\n <i class=\"bi bi-heart-fill me-2 text-danger\"></i>Favorite Management\n </h4>\n <span class=\"badge bg-primary ms-3\">{{ filteredFavorites().length }} favorites</span>\n </div>\n\n <div class=\"d-flex gap-2\">\n <button\n type=\"button\"\n class=\"btn btn-primary btn-sm\"\n (click)=\"openCreateModal()\"\n [disabled]=\"isLoading()\">\n <i class=\"bi bi-plus-circle me-1\"></i>Add Favorite\n </button>\n </div>\n </div>\n </div>\n </nav>\n\n <div class=\"tables-content p-4\">\n <!-- Search and Filters Section -->\n <div class=\"card mb-4\">\n <div class=\"card-header\">\n <h5 class=\"mb-0\">\n <i class=\"bi bi-search me-2\"></i>Search & Filters\n </h5>\n </div>\n <div class=\"card-body\">\n <form [formGroup]=\"searchForm\" class=\"row g-3\">\n <div class=\"col-md-3\">\n <label for=\"search\" class=\"form-label\">Search</label>\n <div class=\"input-group\">\n <span class=\"input-group-text\">\n <i class=\"bi bi-search\"></i>\n </span>\n <input\n type=\"text\"\n class=\"form-control\"\n id=\"search\"\n formControlName=\"search\"\n placeholder=\"Search by name or value\">\n </div>\n </div>\n\n <div class=\"col-md-3\">\n <label for=\"scope\" class=\"form-label\">Scope</label>\n <select class=\"form-select\" id=\"scope\" formControlName=\"scope\">\n <option value=\"\">All Scopes</option>\n <option value=\"personal\">Personal</option>\n <option value=\"lab_group\">Lab Group</option>\n <option value=\"global\">Global</option>\n </select>\n </div>\n\n <div class=\"col-md-3\">\n <label for=\"labGroup\" class=\"form-label\">Lab Group</label>\n <input\n type=\"text\"\n class=\"form-control\"\n id=\"labGroup\"\n formControlName=\"labGroup\"\n placeholder=\"All Lab Groups\"\n [ngbTypeahead]=\"searchLabGroups\"\n [resultFormatter]=\"formatLabGroup\"\n [inputFormatter]=\"inputLabGroupFormatter\"\n (selectItem)=\"onLabGroupSelected($event)\"\n [editable]=\"true\"\n [showHint]=\"true\"\n [focusFirst]=\"false\"\n container=\"body\">\n </div>\n\n <div class=\"col-md-3\">\n <label for=\"columnType\" class=\"form-label\">Column Type</label>\n <select class=\"form-select\" id=\"columnType\" formControlName=\"columnType\">\n @for (type of columnTypes; track type.value) {\n <option [value]=\"type.value\">{{ type.label }}</option>\n }\n </select>\n </div>\n </form>\n </div>\n </div>\n\n <div class=\"card\">\n <div class=\"card-header d-flex justify-content-between align-items-center\">\n <h5 class=\"mb-0\">\n <i class=\"bi bi-table me-2\"></i>Favorites options\n <span class=\"badge bg-primary ms-2\"></span>\n </h5>\n @if (isLoading()) {\n <div class=\"spinner-border spinner-border-sm text-primary\" role=\"status\">\n <span class=\"visually-hidden\">Loading...</span>\n </div>\n }\n </div>\n <div class=\"card-body p-0\">\n @if (!isLoading()) {\n\n @if (filteredFavorites().length === 0) {\n <div class=\"text-center py-5\">\n <i class=\"bi bi-heart display-1 text-muted\"></i>\n <h5 class=\"mt-3 text-muted\">No Favorites Found</h5>\n @if (searchTerm() || scopeFilter() || columnTypeFilter()) {\n <p class=\"text-muted\">Try adjusting your search criteria</p>\n <button type=\"button\" class=\"btn btn-outline-secondary\" (click)=\"clearFilters()\">\n <i class=\"bi bi-arrow-clockwise me-1\"></i>Clear Filters\n </button>\n } @else {\n <p class=\"text-muted\">Create your first favorite to get started</p>\n <button type=\"button\" class=\"btn btn-primary\" (click)=\"openCreateModal()\">\n <i class=\"bi bi-plus-circle me-1\"></i>Add First Favorite\n </button>\n }\n </div>\n }\n\n <!-- Favorites Table -->\n @if (filteredFavorites().length > 0) {\n <div class=\"card\">\n <div class=\"card-body p-0\">\n <div class=\"table-responsive\">\n <table class=\"table table-hover mb-0\">\n <thead>\n <tr>\n <th scope=\"col\" style=\"width: 200px;\">Column</th>\n <th scope=\"col\" style=\"width: 200px;\">Display Name</th>\n <th scope=\"col\">Value</th>\n <th scope=\"col\" style=\"width: 150px;\">Scope</th>\n <th scope=\"col\" style=\"width: 120px;\">Created</th>\n <th scope=\"col\" style=\"width: 120px;\">Actions</th>\n </tr>\n </thead>\n <tbody>\n @for (favorite of paginatedFavorites(); track favorite.id) {\n <tr>\n <td>\n <div class=\"d-flex align-items-center gap-2\">\n <div class=\"d-flex align-items-center\">\n <i class=\"me-2\" [class]=\"getColumnTypeIcon(favorite) + ' ' + getColumnTypeClass(favorite)\"></i>\n <span class=\"fw-semibold\">{{ getCleanColumnName(favorite.name) }}</span>\n </div>\n </div>\n </td>\n <td>\n <div class=\"text-truncate\" [title]=\"favorite.displayValue || 'No display name'\">\n @if (favorite.displayValue && favorite.displayValue.trim()) {\n {{ favorite.displayValue }}\n } @else {\n <span class=\"text-muted fst-italic\">No display name</span>\n }\n </div>\n </td>\n <td>\n <div style=\"max-width: 300px;\">\n @if (isComplexValue(favorite.value)) {\n <!-- Complex value with key-value pairs -->\n <div class=\"d-flex flex-wrap gap-1\">\n @for (pair of parseComplexValue(favorite.value); track $index) {\n <span class=\"badge bg-light text-dark border\">\n <small><strong>{{ pair.key }}:</strong> {{ pair.value }}</small>\n </span>\n }\n </div>\n } @else {\n <!-- Simple value -->\n <div class=\"text-truncate\" [title]=\"favorite.value\">\n {{ favorite.value }}\n </div>\n }\n </div>\n </td>\n <td>\n <span [class]=\"getScopeClass(favorite)\">\n @if (favorite.isGlobal) {\n <i class=\"bi bi-globe me-1\"></i>Global\n } @else if (favorite.labGroup) {\n <i class=\"bi bi-people me-1\"></i>Lab Group\n } @else {\n <i class=\"bi bi-person me-1\"></i>Personal\n }\n </span>\n @if (favorite.userUsername && !favorite.isGlobal) {\n <br><small class=\"text-muted\">{{ favorite.userUsername }}</small>\n }\n @if (favorite.labGroupName) {\n <br><small class=\"text-muted\">{{ favorite.labGroupName }}</small>\n }\n </td>\n <td>\n <small class=\"text-muted\">\n {{ favorite.createdAt | date:'MMM d, y' }}\n </small>\n </td>\n <td>\n <div class=\"btn-group btn-group-sm\">\n @if (canEditFavorite(favorite)) {\n <button\n type=\"button\"\n class=\"btn btn-outline-primary\"\n (click)=\"openEditModal(favorite)\"\n title=\"Edit favorite\">\n <i class=\"bi bi-pencil\"></i>\n </button>\n }\n @if (canDeleteFavorite(favorite)) {\n <button\n type=\"button\"\n class=\"btn btn-outline-danger\"\n (click)=\"deleteFavorite(favorite)\"\n title=\"Delete favorite\">\n <i class=\"bi bi-trash\"></i>\n </button>\n }\n </div>\n </td>\n </tr>\n }\n </tbody>\n </table>\n </div>\n </div>\n </div>\n\n <!-- Pagination -->\n @if (totalPages() > 1) {\n <div class=\"d-flex justify-content-between align-items-center mt-4\">\n <div class=\"d-flex align-items-center gap-2\">\n <span class=\"text-muted\">Show:</span>\n <select\n class=\"form-select form-select-sm\"\n style=\"width: auto;\"\n [value]=\"pageSize()\"\n (change)=\"onPageSizeChange(+($any($event.target).value))\">\n <option value=\"10\">10</option>\n <option value=\"25\">25</option>\n <option value=\"50\">50</option>\n <option value=\"100\">100</option>\n </select>\n <span class=\"text-muted\">per page</span>\n </div>\n\n <ngb-pagination\n [collectionSize]=\"totalFavoritesCount()\"\n [page]=\"currentPage()\"\n [pageSize]=\"pageSize()\"\n [maxSize]=\"5\"\n [boundaryLinks]=\"true\"\n [rotate]=\"true\"\n (pageChange)=\"onPageChange($event)\"\n class=\"pagination-sm mb-0\">\n </ngb-pagination>\n\n <span class=\"text-muted\">\n Showing {{ ((currentPage() - 1) * pageSize()) + 1 }}-{{ Math.min(currentPage() * pageSize(), totalFavoritesCount()) }}\n of {{ totalFavoritesCount() }} favorites\n </span>\n </div>\n }\n }\n }\n </div>\n </div>\n </div>\n</div>\n\n<!-- Edit/Create Modal -->\n@if (showEditModal()) {\n <div class=\"modal fade show d-block\" tabindex=\"-1\" style=\"background: rgba(0,0,0,0.5);\">\n <div class=\"modal-dialog modal-lg\">\n <div class=\"modal-content\">\n <div class=\"modal-header\">\n <h5 class=\"modal-title\">\n <i class=\"bi bi-heart me-2\"></i>{{ editModalTitle }}\n </h5>\n <button type=\"button\" class=\"btn-close\" (click)=\"closeEditModal()\"></button>\n </div>\n\n <div class=\"modal-body\">\n <form [formGroup]=\"editForm\" (ngSubmit)=\"saveFavorite()\">\n <div class=\"row\">\n <div class=\"col-md-6 mb-3\">\n <label for=\"favName\" class=\"form-label\">\n Column Name *\n @if (columnTemplatesLoaded()) {\n <small class=\"text-success ms-1\">\n <i class=\"bi bi-search\"></i> Autocomplete Available\n </small>\n }\n </label>\n <input\n type=\"text\"\n class=\"form-control\"\n id=\"favName\"\n formControlName=\"name\"\n [class.is-invalid]=\"editForm.get('name')?.invalid && editForm.get('name')?.touched\"\n placeholder=\"e.g., organism\"\n [ngbTypeahead]=\"searchColumnNames\"\n (selectItem)=\"onColumnNameSelected($event)\"\n [editable]=\"true\"\n [showHint]=\"true\"\n [focusFirst]=\"false\"\n container=\"body\">\n <div class=\"form-text\">\n @if (columnTemplatesLoaded()) {\n Type to search available column names from templates\n } @else {\n Loading column suggestions...\n }\n </div>\n <div class=\"invalid-feedback\">\n @if (editForm.get('name')?.errors?.['required']) {\n Column name is required\n }\n @if (editForm.get('name')?.errors?.['maxlength']) {\n Column name must be less than 255 characters\n }\n </div>\n </div>\n\n <div class=\"col-md-6 mb-3\">\n <label for=\"favType\" class=\"form-label\">\n Column Type *\n @if (columnTemplatesLoaded()) {\n <small class=\"text-success ms-1\">\n <i class=\"bi bi-search\"></i> Autocomplete Available\n </small>\n }\n </label>\n <input\n type=\"text\"\n class=\"form-control\"\n id=\"favType\"\n formControlName=\"type\"\n [class.is-invalid]=\"editForm.get('type')?.invalid && editForm.get('type')?.touched\"\n placeholder=\"e.g., characteristics[organism]\"\n [ngbTypeahead]=\"searchColumnTypes\"\n (selectItem)=\"onColumnTypeSelected($event)\"\n [editable]=\"true\"\n [showHint]=\"true\"\n [focusFirst]=\"false\"\n container=\"body\">\n <div class=\"form-text\">\n @if (columnTemplatesLoaded()) {\n Type to search available column types (filtered by selected name)\n } @else {\n Loading column suggestions...\n }\n </div>\n <div class=\"invalid-feedback\">\n @if (editForm.get('type')?.errors?.['required']) {\n Column type is required\n }\n @if (editForm.get('type')?.errors?.['maxlength']) {\n Column type must be less than 255 characters\n }\n </div>\n </div>\n </div>\n\n <div class=\"mb-3\">\n <label for=\"favValue\" class=\"form-label\">\n Value\n @if (editForm.get('templateId')?.value) {\n <span class=\"badge bg-success ms-2\">\n <i class=\"bi bi-search me-1\"></i>Advanced Editor Available\n </span>\n }\n @if (specialSyntaxType()) {\n <span class=\"badge bg-info ms-2\">\n <i class=\"bi bi-gear me-1\"></i>{{ getSpecialInputTitle() }}\n </span>\n }\n </label>\n\n <div class=\"input-group\">\n <input\n type=\"text\"\n class=\"form-control\"\n id=\"favValue\"\n formControlName=\"value\"\n [class.is-invalid]=\"editForm.get('value')?.invalid && editForm.get('value')?.touched\"\n placeholder=\"Enter favorite value\"\n readonly>\n <button\n type=\"button\"\n class=\"btn btn-outline-primary\"\n (click)=\"openValueEditModal()\"\n title=\"Open advanced value editor\">\n <i class=\"bi bi-pencil me-1\"></i>Edit\n </button>\n </div>\n\n <div class=\"form-text\">\n @if (editForm.get('templateId')?.value) {\n <i class=\"bi bi-info-circle me-1\"></i>\n Click \"Edit\" to use advanced value editor with ontology autocomplete suggestions\n } @else if (specialSyntaxType()) {\n <i class=\"bi bi-info-circle me-1\"></i>\n Click \"Edit\" to use enhanced {{ specialSyntaxType() }} editor for structured input\n } @else {\n <i class=\"bi bi-info-circle me-1\"></i>\n Click \"Edit\" to set the value (select column name and type for smart suggestions)\n }\n </div>\n <div class=\"invalid-feedback\">\n @if (editForm.get('value')?.errors?.['maxlength']) {\n Value must be less than 500 characters\n }\n </div>\n </div>\n\n <div class=\"mb-3\">\n <label for=\"favDisplayValue\" class=\"form-label\">Display Value</label>\n <input\n type=\"text\"\n class=\"form-control\"\n id=\"favDisplayValue\"\n formControlName=\"displayValue\"\n placeholder=\"Optional: Human-readable version\">\n <div class=\"form-text\">If empty, will use the same value as above</div>\n </div>\n\n <div class=\"mb-3\">\n <label for=\"favScope\" class=\"form-label\">Scope *</label>\n <select\n class=\"form-select\"\n id=\"favScope\"\n formControlName=\"scope\">\n <option value=\"personal\">Personal (Only visible to me)</option>\n @if (userLabGroups().length > 0) {\n <option value=\"lab_group\">Lab Group (Visible to my lab group)</option>\n }\n @if (isAdmin()) {\n <option value=\"global\">Global (Visible to everyone)</option>\n }\n </select>\n </div>\n\n <!-- Lab Group Selection (shown when lab_group scope is selected) -->\n @if (editForm.get('scope')?.value === 'lab_group' && userLabGroups().length > 1) {\n <div class=\"mb-3\">\n <label for=\"favLabGroup\" class=\"form-label\">Lab Group *</label>\n <select\n class=\"form-select\"\n id=\"favLabGroup\"\n formControlName=\"labGroupId\">\n <option value=\"\">Select a lab group</option>\n @for (labGroup of userLabGroups(); track labGroup.id) {\n <option [value]=\"labGroup.id\">{{ labGroup.name }}</option>\n }\n </select>\n <div class=\"form-text\">Choose which lab group this favorite will be visible to</div>\n </div>\n }\n </form>\n </div>\n\n <div class=\"modal-footer\">\n <button type=\"button\" class=\"btn btn-outline-secondary\" (click)=\"closeEditModal()\">\n <i class=\"bi bi-x-circle me-1\"></i>Cancel\n </button>\n <button\n type=\"button\"\n class=\"btn btn-primary\"\n (click)=\"saveFavorite()\"\n [disabled]=\"editForm.invalid\">\n <i class=\"bi bi-check-circle me-1\"></i>\n {{ isEditMode() ? 'Update' : 'Create' }} Favorite\n </button>\n </div>\n </div>\n </div>\n </div>\n}\n", styles: [""], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.SelectControlValueAccessor, selector: "select:not([multiple])[formControlName],select:not([multiple])[formControl],select:not([multiple])[ngModel]", inputs: ["compareWith"] }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i1.FormControlName, selector: "[formControlName]", inputs: ["formControlName", "disabled", "ngModel"], outputs: ["ngModelChange"] }, { kind: "ngmodule", type: RouterModule }, { kind: "ngmodule", type: NgbModule }, { kind: "component", type: i2.NgbPagination, selector: "ngb-pagination", inputs: ["disabled", "boundaryLinks", "directionLinks", "ellipses", "rotate", "collectionSize", "maxSize", "page", "pageSize", "size"], outputs: ["pageChange"] }, { kind: "directive", type: i2.NgbTypeahead, selector: "input[ngbTypeahead]", inputs: ["autocomplete", "container", "editable", "focusFirst", "inputFormatter", "ngbTypeahead", "resultFormatter", "resultTemplate", "selectOnExact", "showHint", "placement", "popperOptions", "popupClass"], outputs: ["selectItem"], exportAs: ["ngbTypeahead"] }, { kind: "pipe", type: i5.DatePipe, name: "date" }] });
7396
+ }
7397
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.3", ngImport: i0, type: FavoriteManagement, decorators: [{
7398
+ type: Component,
7399
+ args: [{ selector: 'ccv-favorite-management', standalone: true, imports: [
7400
+ CommonModule,
7401
+ ReactiveFormsModule,
7402
+ RouterModule,
7403
+ NgbModule,
7404
+ NgbTypeahead
7405
+ ], template: "<div class=\"favorite-management-container\">\n <!-- Header Section -->\n <nav class=\"navbar navbar-expand-lg bg-body-tertiary shadow-sm\">\n <div class=\"container-fluid\">\n <div class=\"d-flex align-items-center justify-content-between w-100\">\n <div class=\"d-flex align-items-center\">\n <h4 class=\"mb-0\">\n <i class=\"bi bi-heart-fill me-2 text-danger\"></i>Favorite Management\n </h4>\n <span class=\"badge bg-primary ms-3\">{{ filteredFavorites().length }} favorites</span>\n </div>\n\n <div class=\"d-flex gap-2\">\n <button\n type=\"button\"\n class=\"btn btn-primary btn-sm\"\n (click)=\"openCreateModal()\"\n [disabled]=\"isLoading()\">\n <i class=\"bi bi-plus-circle me-1\"></i>Add Favorite\n </button>\n </div>\n </div>\n </div>\n </nav>\n\n <div class=\"tables-content p-4\">\n <!-- Search and Filters Section -->\n <div class=\"card mb-4\">\n <div class=\"card-header\">\n <h5 class=\"mb-0\">\n <i class=\"bi bi-search me-2\"></i>Search & Filters\n </h5>\n </div>\n <div class=\"card-body\">\n <form [formGroup]=\"searchForm\" class=\"row g-3\">\n <div class=\"col-md-3\">\n <label for=\"search\" class=\"form-label\">Search</label>\n <div class=\"input-group\">\n <span class=\"input-group-text\">\n <i class=\"bi bi-search\"></i>\n </span>\n <input\n type=\"text\"\n class=\"form-control\"\n id=\"search\"\n formControlName=\"search\"\n placeholder=\"Search by name or value\">\n </div>\n </div>\n\n <div class=\"col-md-3\">\n <label for=\"scope\" class=\"form-label\">Scope</label>\n <select class=\"form-select\" id=\"scope\" formControlName=\"scope\">\n <option value=\"\">All Scopes</option>\n <option value=\"personal\">Personal</option>\n <option value=\"lab_group\">Lab Group</option>\n <option value=\"global\">Global</option>\n </select>\n </div>\n\n <div class=\"col-md-3\">\n <label for=\"labGroup\" class=\"form-label\">Lab Group</label>\n <input\n type=\"text\"\n class=\"form-control\"\n id=\"labGroup\"\n formControlName=\"labGroup\"\n placeholder=\"All Lab Groups\"\n [ngbTypeahead]=\"searchLabGroups\"\n [resultFormatter]=\"formatLabGroup\"\n [inputFormatter]=\"inputLabGroupFormatter\"\n (selectItem)=\"onLabGroupSelected($event)\"\n [editable]=\"true\"\n [showHint]=\"true\"\n [focusFirst]=\"false\"\n container=\"body\">\n </div>\n\n <div class=\"col-md-3\">\n <label for=\"columnType\" class=\"form-label\">Column Type</label>\n <select class=\"form-select\" id=\"columnType\" formControlName=\"columnType\">\n @for (type of columnTypes; track type.value) {\n <option [value]=\"type.value\">{{ type.label }}</option>\n }\n </select>\n </div>\n </form>\n </div>\n </div>\n\n <div class=\"card\">\n <div class=\"card-header d-flex justify-content-between align-items-center\">\n <h5 class=\"mb-0\">\n <i class=\"bi bi-table me-2\"></i>Favorites options\n <span class=\"badge bg-primary ms-2\"></span>\n </h5>\n @if (isLoading()) {\n <div class=\"spinner-border spinner-border-sm text-primary\" role=\"status\">\n <span class=\"visually-hidden\">Loading...</span>\n </div>\n }\n </div>\n <div class=\"card-body p-0\">\n @if (!isLoading()) {\n\n @if (filteredFavorites().length === 0) {\n <div class=\"text-center py-5\">\n <i class=\"bi bi-heart display-1 text-muted\"></i>\n <h5 class=\"mt-3 text-muted\">No Favorites Found</h5>\n @if (searchTerm() || scopeFilter() || columnTypeFilter()) {\n <p class=\"text-muted\">Try adjusting your search criteria</p>\n <button type=\"button\" class=\"btn btn-outline-secondary\" (click)=\"clearFilters()\">\n <i class=\"bi bi-arrow-clockwise me-1\"></i>Clear Filters\n </button>\n } @else {\n <p class=\"text-muted\">Create your first favorite to get started</p>\n <button type=\"button\" class=\"btn btn-primary\" (click)=\"openCreateModal()\">\n <i class=\"bi bi-plus-circle me-1\"></i>Add First Favorite\n </button>\n }\n </div>\n }\n\n <!-- Favorites Table -->\n @if (filteredFavorites().length > 0) {\n <div class=\"card\">\n <div class=\"card-body p-0\">\n <div class=\"table-responsive\">\n <table class=\"table table-hover mb-0\">\n <thead>\n <tr>\n <th scope=\"col\" style=\"width: 200px;\">Column</th>\n <th scope=\"col\" style=\"width: 200px;\">Display Name</th>\n <th scope=\"col\">Value</th>\n <th scope=\"col\" style=\"width: 150px;\">Scope</th>\n <th scope=\"col\" style=\"width: 120px;\">Created</th>\n <th scope=\"col\" style=\"width: 120px;\">Actions</th>\n </tr>\n </thead>\n <tbody>\n @for (favorite of paginatedFavorites(); track favorite.id) {\n <tr>\n <td>\n <div class=\"d-flex align-items-center gap-2\">\n <div class=\"d-flex align-items-center\">\n <i class=\"me-2\" [class]=\"getColumnTypeIcon(favorite) + ' ' + getColumnTypeClass(favorite)\"></i>\n <span class=\"fw-semibold\">{{ getCleanColumnName(favorite.name) }}</span>\n </div>\n </div>\n </td>\n <td>\n <div class=\"text-truncate\" [title]=\"favorite.displayValue || 'No display name'\">\n @if (favorite.displayValue && favorite.displayValue.trim()) {\n {{ favorite.displayValue }}\n } @else {\n <span class=\"text-muted fst-italic\">No display name</span>\n }\n </div>\n </td>\n <td>\n <div style=\"max-width: 300px;\">\n @if (isComplexValue(favorite.value)) {\n <!-- Complex value with key-value pairs -->\n <div class=\"d-flex flex-wrap gap-1\">\n @for (pair of parseComplexValue(favorite.value); track $index) {\n <span class=\"badge bg-light text-dark border\">\n <small><strong>{{ pair.key }}:</strong> {{ pair.value }}</small>\n </span>\n }\n </div>\n } @else {\n <!-- Simple value -->\n <div class=\"text-truncate\" [title]=\"favorite.value\">\n {{ favorite.value }}\n </div>\n }\n </div>\n </td>\n <td>\n <span [class]=\"getScopeClass(favorite)\">\n @if (favorite.isGlobal) {\n <i class=\"bi bi-globe me-1\"></i>Global\n } @else if (favorite.labGroup) {\n <i class=\"bi bi-people me-1\"></i>Lab Group\n } @else {\n <i class=\"bi bi-person me-1\"></i>Personal\n }\n </span>\n @if (favorite.userUsername && !favorite.isGlobal) {\n <br><small class=\"text-muted\">{{ favorite.userUsername }}</small>\n }\n @if (favorite.labGroupName) {\n <br><small class=\"text-muted\">{{ favorite.labGroupName }}</small>\n }\n </td>\n <td>\n <small class=\"text-muted\">\n {{ favorite.createdAt | date:'MMM d, y' }}\n </small>\n </td>\n <td>\n <div class=\"btn-group btn-group-sm\">\n @if (canEditFavorite(favorite)) {\n <button\n type=\"button\"\n class=\"btn btn-outline-primary\"\n (click)=\"openEditModal(favorite)\"\n title=\"Edit favorite\">\n <i class=\"bi bi-pencil\"></i>\n </button>\n }\n @if (canDeleteFavorite(favorite)) {\n <button\n type=\"button\"\n class=\"btn btn-outline-danger\"\n (click)=\"deleteFavorite(favorite)\"\n title=\"Delete favorite\">\n <i class=\"bi bi-trash\"></i>\n </button>\n }\n </div>\n </td>\n </tr>\n }\n </tbody>\n </table>\n </div>\n </div>\n </div>\n\n <!-- Pagination -->\n @if (totalPages() > 1) {\n <div class=\"d-flex justify-content-between align-items-center mt-4\">\n <div class=\"d-flex align-items-center gap-2\">\n <span class=\"text-muted\">Show:</span>\n <select\n class=\"form-select form-select-sm\"\n style=\"width: auto;\"\n [value]=\"pageSize()\"\n (change)=\"onPageSizeChange(+($any($event.target).value))\">\n <option value=\"10\">10</option>\n <option value=\"25\">25</option>\n <option value=\"50\">50</option>\n <option value=\"100\">100</option>\n </select>\n <span class=\"text-muted\">per page</span>\n </div>\n\n <ngb-pagination\n [collectionSize]=\"totalFavoritesCount()\"\n [page]=\"currentPage()\"\n [pageSize]=\"pageSize()\"\n [maxSize]=\"5\"\n [boundaryLinks]=\"true\"\n [rotate]=\"true\"\n (pageChange)=\"onPageChange($event)\"\n class=\"pagination-sm mb-0\">\n </ngb-pagination>\n\n <span class=\"text-muted\">\n Showing {{ ((currentPage() - 1) * pageSize()) + 1 }}-{{ Math.min(currentPage() * pageSize(), totalFavoritesCount()) }}\n of {{ totalFavoritesCount() }} favorites\n </span>\n </div>\n }\n }\n }\n </div>\n </div>\n </div>\n</div>\n\n<!-- Edit/Create Modal -->\n@if (showEditModal()) {\n <div class=\"modal fade show d-block\" tabindex=\"-1\" style=\"background: rgba(0,0,0,0.5);\">\n <div class=\"modal-dialog modal-lg\">\n <div class=\"modal-content\">\n <div class=\"modal-header\">\n <h5 class=\"modal-title\">\n <i class=\"bi bi-heart me-2\"></i>{{ editModalTitle }}\n </h5>\n <button type=\"button\" class=\"btn-close\" (click)=\"closeEditModal()\"></button>\n </div>\n\n <div class=\"modal-body\">\n <form [formGroup]=\"editForm\" (ngSubmit)=\"saveFavorite()\">\n <div class=\"row\">\n <div class=\"col-md-6 mb-3\">\n <label for=\"favName\" class=\"form-label\">\n Column Name *\n @if (columnTemplatesLoaded()) {\n <small class=\"text-success ms-1\">\n <i class=\"bi bi-search\"></i> Autocomplete Available\n </small>\n }\n </label>\n <input\n type=\"text\"\n class=\"form-control\"\n id=\"favName\"\n formControlName=\"name\"\n [class.is-invalid]=\"editForm.get('name')?.invalid && editForm.get('name')?.touched\"\n placeholder=\"e.g., organism\"\n [ngbTypeahead]=\"searchColumnNames\"\n (selectItem)=\"onColumnNameSelected($event)\"\n [editable]=\"true\"\n [showHint]=\"true\"\n [focusFirst]=\"false\"\n container=\"body\">\n <div class=\"form-text\">\n @if (columnTemplatesLoaded()) {\n Type to search available column names from templates\n } @else {\n Loading column suggestions...\n }\n </div>\n <div class=\"invalid-feedback\">\n @if (editForm.get('name')?.errors?.['required']) {\n Column name is required\n }\n @if (editForm.get('name')?.errors?.['maxlength']) {\n Column name must be less than 255 characters\n }\n </div>\n </div>\n\n <div class=\"col-md-6 mb-3\">\n <label for=\"favType\" class=\"form-label\">\n Column Type *\n @if (columnTemplatesLoaded()) {\n <small class=\"text-success ms-1\">\n <i class=\"bi bi-search\"></i> Autocomplete Available\n </small>\n }\n </label>\n <input\n type=\"text\"\n class=\"form-control\"\n id=\"favType\"\n formControlName=\"type\"\n [class.is-invalid]=\"editForm.get('type')?.invalid && editForm.get('type')?.touched\"\n placeholder=\"e.g., characteristics[organism]\"\n [ngbTypeahead]=\"searchColumnTypes\"\n (selectItem)=\"onColumnTypeSelected($event)\"\n [editable]=\"true\"\n [showHint]=\"true\"\n [focusFirst]=\"false\"\n container=\"body\">\n <div class=\"form-text\">\n @if (columnTemplatesLoaded()) {\n Type to search available column types (filtered by selected name)\n } @else {\n Loading column suggestions...\n }\n </div>\n <div class=\"invalid-feedback\">\n @if (editForm.get('type')?.errors?.['required']) {\n Column type is required\n }\n @if (editForm.get('type')?.errors?.['maxlength']) {\n Column type must be less than 255 characters\n }\n </div>\n </div>\n </div>\n\n <div class=\"mb-3\">\n <label for=\"favValue\" class=\"form-label\">\n Value\n @if (editForm.get('templateId')?.value) {\n <span class=\"badge bg-success ms-2\">\n <i class=\"bi bi-search me-1\"></i>Advanced Editor Available\n </span>\n }\n @if (specialSyntaxType()) {\n <span class=\"badge bg-info ms-2\">\n <i class=\"bi bi-gear me-1\"></i>{{ getSpecialInputTitle() }}\n </span>\n }\n </label>\n\n <div class=\"input-group\">\n <input\n type=\"text\"\n class=\"form-control\"\n id=\"favValue\"\n formControlName=\"value\"\n [class.is-invalid]=\"editForm.get('value')?.invalid && editForm.get('value')?.touched\"\n placeholder=\"Enter favorite value\"\n readonly>\n <button\n type=\"button\"\n class=\"btn btn-outline-primary\"\n (click)=\"openValueEditModal()\"\n title=\"Open advanced value editor\">\n <i class=\"bi bi-pencil me-1\"></i>Edit\n </button>\n </div>\n\n <div class=\"form-text\">\n @if (editForm.get('templateId')?.value) {\n <i class=\"bi bi-info-circle me-1\"></i>\n Click \"Edit\" to use advanced value editor with ontology autocomplete suggestions\n } @else if (specialSyntaxType()) {\n <i class=\"bi bi-info-circle me-1\"></i>\n Click \"Edit\" to use enhanced {{ specialSyntaxType() }} editor for structured input\n } @else {\n <i class=\"bi bi-info-circle me-1\"></i>\n Click \"Edit\" to set the value (select column name and type for smart suggestions)\n }\n </div>\n <div class=\"invalid-feedback\">\n @if (editForm.get('value')?.errors?.['maxlength']) {\n Value must be less than 500 characters\n }\n </div>\n </div>\n\n <div class=\"mb-3\">\n <label for=\"favDisplayValue\" class=\"form-label\">Display Value</label>\n <input\n type=\"text\"\n class=\"form-control\"\n id=\"favDisplayValue\"\n formControlName=\"displayValue\"\n placeholder=\"Optional: Human-readable version\">\n <div class=\"form-text\">If empty, will use the same value as above</div>\n </div>\n\n <div class=\"mb-3\">\n <label for=\"favScope\" class=\"form-label\">Scope *</label>\n <select\n class=\"form-select\"\n id=\"favScope\"\n formControlName=\"scope\">\n <option value=\"personal\">Personal (Only visible to me)</option>\n @if (userLabGroups().length > 0) {\n <option value=\"lab_group\">Lab Group (Visible to my lab group)</option>\n }\n @if (isAdmin()) {\n <option value=\"global\">Global (Visible to everyone)</option>\n }\n </select>\n </div>\n\n <!-- Lab Group Selection (shown when lab_group scope is selected) -->\n @if (editForm.get('scope')?.value === 'lab_group' && userLabGroups().length > 1) {\n <div class=\"mb-3\">\n <label for=\"favLabGroup\" class=\"form-label\">Lab Group *</label>\n <select\n class=\"form-select\"\n id=\"favLabGroup\"\n formControlName=\"labGroupId\">\n <option value=\"\">Select a lab group</option>\n @for (labGroup of userLabGroups(); track labGroup.id) {\n <option [value]=\"labGroup.id\">{{ labGroup.name }}</option>\n }\n </select>\n <div class=\"form-text\">Choose which lab group this favorite will be visible to</div>\n </div>\n }\n </form>\n </div>\n\n <div class=\"modal-footer\">\n <button type=\"button\" class=\"btn btn-outline-secondary\" (click)=\"closeEditModal()\">\n <i class=\"bi bi-x-circle me-1\"></i>Cancel\n </button>\n <button\n type=\"button\"\n class=\"btn btn-primary\"\n (click)=\"saveFavorite()\"\n [disabled]=\"editForm.invalid\">\n <i class=\"bi bi-check-circle me-1\"></i>\n {{ isEditMode() ? 'Update' : 'Create' }} Favorite\n </button>\n </div>\n </div>\n </div>\n </div>\n}\n" }]
7406
+ }], ctorParameters: () => [{ type: i1.FormBuilder }, { type: FavouriteMetadataOptionService }, { type: MetadataColumnTemplateService }, { type: i3.LabGroupService }, { type: i3.ToastService }, { type: i2.NgbModal }, { type: i3.AuthService }] });
7407
+
6047
7408
  class CupcakeVanilla {
6048
7409
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.3", ngImport: i0, type: CupcakeVanilla, deps: [], target: i0.ɵɵFactoryTarget.Component });
6049
7410
  static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.3", type: CupcakeVanilla, isStandalone: true, selector: "ccv-cupcake-vanilla", ngImport: i0, template: `
@@ -6069,5 +7430,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.3", ngImpor
6069
7430
  * Generated bundle index. Do not edit.
6070
7431
  */
6071
7432
 
6072
- export { AdminWebSocketService, AsyncExportService, AsyncImportService, AsyncTaskService, AsyncTaskUIService, AsyncValidationService, COLUMN_TYPE_CONFIGS, ChunkedImportService, ChunkedUploadService, ColumnEditModal, ColumnType, CupcakeVanilla, ElectronService, ExcelExportModalComponent, FavouriteMetadataOptionService, MSTermType, MetadataColumnService, MetadataColumnTemplateService, MetadataColumnTemplateShareService, MetadataManagementService, MetadataTableEditModal, MetadataTableService, MetadataTableTemplateEditModal, MetadataTableTemplateService, MetadataTableTemplates, MetadataValidationModal, MetadataValueEditModal, NotificationService, NotificationType, OFFICIAL_SDRF_COLUMNS, ONTOLOGY_TYPE_CONFIGS, OntologySearchService, OntologyType, OntologyTypeLabels, OntologyUtils, SDRFDefaultsService, SamplePoolService, SchemaSelectionModal, SchemaService, SdrfAgeInput, SdrfCleavageInput, SdrfColumnCategory, SdrfModificationInput, SdrfSpikedCompoundInput, SdrfSyntaxService, TASK_STATUS_COLORS, TASK_STATUS_LABELS, TASK_TYPE_LABELS, TableCreationModalComponent, TemplatePermissionLabels, TemplatePermissionLevel, TemplateSchemaSelectionModal, ValidationResultsModal, Websocket, getRequiredSdrfColumns, getSdrfColumnByName, getSdrfColumnsByCategory, isSdrfColumnRequired, isUnimodFullData };
7433
+ export { AdminWebSocketService, AsyncExportService, AsyncImportService, AsyncTaskService, AsyncTaskUIService, AsyncValidationService, COLUMN_TYPE_CONFIGS, ChunkedImportService, ChunkedUploadService, ColumnEditModal, ColumnTemplateEditModal, ColumnTemplates, ColumnType, CupcakeVanilla, ElectronService, ExcelExportModalComponent, FavoriteManagement, FavouriteMetadataOptionService, MSTermType, MetadataColumnService, MetadataColumnTemplateService, MetadataColumnTemplateShareService, MetadataManagementService, MetadataTableEditModal, MetadataTableService, MetadataTableTemplateEditModal, MetadataTableTemplateService, MetadataTableTemplates, MetadataValidationModal, MetadataValueEditModal, NotificationService, NotificationType, OFFICIAL_SDRF_COLUMNS, ONTOLOGY_TYPE_CONFIGS, OntologySearchService, OntologyType, OntologyTypeLabels, OntologyUtils, SDRFDefaultsService, SamplePoolService, SchemaSelectionModal, SchemaService, SdrfAgeInput, SdrfCleavageInput, SdrfColumnCategory, SdrfModificationInput, SdrfSpikedCompoundInput, SdrfSyntaxService, TASK_STATUS_COLORS, TASK_STATUS_LABELS, TASK_TYPE_LABELS, TableCreationModalComponent, TemplatePermissionLabels, TemplatePermissionLevel, TemplateSchemaSelectionModal, ValidationResultsModal, Websocket, getRequiredSdrfColumns, getSdrfColumnByName, getSdrfColumnsByCategory, isSdrfColumnRequired, isUnimodFullData };
6073
7434
  //# sourceMappingURL=noatgnu-cupcake-vanilla.mjs.map