@noatgnu/cupcake-vanilla 1.2.12 → 1.2.13

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.
@@ -1,9 +1,9 @@
1
1
  import * as i0 from '@angular/core';
2
- import { Injectable, signal, computed, EventEmitter, inject, Output, Input, Component, effect } from '@angular/core';
2
+ import { Injectable, signal, computed, EventEmitter, inject, Output, Input, Component, effect, ViewChild } from '@angular/core';
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, firstValueFrom } 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, takeUntil as takeUntil$1 } from 'rxjs';
7
7
  import jsSHA from 'jssha';
8
8
  import * as i5 from '@angular/common';
9
9
  import { CommonModule } from '@angular/common';
@@ -13,6 +13,7 @@ import * as i2 from '@ng-bootstrap/ng-bootstrap';
13
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 * as i1$1 from '@angular/router';
16
17
  import { RouterModule } from '@angular/router';
17
18
 
18
19
  var OntologyType;
@@ -657,6 +658,12 @@ class MetadataTableService extends BaseApiService {
657
658
  confirmed
658
659
  });
659
660
  }
661
+ /**
662
+ * Replace all occurrences of a specific value across columns in this table
663
+ */
664
+ replaceColumnValue(id, request) {
665
+ return this.post(`${this.apiUrl}/metadata-tables/${id}/replace_column_value/`, request);
666
+ }
660
667
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.7", ngImport: i0, type: MetadataTableService, deps: null, target: i0.ɵɵFactoryTarget.Injectable });
661
668
  static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.7", ngImport: i0, type: MetadataTableService, providedIn: 'root' });
662
669
  }
@@ -749,6 +756,12 @@ class MetadataColumnService extends BaseApiService {
749
756
  const httpParams = this.buildHttpParams(params);
750
757
  return this.get(`${this.apiUrl}/metadata-columns/detect_ontology_type/`, { params: httpParams });
751
758
  }
759
+ /**
760
+ * Replace all occurrences of a specific value with a new value in this column
761
+ */
762
+ replaceValue(id, request) {
763
+ return this.post(`${this.apiUrl}/metadata-columns/${id}/replace_value/`, request);
764
+ }
752
765
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.7", ngImport: i0, type: MetadataColumnService, deps: null, target: i0.ɵɵFactoryTarget.Injectable });
753
766
  static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.7", ngImport: i0, type: MetadataColumnService, providedIn: 'root' });
754
767
  }
@@ -3447,7 +3460,12 @@ class MetadataValueEditModal {
3447
3460
  clearValue() {
3448
3461
  this.editForm.get('value')?.setValue('');
3449
3462
  this.editForm.get('value')?.markAsTouched();
3450
- this.selectedFavorite.set(null); // Clear selected favorite when manually clearing
3463
+ this.selectedFavorite.set(null);
3464
+ }
3465
+ setValue(value) {
3466
+ this.editForm.get('value')?.setValue(value);
3467
+ this.editForm.get('value')?.markAsTouched();
3468
+ this.selectedFavorite.set(null);
3451
3469
  }
3452
3470
  // Favorite options methods
3453
3471
  loadFavoriteOptions() {
@@ -3717,7 +3735,7 @@ class MetadataValueEditModal {
3717
3735
  }
3718
3736
  }
3719
3737
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.7", 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 });
3720
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.7", 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" }] });
3738
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.7", 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 <!-- Quick-select buttons for standard values -->\n @if (config.allowNotApplicable || config.allowNotAvailable) {\n <div class=\"mb-2\">\n <small class=\"text-muted d-block mb-1\">Quick Select:</small>\n <div class=\"btn-group btn-group-sm\" role=\"group\">\n @if (config.allowNotApplicable) {\n <button\n type=\"button\"\n class=\"btn btn-outline-secondary\"\n (click)=\"setValue('not applicable')\">\n <i class=\"bi bi-dash-circle me-1\"></i>Not Applicable\n </button>\n }\n @if (config.allowNotAvailable) {\n <button\n type=\"button\"\n class=\"btn btn-outline-secondary\"\n (click)=\"setValue('not available')\">\n <i class=\"bi bi-question-circle me-1\"></i>Not Available\n </button>\n }\n </div>\n </div>\n }\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" }] });
3721
3739
  }
3722
3740
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.7", ngImport: i0, type: MetadataValueEditModal, decorators: [{
3723
3741
  type: Component,
@@ -3730,7 +3748,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.7", ngImpor
3730
3748
  SdrfModificationInput,
3731
3749
  SdrfCleavageInput,
3732
3750
  SdrfSpikedCompoundInput
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"] }]
3751
+ ], 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 <!-- Quick-select buttons for standard values -->\n @if (config.allowNotApplicable || config.allowNotAvailable) {\n <div class=\"mb-2\">\n <small class=\"text-muted d-block mb-1\">Quick Select:</small>\n <div class=\"btn-group btn-group-sm\" role=\"group\">\n @if (config.allowNotApplicable) {\n <button\n type=\"button\"\n class=\"btn btn-outline-secondary\"\n (click)=\"setValue('not applicable')\">\n <i class=\"bi bi-dash-circle me-1\"></i>Not Applicable\n </button>\n }\n @if (config.allowNotAvailable) {\n <button\n type=\"button\"\n class=\"btn btn-outline-secondary\"\n (click)=\"setValue('not available')\">\n <i class=\"bi bi-question-circle me-1\"></i>Not Available\n </button>\n }\n </div>\n </div>\n }\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"] }]
3734
3752
  }], ctorParameters: () => [{ type: i1.FormBuilder }, { type: i2.NgbActiveModal }, { type: i3.AuthService }, { type: FavouriteMetadataOptionService }, { type: MetadataColumnService }, { type: MetadataColumnTemplateService }, { type: i3.LabGroupService }, { type: OntologySearchService }], propDecorators: { config: [{
3735
3753
  type: Input
3736
3754
  }], valueSaved: [{
@@ -4626,7 +4644,9 @@ class ColumnEditModal {
4626
4644
  mandatory: [false],
4627
4645
  hidden: [false],
4628
4646
  readonly: [false],
4629
- notApplicable: [false]
4647
+ notApplicable: [false],
4648
+ notAvailable: [false],
4649
+ staffOnly: [false]
4630
4650
  });
4631
4651
  }
4632
4652
  ngOnInit() {
@@ -4654,7 +4674,9 @@ class ColumnEditModal {
4654
4674
  mandatory: this.column.mandatory || false,
4655
4675
  hidden: this.column.hidden || false,
4656
4676
  readonly: this.column.readonly || false,
4657
- notApplicable: this.column.notApplicable || false
4677
+ notApplicable: this.column.notApplicable || false,
4678
+ notAvailable: this.column.notAvailable || false,
4679
+ staffOnly: this.column.staffOnly || false
4658
4680
  });
4659
4681
  }
4660
4682
  }
@@ -4678,7 +4700,9 @@ class ColumnEditModal {
4678
4700
  mandatory: formValue.mandatory || false,
4679
4701
  hidden: formValue.hidden || false,
4680
4702
  readonly: formValue.readonly || false,
4681
- notApplicable: formValue.notApplicable || false
4703
+ notApplicable: formValue.notApplicable || false,
4704
+ notAvailable: formValue.notAvailable || false,
4705
+ staffOnly: formValue.staffOnly || false
4682
4706
  };
4683
4707
  this.columnSaved.emit(columnData);
4684
4708
  }
@@ -4733,7 +4757,9 @@ class ColumnEditModal {
4733
4757
  ontologyType: ontologyType,
4734
4758
  enableTypeahead: enableTypeahead,
4735
4759
  currentValue: this.editForm.get('value')?.value || '',
4736
- context: this.isEdit ? 'table' : 'template'
4760
+ context: this.isEdit ? 'table' : 'template',
4761
+ allowNotApplicable: this.editForm.get('notApplicable')?.value || false,
4762
+ allowNotAvailable: this.editForm.get('notAvailable')?.value || false
4737
4763
  };
4738
4764
  const hasTemplate = this.selectedTemplate || this.column?.template || this.templateId;
4739
4765
  if (hasTemplate) {
@@ -4761,6 +4787,10 @@ class ColumnEditModal {
4761
4787
  modalRef.componentInstance.onClose();
4762
4788
  });
4763
4789
  }
4790
+ setDefaultValue(value) {
4791
+ this.editForm.get('value')?.setValue(value);
4792
+ this.editForm.get('value')?.markAsTouched();
4793
+ }
4764
4794
  // Template selection methods
4765
4795
  loadColumnTemplates() {
4766
4796
  this.isLoadingTemplates = true;
@@ -5094,11 +5124,11 @@ class ColumnEditModal {
5094
5124
  return columnType;
5095
5125
  }
5096
5126
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.7", ngImport: i0, type: ColumnEditModal, deps: [{ token: i1.FormBuilder }, { token: i2.NgbActiveModal }, { token: i2.NgbModal }, { token: MetadataColumnTemplateService }, { token: SdrfSyntaxService }], target: i0.ɵɵFactoryTarget.Component });
5097
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.7", type: ColumnEditModal, isStandalone: true, selector: "ccv-column-edit-modal", inputs: { column: "column", templateId: "templateId", isEdit: "isEdit" }, outputs: { columnSaved: "columnSaved" }, ngImport: i0, template: "<div class=\"modal-header\">\n <h4 class=\"modal-title\">\n <i class=\"bi bi-columns-gap 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 <!-- Selection Required Notice for New Columns -->\n @if (!isEdit) {\n <div class=\"col-12 mb-3\">\n <div class=\"alert alert-warning\">\n <h6 class=\"alert-heading\">\n <i class=\"bi bi-exclamation-triangle me-2\"></i>Column Selection Required\n </h6>\n <p class=\"mb-0\">\n You must select from official SDRF columns or existing templates to ensure data compliance and consistency.\n </p>\n </div>\n </div>\n }\n\n <!-- Official SDRF Columns -->\n <div class=\"col-12 mb-3\">\n <h6 class=\"text-muted border-bottom pb-2\">\n <i class=\"bi bi-file-earmark-check me-2\"></i>Official SDRF Columns\n <small class=\"text-muted ms-2\">\n @if (!isEdit) {\n (Step 1: Select from standardized SDRF-Proteomics column names)\n } @else {\n (Recommended - select from standardized SDRF-Proteomics column names)\n }\n </small>\n </h6>\n </div>\n\n @if (!isEdit) {\n <div class=\"col-12 mb-3\">\n @if (!showOfficialColumns) {\n <button\n type=\"button\"\n class=\"btn btn-primary mb-3\"\n (click)=\"toggleOfficialColumns()\">\n <i class=\"bi bi-list-ul me-2\"></i>\n Select from Official SDRF Columns\n </button>\n } @else {\n <button\n type=\"button\"\n class=\"btn btn-outline-secondary mb-3\"\n (click)=\"toggleOfficialColumns()\">\n <i class=\"bi bi-x me-2\"></i>\n Hide Official Columns\n </button>\n }\n </div>\n } @else {\n <div class=\"col-12 mb-3\">\n <button\n type=\"button\"\n class=\"btn btn-outline-info mb-3\"\n (click)=\"toggleOfficialColumns()\">\n <i class=\"bi bi-list-ul me-2\"></i>\n {{ showOfficialColumns ? 'Hide' : 'Show' }} Official Column Names\n </button>\n </div>\n }\n\n @if (showOfficialColumns) {\n <div class=\"col-12 mb-3\">\n <div class=\"border rounded p-3\">\n <p class=\"mb-3 text-muted\">\n <i class=\"bi bi-info-circle me-2\"></i>\n Select from official SDRF-Proteomics column names. Only columns without existing templates are shown.\n </p>\n\n @if (availableOfficialColumns.length === 0) {\n <div class=\"alert alert-warning\">\n <i class=\"bi bi-exclamation-triangle me-2\"></i>\n All official SDRF columns already have templates created.\n </div>\n } @else {\n <div class=\"row\">\n @for (column of availableOfficialColumns; track column.name) {\n <div class=\"col-md-6 mb-2\">\n <div class=\"card h-100 cursor-pointer border-info\"\n (click)=\"selectOfficialColumn(column)\"\n [class.border-primary]=\"selectedOfficialColumn?.name === column.name\">\n <div class=\"card-body py-2\">\n <h6 class=\"card-title mb-1 font-monospace\">\n {{ column.name }}\n </h6>\n <p class=\"card-text small mb-1\">\n <span class=\"badge\"\n [class]=\"column.type === 'characteristics' ? 'bg-primary' :\n column.type === 'comment' ? 'bg-success' :\n column.type === 'factor_value' ? 'bg-warning' : 'bg-secondary'\">\n {{ column.type }}\n </span>\n </p>\n <p class=\"card-text small mb-0\">{{ column.description }}</p>\n </div>\n </div>\n </div>\n }\n </div>\n }\n </div>\n </div>\n }\n\n <!-- Template Swapping for Edit Mode -->\n @if (isEdit && showTemplateSwap) {\n <div class=\"col-12 mb-3\">\n <div class=\"alert alert-info\">\n <h6 class=\"alert-heading\">\n <i class=\"bi bi-arrow-left-right me-2\"></i>Swap to Different Template\n </h6>\n <p class=\"mb-3\">\n Choose a different template to change the column name and settings. Your current value will be preserved.\n </p>\n\n <div class=\"mb-3\">\n <input\n type=\"text\"\n class=\"form-control mb-2\"\n placeholder=\"Search templates...\"\n [(ngModel)]=\"templateFilter\"\n [ngModelOptions]=\"{standalone: true}\"\n (input)=\"onTemplateFilterChange()\">\n\n <select\n class=\"form-select mb-3\"\n size=\"4\"\n [disabled]=\"isLoadingTemplates\">\n @if (isLoadingTemplates) {\n <option value=\"\" disabled>Loading templates...</option>\n } @else {\n @for (template of templates; track template.id) {\n <option\n [value]=\"template.id\"\n (click)=\"swapToTemplate(template)\"\n style=\"cursor: pointer;\">\n {{ template.columnName }} - {{ template.columnType }}@if (template.ontologyType) { ({{ getOntologyTypeLabel(template.ontologyType) }})}\n </option>\n }\n }\n </select>\n </div>\n\n <div class=\"d-flex justify-content-between\">\n <button\n type=\"button\"\n class=\"btn btn-success\"\n (click)=\"showCreateCustomTemplateOption()\">\n <i class=\"bi bi-plus-circle me-1\"></i>Create Custom Template\n </button>\n <button\n type=\"button\"\n class=\"btn btn-secondary\"\n (click)=\"cancelTemplateSwap()\">\n <i class=\"bi bi-x-circle me-1\"></i>Cancel\n </button>\n </div>\n </div>\n </div>\n }\n\n <!-- Custom Template Creation Option -->\n @if (isEdit && showCreateCustomTemplate) {\n <div class=\"col-12 mb-3\">\n <div class=\"alert alert-warning\">\n <h6 class=\"alert-heading\">\n <i class=\"bi bi-plus-circle me-2\"></i>Create Custom Template\n </h6>\n <p class=\"mb-3\">\n This will create a new custom template with your desired column name and settings.\n </p>\n\n <div class=\"d-flex justify-content-between\">\n <button\n type=\"button\"\n class=\"btn btn-warning\"\n (click)=\"createCustomTemplate()\">\n <i class=\"bi bi-gear me-1\"></i>Create & Apply Template\n </button>\n <button\n type=\"button\"\n class=\"btn btn-secondary\"\n (click)=\"cancelTemplateSwap()\">\n <i class=\"bi bi-x-circle me-1\"></i>Cancel\n </button>\n </div>\n </div>\n </div>\n }\n\n <!-- Template Selection -->\n <div class=\"col-12 mb-3\">\n <h6 class=\"text-muted border-bottom pb-2\">\n <i class=\"bi bi-file-earmark-text me-2\"></i>Template Selection\n <small class=\"text-muted ms-2\">\n @if (!isEdit) {\n (Step 2: Or select from existing templates)\n } @else {\n (Optional - select a template to auto-fill column settings)\n }\n </small>\n </h6>\n </div>\n\n <div class=\"col-12 mb-3\">\n <div class=\"input-group mb-3\">\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 placeholder=\"Search column templates...\"\n [(ngModel)]=\"templateFilter\"\n [ngModelOptions]=\"{standalone: true}\"\n (input)=\"onTemplateFilterChange()\">\n @if (templateFilter) {\n <button class=\"btn btn-outline-secondary\" type=\"button\" (click)=\"clearTemplateFilter()\">\n <i class=\"bi bi-x\"></i>\n </button>\n }\n </div>\n\n <select \n class=\"form-select mb-3\"\n size=\"5\"\n [(ngModel)]=\"selectedTemplateId\"\n [ngModelOptions]=\"{standalone: true}\"\n (change)=\"onTemplateSelectionChange()\"\n [disabled]=\"isLoadingTemplates\">\n <option value=\"\">No template selected</option>\n @if (isLoadingTemplates) {\n <option value=\"\" disabled>Loading templates...</option>\n } @else {\n @for (template of templates; track template.id) {\n <option [value]=\"template.id\">\n {{ template.columnName }} - {{ template.columnType }}@if (template.ontologyType) { ({{ getOntologyTypeLabel(template.ontologyType) }})}\n </option>\n }\n }\n </select>\n\n @if (totalTemplatePages > 1) {\n <div class=\"d-flex justify-content-between align-items-center\">\n <small class=\"text-muted\">\n Showing {{ (currentTemplatePage - 1) * templatePageSize + 1 }}-{{ Math.min(currentTemplatePage * templatePageSize, totalTemplates) }}\n of {{ totalTemplates }}\n </small>\n <ngb-pagination\n [collectionSize]=\"totalTemplates\"\n [page]=\"currentTemplatePage\"\n [pageSize]=\"templatePageSize\"\n [maxSize]=\"3\"\n [boundaryLinks]=\"true\"\n [rotate]=\"true\"\n (pageChange)=\"onTemplatePageChange($event)\"\n class=\"pagination-sm mb-0\">\n </ngb-pagination>\n </div>\n }\n\n @if (selectedTemplate) {\n <div class=\"alert alert-info mt-3\">\n <i class=\"bi bi-info-circle me-2\"></i>\n Template \"{{ selectedTemplate.name }}\" selected. Settings will be applied automatically.\n <button type=\"button\" class=\"btn btn-sm btn-outline-info float-end\" (click)=\"clearTemplate()\">\n Clear Selection\n </button>\n </div>\n }\n </div>\n\n <!-- Manual Entry -->\n <div class=\"col-12 mb-3\">\n <h6 class=\"text-muted border-bottom pb-2\">\n <i class=\"bi bi-pencil me-2\"></i>Column Details\n @if (selectedTemplate) {\n <small class=\"text-muted ms-2\">(Auto-filled from template - you can modify as needed)</small>\n }\n </h6>\n </div>\n \n <div class=\"col-12 mb-3\">\n <label for=\"columnName\" class=\"form-label\">\n Column Name *\n <span class=\"text-muted ms-2\">\n <i class=\"bi bi-info-circle\"></i>\n Column names are managed through templates\n </span>\n </label>\n <div class=\"input-group\">\n <input\n type=\"text\"\n class=\"form-control\"\n id=\"columnName\"\n formControlName=\"name\"\n [class.is-invalid]=\"editForm.get('name')?.invalid && editForm.get('name')?.touched\"\n readonly\n placeholder=\"Select from official columns or templates below\">\n @if (isEdit) {\n <button\n type=\"button\"\n class=\"btn btn-outline-secondary\"\n (click)=\"showTemplateSwapOptions()\"\n title=\"Change to different template\">\n <i class=\"bi bi-arrow-left-right\"></i> Swap Template\n </button>\n }\n </div>\n <div class=\"form-text\">\n <i class=\"bi bi-info-circle me-1\"></i>\n @if (isEdit) {\n To change the column name, swap to a different template or create a new custom template.\n } @else {\n Select from official SDRF columns or existing templates below to ensure compliance.\n }\n </div>\n </div>\n\n <div class=\"col-md-6 mb-3\">\n <label for=\"columnType\" class=\"form-label\">\n Column Type *\n <span class=\"text-muted ms-2\">\n <i class=\"bi bi-info-circle\"></i>\n Managed through templates\n </span>\n </label>\n <select\n class=\"form-select\"\n id=\"columnType\"\n formControlName=\"type\"\n [class.is-invalid]=\"editForm.get('type')?.invalid && editForm.get('type')?.touched\">\n @for (type of columnTypes; track type.value) {\n <option [value]=\"type.value\">{{ type.label }}</option>\n }\n </select>\n </div>\n\n <!-- Advanced Mode Toggle -->\n <div class=\"col-12 mb-3\">\n <div class=\"form-check form-switch\">\n <input\n class=\"form-check-input\"\n type=\"checkbox\"\n id=\"advancedMode\"\n [checked]=\"showAdvancedMode()\"\n (change)=\"toggleAdvancedMode()\">\n <label class=\"form-check-label\" for=\"advancedMode\">\n <i class=\"bi bi-gear me-1\"></i>Advanced Mode\n </label>\n <div class=\"form-text\">\n <i class=\"bi bi-info-circle me-1\"></i>\n Show advanced options for ontology configuration and column settings\n </div>\n </div>\n </div>\n\n @if (showAdvancedMode()) {\n <div class=\"col-md-6 mb-3\">\n <label for=\"ontologyType\" class=\"form-label\">\n Ontology Type\n <span class=\"badge bg-warning ms-2\">\n <i class=\"bi bi-exclamation-triangle me-1\"></i>Advanced\n </span>\n </label>\n <select\n class=\"form-select\"\n id=\"ontologyType\"\n formControlName=\"ontologyType\"\n (change)=\"onOntologyTypeChange($event)\">\n @for (ontology of ontologyTypes; track $index) {\n <option [value]=\"$index\">{{ ontology.label }}</option>\n }\n </select>\n\n @if (hasOntologyType) {\n <div class=\"form-text mt-2\">\n <i class=\"bi bi-info-circle me-1\"></i>\n Custom ontology filters will be applied based on the selected type\n </div>\n }\n </div>\n\n @if (hasOntologyType) {\n <div class=\"col-12 mb-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 <i class=\"bi bi-search me-1\"></i>Enable Typeahead Autocomplete\n </label>\n <div class=\"form-text\">\n <i class=\"bi bi-info-circle me-1\"></i>\n Enable autocomplete suggestions from {{ selectedOntologyConfig?.label || 'selected' }} ontology when editing values\n </div>\n </div>\n </div>\n }\n\n <!-- Column Options - Advanced Mode Only -->\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>Column Options\n <span class=\"badge bg-warning ms-2\">\n <i class=\"bi bi-exclamation-triangle me-1\"></i>Advanced\n </span>\n </h6>\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=\"mandatory\"\n formControlName=\"mandatory\">\n <label class=\"form-check-label\" for=\"mandatory\">\n <i class=\"bi bi-exclamation-circle me-1\"></i>Mandatory\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=\"hidden\"\n formControlName=\"hidden\">\n <label class=\"form-check-label\" for=\"hidden\">\n <i class=\"bi bi-eye-slash me-1\"></i>Hidden\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=\"readonly\"\n formControlName=\"readonly\">\n <label class=\"form-check-label\" for=\"readonly\">\n <i class=\"bi bi-lock me-1\"></i>Read Only\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=\"notApplicable\"\n formControlName=\"notApplicable\">\n <label class=\"form-check-label\" for=\"notApplicable\">\n <i class=\"bi bi-dash-circle me-1\"></i>N/A\n </label>\n </div>\n </div>\n </div>\n </div>\n }\n\n <div class=\"col-12 mb-3\">\n <label for=\"defaultValue\" class=\"form-label\">\n Default Value\n @if (hasTypeaheadEnabled) {\n <span class=\"badge bg-success ms-2\">\n <i class=\"bi bi-search me-1\"></i>Typeahead Enabled\n </span>\n }\n </label>\n \n <div class=\"input-group\">\n <input \n type=\"text\" \n class=\"form-control\"\n id=\"defaultValue\"\n formControlName=\"value\"\n placeholder=\"Enter default 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 (hasTypeaheadEnabled) {\n <i class=\"bi bi-info-circle me-1\"></i>\n Click \"Edit\" to use autocomplete suggestions from {{ selectedOntologyConfig?.label || 'selected' }} ontology\n } @else if (hasOntologyType) {\n <i class=\"bi bi-info-circle me-1\"></i>\n Click \"Edit\" to set default value. Enable typeahead in Advanced Mode for autocomplete suggestions.\n } @else if (showAdvancedMode()) {\n <i class=\"bi bi-info-circle me-1\"></i>\n Click \"Edit\" to set the default value (select an ontology type above for smart suggestions)\n } @else {\n <i class=\"bi bi-info-circle me-1\"></i>\n Click \"Edit\" to set the default value (enable Advanced Mode for ontology autocomplete)\n }\n </div>\n </div>\n\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' : 'Add' }} Column\n </button>\n</div>\n", styles: [".template-selection-container{border:1px solid var(--bs-border-color);border-radius:.375rem;padding:.5rem}.template-item{cursor:pointer;transition:all .2s ease}.template-item:hover{background-color:rgba(var(--bs-primary-rgb),.1);border-color:var(--bs-primary)!important}.template-item.selected{background-color:rgba(var(--bs-primary-rgb),.15);border-color:var(--bs-primary)!important;box-shadow:0 0 0 .2rem rgba(var(--bs-primary-rgb),.25)}.cursor-pointer{cursor:pointer}.card.cursor-pointer{transition:all .2s ease}.card.cursor-pointer:hover{transform:translateY(-2px);box-shadow:0 4px 8px #0000001a}.card.cursor-pointer.border-info{border-width:2px}.modal-lg{max-width:900px}\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: FormsModule }, { kind: "directive", type: i1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { 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"] }] });
5127
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.7", type: ColumnEditModal, isStandalone: true, selector: "ccv-column-edit-modal", inputs: { column: "column", templateId: "templateId", isEdit: "isEdit" }, outputs: { columnSaved: "columnSaved" }, ngImport: i0, template: "<div class=\"modal-header\">\n <h4 class=\"modal-title\">\n <i class=\"bi bi-columns-gap 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 <!-- Selection Required Notice for New Columns -->\n @if (!isEdit) {\n <div class=\"col-12 mb-3\">\n <div class=\"alert alert-warning\">\n <h6 class=\"alert-heading\">\n <i class=\"bi bi-exclamation-triangle me-2\"></i>Column Selection Required\n </h6>\n <p class=\"mb-0\">\n You must select from official SDRF columns or existing templates to ensure data compliance and consistency.\n </p>\n </div>\n </div>\n }\n\n <!-- Official SDRF Columns -->\n <div class=\"col-12 mb-3\">\n <h6 class=\"text-muted border-bottom pb-2\">\n <i class=\"bi bi-file-earmark-check me-2\"></i>Official SDRF Columns\n <small class=\"text-muted ms-2\">\n @if (!isEdit) {\n (Step 1: Select from standardized SDRF-Proteomics column names)\n } @else {\n (Recommended - select from standardized SDRF-Proteomics column names)\n }\n </small>\n </h6>\n </div>\n\n @if (!isEdit) {\n <div class=\"col-12 mb-3\">\n @if (!showOfficialColumns) {\n <button\n type=\"button\"\n class=\"btn btn-primary mb-3\"\n (click)=\"toggleOfficialColumns()\">\n <i class=\"bi bi-list-ul me-2\"></i>\n Select from Official SDRF Columns\n </button>\n } @else {\n <button\n type=\"button\"\n class=\"btn btn-outline-secondary mb-3\"\n (click)=\"toggleOfficialColumns()\">\n <i class=\"bi bi-x me-2\"></i>\n Hide Official Columns\n </button>\n }\n </div>\n } @else {\n <div class=\"col-12 mb-3\">\n <button\n type=\"button\"\n class=\"btn btn-outline-info mb-3\"\n (click)=\"toggleOfficialColumns()\">\n <i class=\"bi bi-list-ul me-2\"></i>\n {{ showOfficialColumns ? 'Hide' : 'Show' }} Official Column Names\n </button>\n </div>\n }\n\n @if (showOfficialColumns) {\n <div class=\"col-12 mb-3\">\n <div class=\"border rounded p-3\">\n <p class=\"mb-3 text-muted\">\n <i class=\"bi bi-info-circle me-2\"></i>\n Select from official SDRF-Proteomics column names. Only columns without existing templates are shown.\n </p>\n\n @if (availableOfficialColumns.length === 0) {\n <div class=\"alert alert-warning\">\n <i class=\"bi bi-exclamation-triangle me-2\"></i>\n All official SDRF columns already have templates created.\n </div>\n } @else {\n <div class=\"row\">\n @for (column of availableOfficialColumns; track column.name) {\n <div class=\"col-md-6 mb-2\">\n <div class=\"card h-100 cursor-pointer border-info\"\n (click)=\"selectOfficialColumn(column)\"\n [class.border-primary]=\"selectedOfficialColumn?.name === column.name\">\n <div class=\"card-body py-2\">\n <h6 class=\"card-title mb-1 font-monospace\">\n {{ column.name }}\n </h6>\n <p class=\"card-text small mb-1\">\n <span class=\"badge\"\n [class]=\"column.type === 'characteristics' ? 'bg-primary' :\n column.type === 'comment' ? 'bg-success' :\n column.type === 'factor_value' ? 'bg-warning' : 'bg-secondary'\">\n {{ column.type }}\n </span>\n </p>\n <p class=\"card-text small mb-0\">{{ column.description }}</p>\n </div>\n </div>\n </div>\n }\n </div>\n }\n </div>\n </div>\n }\n\n <!-- Template Swapping for Edit Mode -->\n @if (isEdit && showTemplateSwap) {\n <div class=\"col-12 mb-3\">\n <div class=\"alert alert-info\">\n <h6 class=\"alert-heading\">\n <i class=\"bi bi-arrow-left-right me-2\"></i>Swap to Different Template\n </h6>\n <p class=\"mb-3\">\n Choose a different template to change the column name and settings. Your current value will be preserved.\n </p>\n\n <div class=\"mb-3\">\n <input\n type=\"text\"\n class=\"form-control mb-2\"\n placeholder=\"Search templates...\"\n [(ngModel)]=\"templateFilter\"\n [ngModelOptions]=\"{standalone: true}\"\n (input)=\"onTemplateFilterChange()\">\n\n <select\n class=\"form-select mb-3\"\n size=\"4\"\n [disabled]=\"isLoadingTemplates\">\n @if (isLoadingTemplates) {\n <option value=\"\" disabled>Loading templates...</option>\n } @else {\n @for (template of templates; track template.id) {\n <option\n [value]=\"template.id\"\n (click)=\"swapToTemplate(template)\"\n style=\"cursor: pointer;\">\n {{ template.columnName }} - {{ template.columnType }}@if (template.ontologyType) { ({{ getOntologyTypeLabel(template.ontologyType) }})}\n </option>\n }\n }\n </select>\n </div>\n\n <div class=\"d-flex justify-content-between\">\n <button\n type=\"button\"\n class=\"btn btn-success\"\n (click)=\"showCreateCustomTemplateOption()\">\n <i class=\"bi bi-plus-circle me-1\"></i>Create Custom Template\n </button>\n <button\n type=\"button\"\n class=\"btn btn-secondary\"\n (click)=\"cancelTemplateSwap()\">\n <i class=\"bi bi-x-circle me-1\"></i>Cancel\n </button>\n </div>\n </div>\n </div>\n }\n\n <!-- Custom Template Creation Option -->\n @if (isEdit && showCreateCustomTemplate) {\n <div class=\"col-12 mb-3\">\n <div class=\"alert alert-warning\">\n <h6 class=\"alert-heading\">\n <i class=\"bi bi-plus-circle me-2\"></i>Create Custom Template\n </h6>\n <p class=\"mb-3\">\n This will create a new custom template with your desired column name and settings.\n </p>\n\n <div class=\"d-flex justify-content-between\">\n <button\n type=\"button\"\n class=\"btn btn-warning\"\n (click)=\"createCustomTemplate()\">\n <i class=\"bi bi-gear me-1\"></i>Create & Apply Template\n </button>\n <button\n type=\"button\"\n class=\"btn btn-secondary\"\n (click)=\"cancelTemplateSwap()\">\n <i class=\"bi bi-x-circle me-1\"></i>Cancel\n </button>\n </div>\n </div>\n </div>\n }\n\n <!-- Template Selection -->\n <div class=\"col-12 mb-3\">\n <h6 class=\"text-muted border-bottom pb-2\">\n <i class=\"bi bi-file-earmark-text me-2\"></i>Template Selection\n <small class=\"text-muted ms-2\">\n @if (!isEdit) {\n (Step 2: Or select from existing templates)\n } @else {\n (Optional - select a template to auto-fill column settings)\n }\n </small>\n </h6>\n </div>\n\n <div class=\"col-12 mb-3\">\n <div class=\"input-group mb-3\">\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 placeholder=\"Search column templates...\"\n [(ngModel)]=\"templateFilter\"\n [ngModelOptions]=\"{standalone: true}\"\n (input)=\"onTemplateFilterChange()\">\n @if (templateFilter) {\n <button class=\"btn btn-outline-secondary\" type=\"button\" (click)=\"clearTemplateFilter()\">\n <i class=\"bi bi-x\"></i>\n </button>\n }\n </div>\n\n <select \n class=\"form-select mb-3\"\n size=\"5\"\n [(ngModel)]=\"selectedTemplateId\"\n [ngModelOptions]=\"{standalone: true}\"\n (change)=\"onTemplateSelectionChange()\"\n [disabled]=\"isLoadingTemplates\">\n <option value=\"\">No template selected</option>\n @if (isLoadingTemplates) {\n <option value=\"\" disabled>Loading templates...</option>\n } @else {\n @for (template of templates; track template.id) {\n <option [value]=\"template.id\">\n {{ template.columnName }} - {{ template.columnType }}@if (template.ontologyType) { ({{ getOntologyTypeLabel(template.ontologyType) }})}\n </option>\n }\n }\n </select>\n\n @if (totalTemplatePages > 1) {\n <div class=\"d-flex justify-content-between align-items-center\">\n <small class=\"text-muted\">\n Showing {{ (currentTemplatePage - 1) * templatePageSize + 1 }}-{{ Math.min(currentTemplatePage * templatePageSize, totalTemplates) }}\n of {{ totalTemplates }}\n </small>\n <ngb-pagination\n [collectionSize]=\"totalTemplates\"\n [page]=\"currentTemplatePage\"\n [pageSize]=\"templatePageSize\"\n [maxSize]=\"3\"\n [boundaryLinks]=\"true\"\n [rotate]=\"true\"\n (pageChange)=\"onTemplatePageChange($event)\"\n class=\"pagination-sm mb-0\">\n </ngb-pagination>\n </div>\n }\n\n @if (selectedTemplate) {\n <div class=\"alert alert-info mt-3\">\n <i class=\"bi bi-info-circle me-2\"></i>\n Template \"{{ selectedTemplate.name }}\" selected. Settings will be applied automatically.\n <button type=\"button\" class=\"btn btn-sm btn-outline-info float-end\" (click)=\"clearTemplate()\">\n Clear Selection\n </button>\n </div>\n }\n </div>\n\n <!-- Manual Entry -->\n <div class=\"col-12 mb-3\">\n <h6 class=\"text-muted border-bottom pb-2\">\n <i class=\"bi bi-pencil me-2\"></i>Column Details\n @if (selectedTemplate) {\n <small class=\"text-muted ms-2\">(Auto-filled from template - you can modify as needed)</small>\n }\n </h6>\n </div>\n \n <div class=\"col-12 mb-3\">\n <label for=\"columnName\" class=\"form-label\">\n Column Name *\n <span class=\"text-muted ms-2\">\n <i class=\"bi bi-info-circle\"></i>\n Column names are managed through templates\n </span>\n </label>\n <div class=\"input-group\">\n <input\n type=\"text\"\n class=\"form-control\"\n id=\"columnName\"\n formControlName=\"name\"\n [class.is-invalid]=\"editForm.get('name')?.invalid && editForm.get('name')?.touched\"\n readonly\n placeholder=\"Select from official columns or templates below\">\n @if (isEdit) {\n <button\n type=\"button\"\n class=\"btn btn-outline-secondary\"\n (click)=\"showTemplateSwapOptions()\"\n title=\"Change to different template\">\n <i class=\"bi bi-arrow-left-right\"></i> Swap Template\n </button>\n }\n </div>\n <div class=\"form-text\">\n <i class=\"bi bi-info-circle me-1\"></i>\n @if (isEdit) {\n To change the column name, swap to a different template or create a new custom template.\n } @else {\n Select from official SDRF columns or existing templates below to ensure compliance.\n }\n </div>\n </div>\n\n <div class=\"col-md-6 mb-3\">\n <label for=\"columnType\" class=\"form-label\">\n Column Type *\n <span class=\"text-muted ms-2\">\n <i class=\"bi bi-info-circle\"></i>\n Managed through templates\n </span>\n </label>\n <select\n class=\"form-select\"\n id=\"columnType\"\n formControlName=\"type\"\n [class.is-invalid]=\"editForm.get('type')?.invalid && editForm.get('type')?.touched\">\n @for (type of columnTypes; track type.value) {\n <option [value]=\"type.value\">{{ type.label }}</option>\n }\n </select>\n </div>\n\n <!-- Advanced Mode Toggle -->\n <div class=\"col-12 mb-3\">\n <div class=\"form-check form-switch\">\n <input\n class=\"form-check-input\"\n type=\"checkbox\"\n id=\"advancedMode\"\n [checked]=\"showAdvancedMode()\"\n (change)=\"toggleAdvancedMode()\">\n <label class=\"form-check-label\" for=\"advancedMode\">\n <i class=\"bi bi-gear me-1\"></i>Advanced Mode\n </label>\n <div class=\"form-text\">\n <i class=\"bi bi-info-circle me-1\"></i>\n Show advanced options for ontology configuration and column settings\n </div>\n </div>\n </div>\n\n @if (showAdvancedMode()) {\n <div class=\"col-md-6 mb-3\">\n <label for=\"ontologyType\" class=\"form-label\">\n Ontology Type\n <span class=\"badge bg-warning ms-2\">\n <i class=\"bi bi-exclamation-triangle me-1\"></i>Advanced\n </span>\n </label>\n <select\n class=\"form-select\"\n id=\"ontologyType\"\n formControlName=\"ontologyType\"\n (change)=\"onOntologyTypeChange($event)\">\n @for (ontology of ontologyTypes; track $index) {\n <option [value]=\"$index\">{{ ontology.label }}</option>\n }\n </select>\n\n @if (hasOntologyType) {\n <div class=\"form-text mt-2\">\n <i class=\"bi bi-info-circle me-1\"></i>\n Custom ontology filters will be applied based on the selected type\n </div>\n }\n </div>\n\n @if (hasOntologyType) {\n <div class=\"col-12 mb-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 <i class=\"bi bi-search me-1\"></i>Enable Typeahead Autocomplete\n </label>\n <div class=\"form-text\">\n <i class=\"bi bi-info-circle me-1\"></i>\n Enable autocomplete suggestions from {{ selectedOntologyConfig?.label || 'selected' }} ontology when editing values\n </div>\n </div>\n </div>\n }\n\n <!-- Column Options - Advanced Mode Only -->\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>Column Options\n <span class=\"badge bg-warning ms-2\">\n <i class=\"bi bi-exclamation-triangle me-1\"></i>Advanced\n </span>\n </h6>\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=\"mandatory\"\n formControlName=\"mandatory\">\n <label class=\"form-check-label\" for=\"mandatory\">\n <i class=\"bi bi-exclamation-circle me-1\"></i>Mandatory\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=\"hidden\"\n formControlName=\"hidden\">\n <label class=\"form-check-label\" for=\"hidden\">\n <i class=\"bi bi-eye-slash me-1\"></i>Hidden\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=\"readonly\"\n formControlName=\"readonly\">\n <label class=\"form-check-label\" for=\"readonly\">\n <i class=\"bi bi-lock me-1\"></i>Read Only\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=\"notApplicable\"\n formControlName=\"notApplicable\">\n <label class=\"form-check-label\" for=\"notApplicable\">\n <i class=\"bi bi-dash-circle me-1\"></i>Not Applicable\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=\"notAvailable\"\n formControlName=\"notAvailable\">\n <label class=\"form-check-label\" for=\"notAvailable\">\n <i class=\"bi bi-question-circle me-1\"></i>Not Available\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=\"staffOnly\"\n formControlName=\"staffOnly\">\n <label class=\"form-check-label\" for=\"staffOnly\">\n <i class=\"bi bi-shield-lock me-1\"></i>Staff Only\n </label>\n </div>\n </div>\n </div>\n <div class=\"row mt-2\">\n <div class=\"col-12\">\n <div class=\"alert alert-info py-2\">\n <i class=\"bi bi-info-circle me-1\"></i>\n <small>\n <strong>Staff Only:</strong> When enabled, this column will only be visible and editable by staff members.\n </small>\n </div>\n </div>\n </div>\n </div>\n }\n\n <div class=\"col-12 mb-3\">\n <label for=\"defaultValue\" class=\"form-label\">\n Default Value\n @if (hasTypeaheadEnabled) {\n <span class=\"badge bg-success ms-2\">\n <i class=\"bi bi-search me-1\"></i>Typeahead Enabled\n </span>\n }\n </label>\n\n @if (editForm.get('notApplicable')?.value || editForm.get('notAvailable')?.value) {\n <div class=\"mb-2\">\n <small class=\"text-muted d-block mb-1\">Quick Select:</small>\n <div class=\"btn-group btn-group-sm\" role=\"group\">\n @if (editForm.get('notApplicable')?.value) {\n <button\n type=\"button\"\n class=\"btn btn-outline-secondary\"\n (click)=\"setDefaultValue('not applicable')\">\n <i class=\"bi bi-dash-circle me-1\"></i>Not Applicable\n </button>\n }\n @if (editForm.get('notAvailable')?.value) {\n <button\n type=\"button\"\n class=\"btn btn-outline-secondary\"\n (click)=\"setDefaultValue('not available')\">\n <i class=\"bi bi-question-circle me-1\"></i>Not Available\n </button>\n }\n </div>\n </div>\n }\n\n <div class=\"input-group\">\n <input \n type=\"text\" \n class=\"form-control\"\n id=\"defaultValue\"\n formControlName=\"value\"\n placeholder=\"Enter default 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 (hasTypeaheadEnabled) {\n <i class=\"bi bi-info-circle me-1\"></i>\n Click \"Edit\" to use autocomplete suggestions from {{ selectedOntologyConfig?.label || 'selected' }} ontology\n } @else if (hasOntologyType) {\n <i class=\"bi bi-info-circle me-1\"></i>\n Click \"Edit\" to set default value. Enable typeahead in Advanced Mode for autocomplete suggestions.\n } @else if (showAdvancedMode()) {\n <i class=\"bi bi-info-circle me-1\"></i>\n Click \"Edit\" to set the default value (select an ontology type above for smart suggestions)\n } @else {\n <i class=\"bi bi-info-circle me-1\"></i>\n Click \"Edit\" to set the default value (enable Advanced Mode for ontology autocomplete)\n }\n </div>\n </div>\n\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' : 'Add' }} Column\n </button>\n</div>\n", styles: [".template-selection-container{border:1px solid var(--bs-border-color);border-radius:.375rem;padding:.5rem}.template-item{cursor:pointer;transition:all .2s ease}.template-item:hover{background-color:rgba(var(--bs-primary-rgb),.1);border-color:var(--bs-primary)!important}.template-item.selected{background-color:rgba(var(--bs-primary-rgb),.15);border-color:var(--bs-primary)!important;box-shadow:0 0 0 .2rem rgba(var(--bs-primary-rgb),.25)}.cursor-pointer{cursor:pointer}.card.cursor-pointer{transition:all .2s ease}.card.cursor-pointer:hover{transform:translateY(-2px);box-shadow:0 4px 8px #0000001a}.card.cursor-pointer.border-info{border-width:2px}.modal-lg{max-width:900px}\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: FormsModule }, { kind: "directive", type: i1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { 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"] }] });
5098
5128
  }
5099
5129
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.7", ngImport: i0, type: ColumnEditModal, decorators: [{
5100
5130
  type: Component,
5101
- args: [{ selector: 'ccv-column-edit-modal', imports: [CommonModule, ReactiveFormsModule, FormsModule, NgbModule], template: "<div class=\"modal-header\">\n <h4 class=\"modal-title\">\n <i class=\"bi bi-columns-gap 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 <!-- Selection Required Notice for New Columns -->\n @if (!isEdit) {\n <div class=\"col-12 mb-3\">\n <div class=\"alert alert-warning\">\n <h6 class=\"alert-heading\">\n <i class=\"bi bi-exclamation-triangle me-2\"></i>Column Selection Required\n </h6>\n <p class=\"mb-0\">\n You must select from official SDRF columns or existing templates to ensure data compliance and consistency.\n </p>\n </div>\n </div>\n }\n\n <!-- Official SDRF Columns -->\n <div class=\"col-12 mb-3\">\n <h6 class=\"text-muted border-bottom pb-2\">\n <i class=\"bi bi-file-earmark-check me-2\"></i>Official SDRF Columns\n <small class=\"text-muted ms-2\">\n @if (!isEdit) {\n (Step 1: Select from standardized SDRF-Proteomics column names)\n } @else {\n (Recommended - select from standardized SDRF-Proteomics column names)\n }\n </small>\n </h6>\n </div>\n\n @if (!isEdit) {\n <div class=\"col-12 mb-3\">\n @if (!showOfficialColumns) {\n <button\n type=\"button\"\n class=\"btn btn-primary mb-3\"\n (click)=\"toggleOfficialColumns()\">\n <i class=\"bi bi-list-ul me-2\"></i>\n Select from Official SDRF Columns\n </button>\n } @else {\n <button\n type=\"button\"\n class=\"btn btn-outline-secondary mb-3\"\n (click)=\"toggleOfficialColumns()\">\n <i class=\"bi bi-x me-2\"></i>\n Hide Official Columns\n </button>\n }\n </div>\n } @else {\n <div class=\"col-12 mb-3\">\n <button\n type=\"button\"\n class=\"btn btn-outline-info mb-3\"\n (click)=\"toggleOfficialColumns()\">\n <i class=\"bi bi-list-ul me-2\"></i>\n {{ showOfficialColumns ? 'Hide' : 'Show' }} Official Column Names\n </button>\n </div>\n }\n\n @if (showOfficialColumns) {\n <div class=\"col-12 mb-3\">\n <div class=\"border rounded p-3\">\n <p class=\"mb-3 text-muted\">\n <i class=\"bi bi-info-circle me-2\"></i>\n Select from official SDRF-Proteomics column names. Only columns without existing templates are shown.\n </p>\n\n @if (availableOfficialColumns.length === 0) {\n <div class=\"alert alert-warning\">\n <i class=\"bi bi-exclamation-triangle me-2\"></i>\n All official SDRF columns already have templates created.\n </div>\n } @else {\n <div class=\"row\">\n @for (column of availableOfficialColumns; track column.name) {\n <div class=\"col-md-6 mb-2\">\n <div class=\"card h-100 cursor-pointer border-info\"\n (click)=\"selectOfficialColumn(column)\"\n [class.border-primary]=\"selectedOfficialColumn?.name === column.name\">\n <div class=\"card-body py-2\">\n <h6 class=\"card-title mb-1 font-monospace\">\n {{ column.name }}\n </h6>\n <p class=\"card-text small mb-1\">\n <span class=\"badge\"\n [class]=\"column.type === 'characteristics' ? 'bg-primary' :\n column.type === 'comment' ? 'bg-success' :\n column.type === 'factor_value' ? 'bg-warning' : 'bg-secondary'\">\n {{ column.type }}\n </span>\n </p>\n <p class=\"card-text small mb-0\">{{ column.description }}</p>\n </div>\n </div>\n </div>\n }\n </div>\n }\n </div>\n </div>\n }\n\n <!-- Template Swapping for Edit Mode -->\n @if (isEdit && showTemplateSwap) {\n <div class=\"col-12 mb-3\">\n <div class=\"alert alert-info\">\n <h6 class=\"alert-heading\">\n <i class=\"bi bi-arrow-left-right me-2\"></i>Swap to Different Template\n </h6>\n <p class=\"mb-3\">\n Choose a different template to change the column name and settings. Your current value will be preserved.\n </p>\n\n <div class=\"mb-3\">\n <input\n type=\"text\"\n class=\"form-control mb-2\"\n placeholder=\"Search templates...\"\n [(ngModel)]=\"templateFilter\"\n [ngModelOptions]=\"{standalone: true}\"\n (input)=\"onTemplateFilterChange()\">\n\n <select\n class=\"form-select mb-3\"\n size=\"4\"\n [disabled]=\"isLoadingTemplates\">\n @if (isLoadingTemplates) {\n <option value=\"\" disabled>Loading templates...</option>\n } @else {\n @for (template of templates; track template.id) {\n <option\n [value]=\"template.id\"\n (click)=\"swapToTemplate(template)\"\n style=\"cursor: pointer;\">\n {{ template.columnName }} - {{ template.columnType }}@if (template.ontologyType) { ({{ getOntologyTypeLabel(template.ontologyType) }})}\n </option>\n }\n }\n </select>\n </div>\n\n <div class=\"d-flex justify-content-between\">\n <button\n type=\"button\"\n class=\"btn btn-success\"\n (click)=\"showCreateCustomTemplateOption()\">\n <i class=\"bi bi-plus-circle me-1\"></i>Create Custom Template\n </button>\n <button\n type=\"button\"\n class=\"btn btn-secondary\"\n (click)=\"cancelTemplateSwap()\">\n <i class=\"bi bi-x-circle me-1\"></i>Cancel\n </button>\n </div>\n </div>\n </div>\n }\n\n <!-- Custom Template Creation Option -->\n @if (isEdit && showCreateCustomTemplate) {\n <div class=\"col-12 mb-3\">\n <div class=\"alert alert-warning\">\n <h6 class=\"alert-heading\">\n <i class=\"bi bi-plus-circle me-2\"></i>Create Custom Template\n </h6>\n <p class=\"mb-3\">\n This will create a new custom template with your desired column name and settings.\n </p>\n\n <div class=\"d-flex justify-content-between\">\n <button\n type=\"button\"\n class=\"btn btn-warning\"\n (click)=\"createCustomTemplate()\">\n <i class=\"bi bi-gear me-1\"></i>Create & Apply Template\n </button>\n <button\n type=\"button\"\n class=\"btn btn-secondary\"\n (click)=\"cancelTemplateSwap()\">\n <i class=\"bi bi-x-circle me-1\"></i>Cancel\n </button>\n </div>\n </div>\n </div>\n }\n\n <!-- Template Selection -->\n <div class=\"col-12 mb-3\">\n <h6 class=\"text-muted border-bottom pb-2\">\n <i class=\"bi bi-file-earmark-text me-2\"></i>Template Selection\n <small class=\"text-muted ms-2\">\n @if (!isEdit) {\n (Step 2: Or select from existing templates)\n } @else {\n (Optional - select a template to auto-fill column settings)\n }\n </small>\n </h6>\n </div>\n\n <div class=\"col-12 mb-3\">\n <div class=\"input-group mb-3\">\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 placeholder=\"Search column templates...\"\n [(ngModel)]=\"templateFilter\"\n [ngModelOptions]=\"{standalone: true}\"\n (input)=\"onTemplateFilterChange()\">\n @if (templateFilter) {\n <button class=\"btn btn-outline-secondary\" type=\"button\" (click)=\"clearTemplateFilter()\">\n <i class=\"bi bi-x\"></i>\n </button>\n }\n </div>\n\n <select \n class=\"form-select mb-3\"\n size=\"5\"\n [(ngModel)]=\"selectedTemplateId\"\n [ngModelOptions]=\"{standalone: true}\"\n (change)=\"onTemplateSelectionChange()\"\n [disabled]=\"isLoadingTemplates\">\n <option value=\"\">No template selected</option>\n @if (isLoadingTemplates) {\n <option value=\"\" disabled>Loading templates...</option>\n } @else {\n @for (template of templates; track template.id) {\n <option [value]=\"template.id\">\n {{ template.columnName }} - {{ template.columnType }}@if (template.ontologyType) { ({{ getOntologyTypeLabel(template.ontologyType) }})}\n </option>\n }\n }\n </select>\n\n @if (totalTemplatePages > 1) {\n <div class=\"d-flex justify-content-between align-items-center\">\n <small class=\"text-muted\">\n Showing {{ (currentTemplatePage - 1) * templatePageSize + 1 }}-{{ Math.min(currentTemplatePage * templatePageSize, totalTemplates) }}\n of {{ totalTemplates }}\n </small>\n <ngb-pagination\n [collectionSize]=\"totalTemplates\"\n [page]=\"currentTemplatePage\"\n [pageSize]=\"templatePageSize\"\n [maxSize]=\"3\"\n [boundaryLinks]=\"true\"\n [rotate]=\"true\"\n (pageChange)=\"onTemplatePageChange($event)\"\n class=\"pagination-sm mb-0\">\n </ngb-pagination>\n </div>\n }\n\n @if (selectedTemplate) {\n <div class=\"alert alert-info mt-3\">\n <i class=\"bi bi-info-circle me-2\"></i>\n Template \"{{ selectedTemplate.name }}\" selected. Settings will be applied automatically.\n <button type=\"button\" class=\"btn btn-sm btn-outline-info float-end\" (click)=\"clearTemplate()\">\n Clear Selection\n </button>\n </div>\n }\n </div>\n\n <!-- Manual Entry -->\n <div class=\"col-12 mb-3\">\n <h6 class=\"text-muted border-bottom pb-2\">\n <i class=\"bi bi-pencil me-2\"></i>Column Details\n @if (selectedTemplate) {\n <small class=\"text-muted ms-2\">(Auto-filled from template - you can modify as needed)</small>\n }\n </h6>\n </div>\n \n <div class=\"col-12 mb-3\">\n <label for=\"columnName\" class=\"form-label\">\n Column Name *\n <span class=\"text-muted ms-2\">\n <i class=\"bi bi-info-circle\"></i>\n Column names are managed through templates\n </span>\n </label>\n <div class=\"input-group\">\n <input\n type=\"text\"\n class=\"form-control\"\n id=\"columnName\"\n formControlName=\"name\"\n [class.is-invalid]=\"editForm.get('name')?.invalid && editForm.get('name')?.touched\"\n readonly\n placeholder=\"Select from official columns or templates below\">\n @if (isEdit) {\n <button\n type=\"button\"\n class=\"btn btn-outline-secondary\"\n (click)=\"showTemplateSwapOptions()\"\n title=\"Change to different template\">\n <i class=\"bi bi-arrow-left-right\"></i> Swap Template\n </button>\n }\n </div>\n <div class=\"form-text\">\n <i class=\"bi bi-info-circle me-1\"></i>\n @if (isEdit) {\n To change the column name, swap to a different template or create a new custom template.\n } @else {\n Select from official SDRF columns or existing templates below to ensure compliance.\n }\n </div>\n </div>\n\n <div class=\"col-md-6 mb-3\">\n <label for=\"columnType\" class=\"form-label\">\n Column Type *\n <span class=\"text-muted ms-2\">\n <i class=\"bi bi-info-circle\"></i>\n Managed through templates\n </span>\n </label>\n <select\n class=\"form-select\"\n id=\"columnType\"\n formControlName=\"type\"\n [class.is-invalid]=\"editForm.get('type')?.invalid && editForm.get('type')?.touched\">\n @for (type of columnTypes; track type.value) {\n <option [value]=\"type.value\">{{ type.label }}</option>\n }\n </select>\n </div>\n\n <!-- Advanced Mode Toggle -->\n <div class=\"col-12 mb-3\">\n <div class=\"form-check form-switch\">\n <input\n class=\"form-check-input\"\n type=\"checkbox\"\n id=\"advancedMode\"\n [checked]=\"showAdvancedMode()\"\n (change)=\"toggleAdvancedMode()\">\n <label class=\"form-check-label\" for=\"advancedMode\">\n <i class=\"bi bi-gear me-1\"></i>Advanced Mode\n </label>\n <div class=\"form-text\">\n <i class=\"bi bi-info-circle me-1\"></i>\n Show advanced options for ontology configuration and column settings\n </div>\n </div>\n </div>\n\n @if (showAdvancedMode()) {\n <div class=\"col-md-6 mb-3\">\n <label for=\"ontologyType\" class=\"form-label\">\n Ontology Type\n <span class=\"badge bg-warning ms-2\">\n <i class=\"bi bi-exclamation-triangle me-1\"></i>Advanced\n </span>\n </label>\n <select\n class=\"form-select\"\n id=\"ontologyType\"\n formControlName=\"ontologyType\"\n (change)=\"onOntologyTypeChange($event)\">\n @for (ontology of ontologyTypes; track $index) {\n <option [value]=\"$index\">{{ ontology.label }}</option>\n }\n </select>\n\n @if (hasOntologyType) {\n <div class=\"form-text mt-2\">\n <i class=\"bi bi-info-circle me-1\"></i>\n Custom ontology filters will be applied based on the selected type\n </div>\n }\n </div>\n\n @if (hasOntologyType) {\n <div class=\"col-12 mb-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 <i class=\"bi bi-search me-1\"></i>Enable Typeahead Autocomplete\n </label>\n <div class=\"form-text\">\n <i class=\"bi bi-info-circle me-1\"></i>\n Enable autocomplete suggestions from {{ selectedOntologyConfig?.label || 'selected' }} ontology when editing values\n </div>\n </div>\n </div>\n }\n\n <!-- Column Options - Advanced Mode Only -->\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>Column Options\n <span class=\"badge bg-warning ms-2\">\n <i class=\"bi bi-exclamation-triangle me-1\"></i>Advanced\n </span>\n </h6>\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=\"mandatory\"\n formControlName=\"mandatory\">\n <label class=\"form-check-label\" for=\"mandatory\">\n <i class=\"bi bi-exclamation-circle me-1\"></i>Mandatory\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=\"hidden\"\n formControlName=\"hidden\">\n <label class=\"form-check-label\" for=\"hidden\">\n <i class=\"bi bi-eye-slash me-1\"></i>Hidden\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=\"readonly\"\n formControlName=\"readonly\">\n <label class=\"form-check-label\" for=\"readonly\">\n <i class=\"bi bi-lock me-1\"></i>Read Only\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=\"notApplicable\"\n formControlName=\"notApplicable\">\n <label class=\"form-check-label\" for=\"notApplicable\">\n <i class=\"bi bi-dash-circle me-1\"></i>N/A\n </label>\n </div>\n </div>\n </div>\n </div>\n }\n\n <div class=\"col-12 mb-3\">\n <label for=\"defaultValue\" class=\"form-label\">\n Default Value\n @if (hasTypeaheadEnabled) {\n <span class=\"badge bg-success ms-2\">\n <i class=\"bi bi-search me-1\"></i>Typeahead Enabled\n </span>\n }\n </label>\n \n <div class=\"input-group\">\n <input \n type=\"text\" \n class=\"form-control\"\n id=\"defaultValue\"\n formControlName=\"value\"\n placeholder=\"Enter default 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 (hasTypeaheadEnabled) {\n <i class=\"bi bi-info-circle me-1\"></i>\n Click \"Edit\" to use autocomplete suggestions from {{ selectedOntologyConfig?.label || 'selected' }} ontology\n } @else if (hasOntologyType) {\n <i class=\"bi bi-info-circle me-1\"></i>\n Click \"Edit\" to set default value. Enable typeahead in Advanced Mode for autocomplete suggestions.\n } @else if (showAdvancedMode()) {\n <i class=\"bi bi-info-circle me-1\"></i>\n Click \"Edit\" to set the default value (select an ontology type above for smart suggestions)\n } @else {\n <i class=\"bi bi-info-circle me-1\"></i>\n Click \"Edit\" to set the default value (enable Advanced Mode for ontology autocomplete)\n }\n </div>\n </div>\n\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' : 'Add' }} Column\n </button>\n</div>\n", styles: [".template-selection-container{border:1px solid var(--bs-border-color);border-radius:.375rem;padding:.5rem}.template-item{cursor:pointer;transition:all .2s ease}.template-item:hover{background-color:rgba(var(--bs-primary-rgb),.1);border-color:var(--bs-primary)!important}.template-item.selected{background-color:rgba(var(--bs-primary-rgb),.15);border-color:var(--bs-primary)!important;box-shadow:0 0 0 .2rem rgba(var(--bs-primary-rgb),.25)}.cursor-pointer{cursor:pointer}.card.cursor-pointer{transition:all .2s ease}.card.cursor-pointer:hover{transform:translateY(-2px);box-shadow:0 4px 8px #0000001a}.card.cursor-pointer.border-info{border-width:2px}.modal-lg{max-width:900px}\n"] }]
5131
+ args: [{ selector: 'ccv-column-edit-modal', imports: [CommonModule, ReactiveFormsModule, FormsModule, NgbModule], template: "<div class=\"modal-header\">\n <h4 class=\"modal-title\">\n <i class=\"bi bi-columns-gap 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 <!-- Selection Required Notice for New Columns -->\n @if (!isEdit) {\n <div class=\"col-12 mb-3\">\n <div class=\"alert alert-warning\">\n <h6 class=\"alert-heading\">\n <i class=\"bi bi-exclamation-triangle me-2\"></i>Column Selection Required\n </h6>\n <p class=\"mb-0\">\n You must select from official SDRF columns or existing templates to ensure data compliance and consistency.\n </p>\n </div>\n </div>\n }\n\n <!-- Official SDRF Columns -->\n <div class=\"col-12 mb-3\">\n <h6 class=\"text-muted border-bottom pb-2\">\n <i class=\"bi bi-file-earmark-check me-2\"></i>Official SDRF Columns\n <small class=\"text-muted ms-2\">\n @if (!isEdit) {\n (Step 1: Select from standardized SDRF-Proteomics column names)\n } @else {\n (Recommended - select from standardized SDRF-Proteomics column names)\n }\n </small>\n </h6>\n </div>\n\n @if (!isEdit) {\n <div class=\"col-12 mb-3\">\n @if (!showOfficialColumns) {\n <button\n type=\"button\"\n class=\"btn btn-primary mb-3\"\n (click)=\"toggleOfficialColumns()\">\n <i class=\"bi bi-list-ul me-2\"></i>\n Select from Official SDRF Columns\n </button>\n } @else {\n <button\n type=\"button\"\n class=\"btn btn-outline-secondary mb-3\"\n (click)=\"toggleOfficialColumns()\">\n <i class=\"bi bi-x me-2\"></i>\n Hide Official Columns\n </button>\n }\n </div>\n } @else {\n <div class=\"col-12 mb-3\">\n <button\n type=\"button\"\n class=\"btn btn-outline-info mb-3\"\n (click)=\"toggleOfficialColumns()\">\n <i class=\"bi bi-list-ul me-2\"></i>\n {{ showOfficialColumns ? 'Hide' : 'Show' }} Official Column Names\n </button>\n </div>\n }\n\n @if (showOfficialColumns) {\n <div class=\"col-12 mb-3\">\n <div class=\"border rounded p-3\">\n <p class=\"mb-3 text-muted\">\n <i class=\"bi bi-info-circle me-2\"></i>\n Select from official SDRF-Proteomics column names. Only columns without existing templates are shown.\n </p>\n\n @if (availableOfficialColumns.length === 0) {\n <div class=\"alert alert-warning\">\n <i class=\"bi bi-exclamation-triangle me-2\"></i>\n All official SDRF columns already have templates created.\n </div>\n } @else {\n <div class=\"row\">\n @for (column of availableOfficialColumns; track column.name) {\n <div class=\"col-md-6 mb-2\">\n <div class=\"card h-100 cursor-pointer border-info\"\n (click)=\"selectOfficialColumn(column)\"\n [class.border-primary]=\"selectedOfficialColumn?.name === column.name\">\n <div class=\"card-body py-2\">\n <h6 class=\"card-title mb-1 font-monospace\">\n {{ column.name }}\n </h6>\n <p class=\"card-text small mb-1\">\n <span class=\"badge\"\n [class]=\"column.type === 'characteristics' ? 'bg-primary' :\n column.type === 'comment' ? 'bg-success' :\n column.type === 'factor_value' ? 'bg-warning' : 'bg-secondary'\">\n {{ column.type }}\n </span>\n </p>\n <p class=\"card-text small mb-0\">{{ column.description }}</p>\n </div>\n </div>\n </div>\n }\n </div>\n }\n </div>\n </div>\n }\n\n <!-- Template Swapping for Edit Mode -->\n @if (isEdit && showTemplateSwap) {\n <div class=\"col-12 mb-3\">\n <div class=\"alert alert-info\">\n <h6 class=\"alert-heading\">\n <i class=\"bi bi-arrow-left-right me-2\"></i>Swap to Different Template\n </h6>\n <p class=\"mb-3\">\n Choose a different template to change the column name and settings. Your current value will be preserved.\n </p>\n\n <div class=\"mb-3\">\n <input\n type=\"text\"\n class=\"form-control mb-2\"\n placeholder=\"Search templates...\"\n [(ngModel)]=\"templateFilter\"\n [ngModelOptions]=\"{standalone: true}\"\n (input)=\"onTemplateFilterChange()\">\n\n <select\n class=\"form-select mb-3\"\n size=\"4\"\n [disabled]=\"isLoadingTemplates\">\n @if (isLoadingTemplates) {\n <option value=\"\" disabled>Loading templates...</option>\n } @else {\n @for (template of templates; track template.id) {\n <option\n [value]=\"template.id\"\n (click)=\"swapToTemplate(template)\"\n style=\"cursor: pointer;\">\n {{ template.columnName }} - {{ template.columnType }}@if (template.ontologyType) { ({{ getOntologyTypeLabel(template.ontologyType) }})}\n </option>\n }\n }\n </select>\n </div>\n\n <div class=\"d-flex justify-content-between\">\n <button\n type=\"button\"\n class=\"btn btn-success\"\n (click)=\"showCreateCustomTemplateOption()\">\n <i class=\"bi bi-plus-circle me-1\"></i>Create Custom Template\n </button>\n <button\n type=\"button\"\n class=\"btn btn-secondary\"\n (click)=\"cancelTemplateSwap()\">\n <i class=\"bi bi-x-circle me-1\"></i>Cancel\n </button>\n </div>\n </div>\n </div>\n }\n\n <!-- Custom Template Creation Option -->\n @if (isEdit && showCreateCustomTemplate) {\n <div class=\"col-12 mb-3\">\n <div class=\"alert alert-warning\">\n <h6 class=\"alert-heading\">\n <i class=\"bi bi-plus-circle me-2\"></i>Create Custom Template\n </h6>\n <p class=\"mb-3\">\n This will create a new custom template with your desired column name and settings.\n </p>\n\n <div class=\"d-flex justify-content-between\">\n <button\n type=\"button\"\n class=\"btn btn-warning\"\n (click)=\"createCustomTemplate()\">\n <i class=\"bi bi-gear me-1\"></i>Create & Apply Template\n </button>\n <button\n type=\"button\"\n class=\"btn btn-secondary\"\n (click)=\"cancelTemplateSwap()\">\n <i class=\"bi bi-x-circle me-1\"></i>Cancel\n </button>\n </div>\n </div>\n </div>\n }\n\n <!-- Template Selection -->\n <div class=\"col-12 mb-3\">\n <h6 class=\"text-muted border-bottom pb-2\">\n <i class=\"bi bi-file-earmark-text me-2\"></i>Template Selection\n <small class=\"text-muted ms-2\">\n @if (!isEdit) {\n (Step 2: Or select from existing templates)\n } @else {\n (Optional - select a template to auto-fill column settings)\n }\n </small>\n </h6>\n </div>\n\n <div class=\"col-12 mb-3\">\n <div class=\"input-group mb-3\">\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 placeholder=\"Search column templates...\"\n [(ngModel)]=\"templateFilter\"\n [ngModelOptions]=\"{standalone: true}\"\n (input)=\"onTemplateFilterChange()\">\n @if (templateFilter) {\n <button class=\"btn btn-outline-secondary\" type=\"button\" (click)=\"clearTemplateFilter()\">\n <i class=\"bi bi-x\"></i>\n </button>\n }\n </div>\n\n <select \n class=\"form-select mb-3\"\n size=\"5\"\n [(ngModel)]=\"selectedTemplateId\"\n [ngModelOptions]=\"{standalone: true}\"\n (change)=\"onTemplateSelectionChange()\"\n [disabled]=\"isLoadingTemplates\">\n <option value=\"\">No template selected</option>\n @if (isLoadingTemplates) {\n <option value=\"\" disabled>Loading templates...</option>\n } @else {\n @for (template of templates; track template.id) {\n <option [value]=\"template.id\">\n {{ template.columnName }} - {{ template.columnType }}@if (template.ontologyType) { ({{ getOntologyTypeLabel(template.ontologyType) }})}\n </option>\n }\n }\n </select>\n\n @if (totalTemplatePages > 1) {\n <div class=\"d-flex justify-content-between align-items-center\">\n <small class=\"text-muted\">\n Showing {{ (currentTemplatePage - 1) * templatePageSize + 1 }}-{{ Math.min(currentTemplatePage * templatePageSize, totalTemplates) }}\n of {{ totalTemplates }}\n </small>\n <ngb-pagination\n [collectionSize]=\"totalTemplates\"\n [page]=\"currentTemplatePage\"\n [pageSize]=\"templatePageSize\"\n [maxSize]=\"3\"\n [boundaryLinks]=\"true\"\n [rotate]=\"true\"\n (pageChange)=\"onTemplatePageChange($event)\"\n class=\"pagination-sm mb-0\">\n </ngb-pagination>\n </div>\n }\n\n @if (selectedTemplate) {\n <div class=\"alert alert-info mt-3\">\n <i class=\"bi bi-info-circle me-2\"></i>\n Template \"{{ selectedTemplate.name }}\" selected. Settings will be applied automatically.\n <button type=\"button\" class=\"btn btn-sm btn-outline-info float-end\" (click)=\"clearTemplate()\">\n Clear Selection\n </button>\n </div>\n }\n </div>\n\n <!-- Manual Entry -->\n <div class=\"col-12 mb-3\">\n <h6 class=\"text-muted border-bottom pb-2\">\n <i class=\"bi bi-pencil me-2\"></i>Column Details\n @if (selectedTemplate) {\n <small class=\"text-muted ms-2\">(Auto-filled from template - you can modify as needed)</small>\n }\n </h6>\n </div>\n \n <div class=\"col-12 mb-3\">\n <label for=\"columnName\" class=\"form-label\">\n Column Name *\n <span class=\"text-muted ms-2\">\n <i class=\"bi bi-info-circle\"></i>\n Column names are managed through templates\n </span>\n </label>\n <div class=\"input-group\">\n <input\n type=\"text\"\n class=\"form-control\"\n id=\"columnName\"\n formControlName=\"name\"\n [class.is-invalid]=\"editForm.get('name')?.invalid && editForm.get('name')?.touched\"\n readonly\n placeholder=\"Select from official columns or templates below\">\n @if (isEdit) {\n <button\n type=\"button\"\n class=\"btn btn-outline-secondary\"\n (click)=\"showTemplateSwapOptions()\"\n title=\"Change to different template\">\n <i class=\"bi bi-arrow-left-right\"></i> Swap Template\n </button>\n }\n </div>\n <div class=\"form-text\">\n <i class=\"bi bi-info-circle me-1\"></i>\n @if (isEdit) {\n To change the column name, swap to a different template or create a new custom template.\n } @else {\n Select from official SDRF columns or existing templates below to ensure compliance.\n }\n </div>\n </div>\n\n <div class=\"col-md-6 mb-3\">\n <label for=\"columnType\" class=\"form-label\">\n Column Type *\n <span class=\"text-muted ms-2\">\n <i class=\"bi bi-info-circle\"></i>\n Managed through templates\n </span>\n </label>\n <select\n class=\"form-select\"\n id=\"columnType\"\n formControlName=\"type\"\n [class.is-invalid]=\"editForm.get('type')?.invalid && editForm.get('type')?.touched\">\n @for (type of columnTypes; track type.value) {\n <option [value]=\"type.value\">{{ type.label }}</option>\n }\n </select>\n </div>\n\n <!-- Advanced Mode Toggle -->\n <div class=\"col-12 mb-3\">\n <div class=\"form-check form-switch\">\n <input\n class=\"form-check-input\"\n type=\"checkbox\"\n id=\"advancedMode\"\n [checked]=\"showAdvancedMode()\"\n (change)=\"toggleAdvancedMode()\">\n <label class=\"form-check-label\" for=\"advancedMode\">\n <i class=\"bi bi-gear me-1\"></i>Advanced Mode\n </label>\n <div class=\"form-text\">\n <i class=\"bi bi-info-circle me-1\"></i>\n Show advanced options for ontology configuration and column settings\n </div>\n </div>\n </div>\n\n @if (showAdvancedMode()) {\n <div class=\"col-md-6 mb-3\">\n <label for=\"ontologyType\" class=\"form-label\">\n Ontology Type\n <span class=\"badge bg-warning ms-2\">\n <i class=\"bi bi-exclamation-triangle me-1\"></i>Advanced\n </span>\n </label>\n <select\n class=\"form-select\"\n id=\"ontologyType\"\n formControlName=\"ontologyType\"\n (change)=\"onOntologyTypeChange($event)\">\n @for (ontology of ontologyTypes; track $index) {\n <option [value]=\"$index\">{{ ontology.label }}</option>\n }\n </select>\n\n @if (hasOntologyType) {\n <div class=\"form-text mt-2\">\n <i class=\"bi bi-info-circle me-1\"></i>\n Custom ontology filters will be applied based on the selected type\n </div>\n }\n </div>\n\n @if (hasOntologyType) {\n <div class=\"col-12 mb-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 <i class=\"bi bi-search me-1\"></i>Enable Typeahead Autocomplete\n </label>\n <div class=\"form-text\">\n <i class=\"bi bi-info-circle me-1\"></i>\n Enable autocomplete suggestions from {{ selectedOntologyConfig?.label || 'selected' }} ontology when editing values\n </div>\n </div>\n </div>\n }\n\n <!-- Column Options - Advanced Mode Only -->\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>Column Options\n <span class=\"badge bg-warning ms-2\">\n <i class=\"bi bi-exclamation-triangle me-1\"></i>Advanced\n </span>\n </h6>\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=\"mandatory\"\n formControlName=\"mandatory\">\n <label class=\"form-check-label\" for=\"mandatory\">\n <i class=\"bi bi-exclamation-circle me-1\"></i>Mandatory\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=\"hidden\"\n formControlName=\"hidden\">\n <label class=\"form-check-label\" for=\"hidden\">\n <i class=\"bi bi-eye-slash me-1\"></i>Hidden\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=\"readonly\"\n formControlName=\"readonly\">\n <label class=\"form-check-label\" for=\"readonly\">\n <i class=\"bi bi-lock me-1\"></i>Read Only\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=\"notApplicable\"\n formControlName=\"notApplicable\">\n <label class=\"form-check-label\" for=\"notApplicable\">\n <i class=\"bi bi-dash-circle me-1\"></i>Not Applicable\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=\"notAvailable\"\n formControlName=\"notAvailable\">\n <label class=\"form-check-label\" for=\"notAvailable\">\n <i class=\"bi bi-question-circle me-1\"></i>Not Available\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=\"staffOnly\"\n formControlName=\"staffOnly\">\n <label class=\"form-check-label\" for=\"staffOnly\">\n <i class=\"bi bi-shield-lock me-1\"></i>Staff Only\n </label>\n </div>\n </div>\n </div>\n <div class=\"row mt-2\">\n <div class=\"col-12\">\n <div class=\"alert alert-info py-2\">\n <i class=\"bi bi-info-circle me-1\"></i>\n <small>\n <strong>Staff Only:</strong> When enabled, this column will only be visible and editable by staff members.\n </small>\n </div>\n </div>\n </div>\n </div>\n }\n\n <div class=\"col-12 mb-3\">\n <label for=\"defaultValue\" class=\"form-label\">\n Default Value\n @if (hasTypeaheadEnabled) {\n <span class=\"badge bg-success ms-2\">\n <i class=\"bi bi-search me-1\"></i>Typeahead Enabled\n </span>\n }\n </label>\n\n @if (editForm.get('notApplicable')?.value || editForm.get('notAvailable')?.value) {\n <div class=\"mb-2\">\n <small class=\"text-muted d-block mb-1\">Quick Select:</small>\n <div class=\"btn-group btn-group-sm\" role=\"group\">\n @if (editForm.get('notApplicable')?.value) {\n <button\n type=\"button\"\n class=\"btn btn-outline-secondary\"\n (click)=\"setDefaultValue('not applicable')\">\n <i class=\"bi bi-dash-circle me-1\"></i>Not Applicable\n </button>\n }\n @if (editForm.get('notAvailable')?.value) {\n <button\n type=\"button\"\n class=\"btn btn-outline-secondary\"\n (click)=\"setDefaultValue('not available')\">\n <i class=\"bi bi-question-circle me-1\"></i>Not Available\n </button>\n }\n </div>\n </div>\n }\n\n <div class=\"input-group\">\n <input \n type=\"text\" \n class=\"form-control\"\n id=\"defaultValue\"\n formControlName=\"value\"\n placeholder=\"Enter default 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 (hasTypeaheadEnabled) {\n <i class=\"bi bi-info-circle me-1\"></i>\n Click \"Edit\" to use autocomplete suggestions from {{ selectedOntologyConfig?.label || 'selected' }} ontology\n } @else if (hasOntologyType) {\n <i class=\"bi bi-info-circle me-1\"></i>\n Click \"Edit\" to set default value. Enable typeahead in Advanced Mode for autocomplete suggestions.\n } @else if (showAdvancedMode()) {\n <i class=\"bi bi-info-circle me-1\"></i>\n Click \"Edit\" to set the default value (select an ontology type above for smart suggestions)\n } @else {\n <i class=\"bi bi-info-circle me-1\"></i>\n Click \"Edit\" to set the default value (enable Advanced Mode for ontology autocomplete)\n }\n </div>\n </div>\n\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' : 'Add' }} Column\n </button>\n</div>\n", styles: [".template-selection-container{border:1px solid var(--bs-border-color);border-radius:.375rem;padding:.5rem}.template-item{cursor:pointer;transition:all .2s ease}.template-item:hover{background-color:rgba(var(--bs-primary-rgb),.1);border-color:var(--bs-primary)!important}.template-item.selected{background-color:rgba(var(--bs-primary-rgb),.15);border-color:var(--bs-primary)!important;box-shadow:0 0 0 .2rem rgba(var(--bs-primary-rgb),.25)}.cursor-pointer{cursor:pointer}.card.cursor-pointer{transition:all .2s ease}.card.cursor-pointer:hover{transform:translateY(-2px);box-shadow:0 4px 8px #0000001a}.card.cursor-pointer.border-info{border-width:2px}.modal-lg{max-width:900px}\n"] }]
5102
5132
  }], ctorParameters: () => [{ type: i1.FormBuilder }, { type: i2.NgbActiveModal }, { type: i2.NgbModal }, { type: MetadataColumnTemplateService }, { type: SdrfSyntaxService }], propDecorators: { column: [{
5103
5133
  type: Input
5104
5134
  }], templateId: [{
@@ -6201,6 +6231,8 @@ class ColumnTemplateEditModal {
6201
6231
  visibility: ['private', Validators.required],
6202
6232
  labGroup: [null],
6203
6233
  enableTypeahead: [false],
6234
+ notApplicable: [false],
6235
+ notAvailable: [false],
6204
6236
  excelValidation: [false],
6205
6237
  isActive: [true],
6206
6238
  tags: [''],
@@ -6232,6 +6264,8 @@ class ColumnTemplateEditModal {
6232
6264
  visibility: this.template.visibility || 'private',
6233
6265
  labGroup: this.template.labGroup || null,
6234
6266
  enableTypeahead: this.template.enableTypeahead || false,
6267
+ notApplicable: this.template.notApplicable || false,
6268
+ notAvailable: this.template.notAvailable || false,
6235
6269
  excelValidation: this.template.excelValidation || false,
6236
6270
  isActive: this.template.isActive !== false,
6237
6271
  tags: this.template.tags || '',
@@ -6290,6 +6324,10 @@ class ColumnTemplateEditModal {
6290
6324
  control?.markAsTouched();
6291
6325
  });
6292
6326
  }
6327
+ setDefaultValue(value) {
6328
+ this.editForm.get('defaultValue')?.setValue(value);
6329
+ this.editForm.get('defaultValue')?.markAsTouched();
6330
+ }
6293
6331
  onCancel() {
6294
6332
  this.activeModal.dismiss();
6295
6333
  }
@@ -6315,11 +6353,11 @@ class ColumnTemplateEditModal {
6315
6353
  return this.isEdit ? 'Edit Column Template' : 'Create Column Template';
6316
6354
  }
6317
6355
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.7", 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.7", 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" }] });
6356
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.7", 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 @if (editForm.get('notApplicable')?.value || editForm.get('notAvailable')?.value) {\n <div class=\"mb-2\">\n <div class=\"btn-group btn-group-sm\" role=\"group\">\n @if (editForm.get('notApplicable')?.value) {\n <button\n type=\"button\"\n class=\"btn btn-outline-secondary\"\n (click)=\"setDefaultValue('not applicable')\">\n <i class=\"bi bi-dash-circle me-1\"></i>Not Applicable\n </button>\n }\n @if (editForm.get('notAvailable')?.value) {\n <button\n type=\"button\"\n class=\"btn btn-outline-secondary\"\n (click)=\"setDefaultValue('not available')\">\n <i class=\"bi bi-question-circle me-1\"></i>Not Available\n </button>\n }\n </div>\n </div>\n }\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=\"notApplicable\"\n formControlName=\"notApplicable\">\n <label class=\"form-check-label\" for=\"notApplicable\">\n <i class=\"bi bi-dash-circle me-1\"></i>Not Applicable\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=\"notAvailable\"\n formControlName=\"notAvailable\">\n <label class=\"form-check-label\" for=\"notAvailable\">\n <i class=\"bi bi-question-circle me-1\"></i>Not Available\n </label>\n </div>\n </div>\n </div>\n </div>\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=\"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
6357
  }
6320
6358
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.7", ngImport: i0, type: ColumnTemplateEditModal, decorators: [{
6321
6359
  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" }]
6360
+ 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 @if (editForm.get('notApplicable')?.value || editForm.get('notAvailable')?.value) {\n <div class=\"mb-2\">\n <div class=\"btn-group btn-group-sm\" role=\"group\">\n @if (editForm.get('notApplicable')?.value) {\n <button\n type=\"button\"\n class=\"btn btn-outline-secondary\"\n (click)=\"setDefaultValue('not applicable')\">\n <i class=\"bi bi-dash-circle me-1\"></i>Not Applicable\n </button>\n }\n @if (editForm.get('notAvailable')?.value) {\n <button\n type=\"button\"\n class=\"btn btn-outline-secondary\"\n (click)=\"setDefaultValue('not available')\">\n <i class=\"bi bi-question-circle me-1\"></i>Not Available\n </button>\n }\n </div>\n </div>\n }\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=\"notApplicable\"\n formControlName=\"notApplicable\">\n <label class=\"form-check-label\" for=\"notApplicable\">\n <i class=\"bi bi-dash-circle me-1\"></i>Not Applicable\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=\"notAvailable\"\n formControlName=\"notAvailable\">\n <label class=\"form-check-label\" for=\"notAvailable\">\n <i class=\"bi bi-question-circle me-1\"></i>Not Available\n </label>\n </div>\n </div>\n </div>\n </div>\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=\"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
6361
  }], ctorParameters: () => [{ type: i1.FormBuilder }, { type: i2.NgbActiveModal }], propDecorators: { template: [{
6324
6362
  type: Input
6325
6363
  }], labGroups: [{
@@ -7405,6 +7443,1699 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.7", ngImpor
7405
7443
  ], 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
7444
  }], ctorParameters: () => [{ type: i1.FormBuilder }, { type: FavouriteMetadataOptionService }, { type: MetadataColumnTemplateService }, { type: i3.LabGroupService }, { type: i3.ToastService }, { type: i2.NgbModal }, { type: i3.AuthService }] });
7407
7445
 
7446
+ class SearchReplaceModal {
7447
+ fb;
7448
+ activeModal;
7449
+ config;
7450
+ searchReplaceForm;
7451
+ isProcessing = signal(false, ...(ngDevMode ? [{ debugName: "isProcessing" }] : []));
7452
+ result = signal(null, ...(ngDevMode ? [{ debugName: "result" }] : []));
7453
+ constructor(fb, activeModal) {
7454
+ this.fb = fb;
7455
+ this.activeModal = activeModal;
7456
+ this.searchReplaceForm = this.fb.group({
7457
+ oldValue: ['', [Validators.required, Validators.minLength(1)]],
7458
+ newValue: ['', [Validators.required]],
7459
+ columnId: [null],
7460
+ columnName: [''],
7461
+ updatePools: [false]
7462
+ });
7463
+ }
7464
+ get isTableContext() {
7465
+ return this.config?.context === 'table';
7466
+ }
7467
+ get isColumnContext() {
7468
+ return this.config?.context === 'column';
7469
+ }
7470
+ onSubmit() {
7471
+ if (this.searchReplaceForm.valid) {
7472
+ this.activeModal.close({
7473
+ action: 'replace',
7474
+ data: this.searchReplaceForm.value
7475
+ });
7476
+ }
7477
+ else {
7478
+ this.markFormGroupTouched();
7479
+ }
7480
+ }
7481
+ markFormGroupTouched() {
7482
+ Object.keys(this.searchReplaceForm.controls).forEach(key => {
7483
+ const control = this.searchReplaceForm.get(key);
7484
+ control?.markAsTouched();
7485
+ });
7486
+ }
7487
+ onCancel() {
7488
+ this.activeModal.dismiss();
7489
+ }
7490
+ getFieldError(fieldName) {
7491
+ const control = this.searchReplaceForm.get(fieldName);
7492
+ if (control?.errors && control.touched) {
7493
+ if (control.errors['required'])
7494
+ return `${fieldName} is required`;
7495
+ if (control.errors['minlength'])
7496
+ return `${fieldName} must be at least ${control.errors['minlength'].requiredLength} characters`;
7497
+ }
7498
+ return '';
7499
+ }
7500
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.7", ngImport: i0, type: SearchReplaceModal, deps: [{ token: i1.FormBuilder }, { token: i2.NgbActiveModal }], target: i0.ɵɵFactoryTarget.Component });
7501
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.7", type: SearchReplaceModal, isStandalone: true, selector: "ccv-search-replace-modal", inputs: { config: "config" }, ngImport: i0, template: "<div class=\"modal-header\">\r\n <h4 class=\"modal-title\">\r\n <i class=\"bi bi-search me-2\"></i>Search and Replace Values\r\n </h4>\r\n <button type=\"button\" class=\"btn-close\" (click)=\"onCancel()\"></button>\r\n</div>\r\n\r\n<div class=\"modal-body\">\r\n <form [formGroup]=\"searchReplaceForm\" (ngSubmit)=\"onSubmit()\">\r\n <div class=\"row\">\r\n <div class=\"col-12 mb-3\">\r\n <div class=\"alert alert-info\">\r\n <i class=\"bi bi-info-circle me-2\"></i>\r\n @if (isTableContext) {\r\n <strong>Table Context:</strong> Search and replace values across {{ config.tableName }}\r\n } @else {\r\n <strong>Column Context:</strong> Search and replace values in {{ config.columnName }}\r\n }\r\n </div>\r\n </div>\r\n\r\n @if (isTableContext && config.columns && config.columns.length > 0) {\r\n <div class=\"col-12 mb-3\">\r\n <label for=\"columnSelect\" class=\"form-label\">\r\n Target Column <small class=\"text-muted\">(optional - leave empty to search all columns)</small>\r\n </label>\r\n <select\r\n class=\"form-select\"\r\n id=\"columnSelect\"\r\n formControlName=\"columnId\">\r\n <option [ngValue]=\"null\">All Columns</option>\r\n @for (column of config.columns; track column.id) {\r\n <option [ngValue]=\"column.id\">{{ column.name }}</option>\r\n }\r\n </select>\r\n <div class=\"form-text\">\r\n <i class=\"bi bi-info-circle me-1\"></i>\r\n Select a specific column to limit the search, or leave as \"All Columns\" to search everywhere.\r\n </div>\r\n </div>\r\n }\r\n\r\n <div class=\"col-12 mb-3\">\r\n <label for=\"oldValue\" class=\"form-label\">Find Value *</label>\r\n <input\r\n type=\"text\"\r\n class=\"form-control\"\r\n id=\"oldValue\"\r\n formControlName=\"oldValue\"\r\n [class.is-invalid]=\"searchReplaceForm.get('oldValue')?.invalid && searchReplaceForm.get('oldValue')?.touched\"\r\n placeholder=\"Enter value to find\">\r\n <div class=\"invalid-feedback\">\r\n {{ getFieldError('oldValue') }}\r\n </div>\r\n <div class=\"form-text\">\r\n <i class=\"bi bi-info-circle me-1\"></i>\r\n Exact match - this will find all cells with exactly this value.\r\n </div>\r\n </div>\r\n\r\n <div class=\"col-12 mb-3\">\r\n <label for=\"newValue\" class=\"form-label\">Replace With *</label>\r\n <input\r\n type=\"text\"\r\n class=\"form-control\"\r\n id=\"newValue\"\r\n formControlName=\"newValue\"\r\n [class.is-invalid]=\"searchReplaceForm.get('newValue')?.invalid && searchReplaceForm.get('newValue')?.touched\"\r\n placeholder=\"Enter replacement value\">\r\n <div class=\"invalid-feedback\">\r\n {{ getFieldError('newValue') }}\r\n </div>\r\n <div class=\"form-text\">\r\n <i class=\"bi bi-info-circle me-1\"></i>\r\n All matching values will be replaced with this value.\r\n </div>\r\n </div>\r\n\r\n <div class=\"col-12 mb-3\">\r\n <div class=\"form-check form-switch\">\r\n <input\r\n class=\"form-check-input\"\r\n type=\"checkbox\"\r\n id=\"updatePools\"\r\n formControlName=\"updatePools\">\r\n <label class=\"form-check-label\" for=\"updatePools\">\r\n Update Sample Pools\r\n </label>\r\n </div>\r\n <div class=\"form-text\">\r\n <i class=\"bi bi-info-circle me-1\"></i>\r\n When enabled, this will also update values in related sample pools.\r\n </div>\r\n </div>\r\n\r\n <div class=\"col-12\">\r\n <div class=\"alert alert-warning\">\r\n <i class=\"bi bi-exclamation-triangle me-2\"></i>\r\n <strong>Warning:</strong> This operation cannot be undone. All matching values will be permanently replaced.\r\n </div>\r\n </div>\r\n </div>\r\n </form>\r\n</div>\r\n\r\n<div class=\"modal-footer\">\r\n <button type=\"button\" class=\"btn btn-outline-secondary\" (click)=\"onCancel()\">\r\n <i class=\"bi bi-x-circle me-1\"></i>Cancel\r\n </button>\r\n <button\r\n type=\"button\"\r\n class=\"btn btn-primary\"\r\n (click)=\"onSubmit()\"\r\n [disabled]=\"searchReplaceForm.invalid || isProcessing()\">\r\n @if (isProcessing()) {\r\n <span class=\"spinner-border spinner-border-sm me-2\"></span>\r\n }\r\n <i class=\"bi bi-arrow-repeat me-1\"></i>\r\n Replace All\r\n </button>\r\n</div>\r\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.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"] }] });
7502
+ }
7503
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.7", ngImport: i0, type: SearchReplaceModal, decorators: [{
7504
+ type: Component,
7505
+ args: [{ selector: 'ccv-search-replace-modal', standalone: true, imports: [CommonModule, ReactiveFormsModule], template: "<div class=\"modal-header\">\r\n <h4 class=\"modal-title\">\r\n <i class=\"bi bi-search me-2\"></i>Search and Replace Values\r\n </h4>\r\n <button type=\"button\" class=\"btn-close\" (click)=\"onCancel()\"></button>\r\n</div>\r\n\r\n<div class=\"modal-body\">\r\n <form [formGroup]=\"searchReplaceForm\" (ngSubmit)=\"onSubmit()\">\r\n <div class=\"row\">\r\n <div class=\"col-12 mb-3\">\r\n <div class=\"alert alert-info\">\r\n <i class=\"bi bi-info-circle me-2\"></i>\r\n @if (isTableContext) {\r\n <strong>Table Context:</strong> Search and replace values across {{ config.tableName }}\r\n } @else {\r\n <strong>Column Context:</strong> Search and replace values in {{ config.columnName }}\r\n }\r\n </div>\r\n </div>\r\n\r\n @if (isTableContext && config.columns && config.columns.length > 0) {\r\n <div class=\"col-12 mb-3\">\r\n <label for=\"columnSelect\" class=\"form-label\">\r\n Target Column <small class=\"text-muted\">(optional - leave empty to search all columns)</small>\r\n </label>\r\n <select\r\n class=\"form-select\"\r\n id=\"columnSelect\"\r\n formControlName=\"columnId\">\r\n <option [ngValue]=\"null\">All Columns</option>\r\n @for (column of config.columns; track column.id) {\r\n <option [ngValue]=\"column.id\">{{ column.name }}</option>\r\n }\r\n </select>\r\n <div class=\"form-text\">\r\n <i class=\"bi bi-info-circle me-1\"></i>\r\n Select a specific column to limit the search, or leave as \"All Columns\" to search everywhere.\r\n </div>\r\n </div>\r\n }\r\n\r\n <div class=\"col-12 mb-3\">\r\n <label for=\"oldValue\" class=\"form-label\">Find Value *</label>\r\n <input\r\n type=\"text\"\r\n class=\"form-control\"\r\n id=\"oldValue\"\r\n formControlName=\"oldValue\"\r\n [class.is-invalid]=\"searchReplaceForm.get('oldValue')?.invalid && searchReplaceForm.get('oldValue')?.touched\"\r\n placeholder=\"Enter value to find\">\r\n <div class=\"invalid-feedback\">\r\n {{ getFieldError('oldValue') }}\r\n </div>\r\n <div class=\"form-text\">\r\n <i class=\"bi bi-info-circle me-1\"></i>\r\n Exact match - this will find all cells with exactly this value.\r\n </div>\r\n </div>\r\n\r\n <div class=\"col-12 mb-3\">\r\n <label for=\"newValue\" class=\"form-label\">Replace With *</label>\r\n <input\r\n type=\"text\"\r\n class=\"form-control\"\r\n id=\"newValue\"\r\n formControlName=\"newValue\"\r\n [class.is-invalid]=\"searchReplaceForm.get('newValue')?.invalid && searchReplaceForm.get('newValue')?.touched\"\r\n placeholder=\"Enter replacement value\">\r\n <div class=\"invalid-feedback\">\r\n {{ getFieldError('newValue') }}\r\n </div>\r\n <div class=\"form-text\">\r\n <i class=\"bi bi-info-circle me-1\"></i>\r\n All matching values will be replaced with this value.\r\n </div>\r\n </div>\r\n\r\n <div class=\"col-12 mb-3\">\r\n <div class=\"form-check form-switch\">\r\n <input\r\n class=\"form-check-input\"\r\n type=\"checkbox\"\r\n id=\"updatePools\"\r\n formControlName=\"updatePools\">\r\n <label class=\"form-check-label\" for=\"updatePools\">\r\n Update Sample Pools\r\n </label>\r\n </div>\r\n <div class=\"form-text\">\r\n <i class=\"bi bi-info-circle me-1\"></i>\r\n When enabled, this will also update values in related sample pools.\r\n </div>\r\n </div>\r\n\r\n <div class=\"col-12\">\r\n <div class=\"alert alert-warning\">\r\n <i class=\"bi bi-exclamation-triangle me-2\"></i>\r\n <strong>Warning:</strong> This operation cannot be undone. All matching values will be permanently replaced.\r\n </div>\r\n </div>\r\n </div>\r\n </form>\r\n</div>\r\n\r\n<div class=\"modal-footer\">\r\n <button type=\"button\" class=\"btn btn-outline-secondary\" (click)=\"onCancel()\">\r\n <i class=\"bi bi-x-circle me-1\"></i>Cancel\r\n </button>\r\n <button\r\n type=\"button\"\r\n class=\"btn btn-primary\"\r\n (click)=\"onSubmit()\"\r\n [disabled]=\"searchReplaceForm.invalid || isProcessing()\">\r\n @if (isProcessing()) {\r\n <span class=\"spinner-border spinner-border-sm me-2\"></span>\r\n }\r\n <i class=\"bi bi-arrow-repeat me-1\"></i>\r\n Replace All\r\n </button>\r\n</div>\r\n" }]
7506
+ }], ctorParameters: () => [{ type: i1.FormBuilder }, { type: i2.NgbActiveModal }], propDecorators: { config: [{
7507
+ type: Input
7508
+ }] } });
7509
+
7510
+ class SamplePoolDetailsModal {
7511
+ activeModal;
7512
+ pool;
7513
+ constructor(activeModal) {
7514
+ this.activeModal = activeModal;
7515
+ }
7516
+ onClose() {
7517
+ this.activeModal.close();
7518
+ }
7519
+ get title() {
7520
+ return this.pool?.poolName || 'Sample Pool Details';
7521
+ }
7522
+ get poolTypeLabel() {
7523
+ return this.pool?.isReference ? 'Reference Pool' : 'Sample Pool';
7524
+ }
7525
+ get poolTypeClass() {
7526
+ return this.pool?.isReference ? 'badge bg-info' : 'badge bg-primary';
7527
+ }
7528
+ get pooledOnlySamplesText() {
7529
+ if (!this.pool?.pooledOnlySamples || this.pool.pooledOnlySamples.length === 0) {
7530
+ return 'None';
7531
+ }
7532
+ return this.pool.pooledOnlySamples.join(', ');
7533
+ }
7534
+ get pooledAndIndependentSamplesText() {
7535
+ if (!this.pool?.pooledAndIndependentSamples || this.pool.pooledAndIndependentSamples.length === 0) {
7536
+ return 'None';
7537
+ }
7538
+ return this.pool.pooledAndIndependentSamples.join(', ');
7539
+ }
7540
+ get allSamplesCount() {
7541
+ const pooledOnly = this.pool?.pooledOnlySamples?.length || 0;
7542
+ const pooledAndIndependent = this.pool?.pooledAndIndependentSamples?.length || 0;
7543
+ return pooledOnly + pooledAndIndependent;
7544
+ }
7545
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.7", ngImport: i0, type: SamplePoolDetailsModal, deps: [{ token: i2.NgbActiveModal }], target: i0.ɵɵFactoryTarget.Component });
7546
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.7", type: SamplePoolDetailsModal, isStandalone: true, selector: "ccv-sample-pool-details-modal", inputs: { pool: "pool" }, ngImport: i0, template: "<div class=\"modal-header\">\n <h4 class=\"modal-title\">\n <i class=\"bi bi-collection me-2\"></i>{{ title }}\n </h4>\n <button type=\"button\" class=\"btn-close\" (click)=\"onClose()\"></button>\n</div>\n\n<div class=\"modal-body\">\n @if (pool) {\n <!-- Pool Type and Basic Info -->\n <div class=\"mb-4\">\n <div class=\"d-flex justify-content-between align-items-start mb-3\">\n <div>\n <h5 class=\"mb-2\">{{ pool.poolName }}</h5>\n <span [class]=\"poolTypeClass\">\n <i class=\"bi bi-collection me-1\"></i>{{ poolTypeLabel }}\n </span>\n </div>\n <div class=\"text-end\">\n <small class=\"text-muted d-block\">Pool ID: {{ pool.id }}</small>\n <small class=\"text-muted\">Total Samples: {{ allSamplesCount }}</small>\n </div>\n </div>\n \n @if (pool.poolDescription) {\n <div class=\"mt-3\">\n <strong>Description:</strong>\n <p class=\"mt-1 mb-0\">{{ pool.poolDescription }}</p>\n </div>\n }\n </div>\n\n <!-- Sample Configuration -->\n <div class=\"mb-4\">\n <h6 class=\"text-muted mb-3\">\n <i class=\"bi bi-diagram-3 me-2\"></i>Sample Configuration\n </h6>\n \n <div class=\"row\">\n <!-- Pooled Only Samples -->\n <div class=\"col-md-6 mb-3\">\n <div class=\"card h-100\">\n <div class=\"card-body\">\n <h6 class=\"card-title\">\n <i class=\"bi bi-collection text-primary me-2\"></i>Pooled Only Samples\n </h6>\n <p class=\"card-text\">\n <small class=\"text-muted\">{{ pool.pooledOnlySamples.length || 0 }} samples</small>\n </p>\n <div class=\"sample-list\">\n @if (pool.pooledOnlySamples && pool.pooledOnlySamples.length > 0) {\n <div class=\"sample-numbers\">{{ pooledOnlySamplesText }}</div>\n } @else {\n <em class=\"text-muted\">No pooled-only samples</em>\n }\n </div>\n </div>\n </div>\n </div>\n\n <!-- Pooled and Independent Samples -->\n <div class=\"col-md-6 mb-3\">\n <div class=\"card h-100\">\n <div class=\"card-body\">\n <h6 class=\"card-title\">\n <i class=\"bi bi-diagram-2 text-success me-2\"></i>Pooled & Independent Samples\n </h6>\n <p class=\"card-text\">\n <small class=\"text-muted\">{{ pool.pooledAndIndependentSamples.length || 0 }} samples</small>\n </p>\n <div class=\"sample-list\">\n @if (pool.pooledAndIndependentSamples && pool.pooledAndIndependentSamples.length > 0) {\n <div class=\"sample-numbers\">{{ pooledAndIndependentSamplesText }}</div>\n } @else {\n <em class=\"text-muted\">No pooled & independent samples</em>\n }\n </div>\n </div>\n </div>\n </div>\n </div>\n\n <!-- Template Sample -->\n </div>\n\n <!-- Metadata Columns -->\n @if (pool.metadataColumns && pool.metadataColumns.length > 0) {\n <div class=\"mb-4\">\n <h6 class=\"text-muted mb-3\">\n <i class=\"bi bi-table me-2\"></i>Metadata Columns ({{ pool.metadataColumns.length }})\n </h6>\n <div class=\"table-responsive\">\n <table class=\"table table-sm\">\n <thead>\n <tr>\n <th>Column Name</th>\n <th>Type</th>\n <th>Value</th>\n <th>Ontology</th>\n </tr>\n </thead>\n <tbody>\n @for (column of pool.metadataColumns; track column.id) {\n <tr>\n <td>\n <strong>{{ column.name }}</strong>\n </td>\n <td>\n <span class=\"badge bg-light text-dark\">{{ column.type }}</span>\n </td>\n <td>\n @if (column.value) {\n <code>{{ column.value }}</code>\n } @else {\n <em class=\"text-muted\">No default value</em>\n }\n </td>\n <td>\n @if (column.ontologyType) {\n <span class=\"badge bg-info\">{{ column.ontologyType }}</span>\n } @else {\n <em class=\"text-muted\">None</em>\n }\n </td>\n </tr>\n }\n </tbody>\n </table>\n </div>\n </div>\n }\n\n <!-- Timestamps -->\n <div class=\"mt-4\">\n <div class=\"row\">\n <div class=\"col-md-6\">\n <small class=\"text-muted\">\n <i class=\"bi bi-calendar-plus me-1\"></i>\n Created: {{ pool.createdAt | date:'MMM d, y, h:mm a' }}\n </small>\n </div>\n <div class=\"col-md-6\">\n <small class=\"text-muted\">\n <i class=\"bi bi-calendar-check me-1\"></i>\n Updated: {{ pool.updatedAt | date:'MMM d, y, h:mm a' }}\n </small>\n </div>\n </div>\n </div>\n\n } @else {\n <div class=\"text-center py-4\">\n <i class=\"bi bi-exclamation-triangle text-warning display-4\"></i>\n <h5 class=\"mt-3 text-muted\">No Pool Data Available</h5>\n <p class=\"text-muted\">Pool information could not be loaded.</p>\n </div>\n }\n</div>\n\n<div class=\"modal-footer\">\n <button type=\"button\" class=\"btn btn-secondary\" (click)=\"onClose()\">\n <i class=\"bi bi-x-circle me-1\"></i>Close\n </button>\n</div>", styles: [""], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: NgbModule }, { kind: "pipe", type: i5.DatePipe, name: "date" }] });
7547
+ }
7548
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.7", ngImport: i0, type: SamplePoolDetailsModal, decorators: [{
7549
+ type: Component,
7550
+ args: [{ selector: 'ccv-sample-pool-details-modal', standalone: true, imports: [CommonModule, NgbModule], template: "<div class=\"modal-header\">\n <h4 class=\"modal-title\">\n <i class=\"bi bi-collection me-2\"></i>{{ title }}\n </h4>\n <button type=\"button\" class=\"btn-close\" (click)=\"onClose()\"></button>\n</div>\n\n<div class=\"modal-body\">\n @if (pool) {\n <!-- Pool Type and Basic Info -->\n <div class=\"mb-4\">\n <div class=\"d-flex justify-content-between align-items-start mb-3\">\n <div>\n <h5 class=\"mb-2\">{{ pool.poolName }}</h5>\n <span [class]=\"poolTypeClass\">\n <i class=\"bi bi-collection me-1\"></i>{{ poolTypeLabel }}\n </span>\n </div>\n <div class=\"text-end\">\n <small class=\"text-muted d-block\">Pool ID: {{ pool.id }}</small>\n <small class=\"text-muted\">Total Samples: {{ allSamplesCount }}</small>\n </div>\n </div>\n \n @if (pool.poolDescription) {\n <div class=\"mt-3\">\n <strong>Description:</strong>\n <p class=\"mt-1 mb-0\">{{ pool.poolDescription }}</p>\n </div>\n }\n </div>\n\n <!-- Sample Configuration -->\n <div class=\"mb-4\">\n <h6 class=\"text-muted mb-3\">\n <i class=\"bi bi-diagram-3 me-2\"></i>Sample Configuration\n </h6>\n \n <div class=\"row\">\n <!-- Pooled Only Samples -->\n <div class=\"col-md-6 mb-3\">\n <div class=\"card h-100\">\n <div class=\"card-body\">\n <h6 class=\"card-title\">\n <i class=\"bi bi-collection text-primary me-2\"></i>Pooled Only Samples\n </h6>\n <p class=\"card-text\">\n <small class=\"text-muted\">{{ pool.pooledOnlySamples.length || 0 }} samples</small>\n </p>\n <div class=\"sample-list\">\n @if (pool.pooledOnlySamples && pool.pooledOnlySamples.length > 0) {\n <div class=\"sample-numbers\">{{ pooledOnlySamplesText }}</div>\n } @else {\n <em class=\"text-muted\">No pooled-only samples</em>\n }\n </div>\n </div>\n </div>\n </div>\n\n <!-- Pooled and Independent Samples -->\n <div class=\"col-md-6 mb-3\">\n <div class=\"card h-100\">\n <div class=\"card-body\">\n <h6 class=\"card-title\">\n <i class=\"bi bi-diagram-2 text-success me-2\"></i>Pooled & Independent Samples\n </h6>\n <p class=\"card-text\">\n <small class=\"text-muted\">{{ pool.pooledAndIndependentSamples.length || 0 }} samples</small>\n </p>\n <div class=\"sample-list\">\n @if (pool.pooledAndIndependentSamples && pool.pooledAndIndependentSamples.length > 0) {\n <div class=\"sample-numbers\">{{ pooledAndIndependentSamplesText }}</div>\n } @else {\n <em class=\"text-muted\">No pooled & independent samples</em>\n }\n </div>\n </div>\n </div>\n </div>\n </div>\n\n <!-- Template Sample -->\n </div>\n\n <!-- Metadata Columns -->\n @if (pool.metadataColumns && pool.metadataColumns.length > 0) {\n <div class=\"mb-4\">\n <h6 class=\"text-muted mb-3\">\n <i class=\"bi bi-table me-2\"></i>Metadata Columns ({{ pool.metadataColumns.length }})\n </h6>\n <div class=\"table-responsive\">\n <table class=\"table table-sm\">\n <thead>\n <tr>\n <th>Column Name</th>\n <th>Type</th>\n <th>Value</th>\n <th>Ontology</th>\n </tr>\n </thead>\n <tbody>\n @for (column of pool.metadataColumns; track column.id) {\n <tr>\n <td>\n <strong>{{ column.name }}</strong>\n </td>\n <td>\n <span class=\"badge bg-light text-dark\">{{ column.type }}</span>\n </td>\n <td>\n @if (column.value) {\n <code>{{ column.value }}</code>\n } @else {\n <em class=\"text-muted\">No default value</em>\n }\n </td>\n <td>\n @if (column.ontologyType) {\n <span class=\"badge bg-info\">{{ column.ontologyType }}</span>\n } @else {\n <em class=\"text-muted\">None</em>\n }\n </td>\n </tr>\n }\n </tbody>\n </table>\n </div>\n </div>\n }\n\n <!-- Timestamps -->\n <div class=\"mt-4\">\n <div class=\"row\">\n <div class=\"col-md-6\">\n <small class=\"text-muted\">\n <i class=\"bi bi-calendar-plus me-1\"></i>\n Created: {{ pool.createdAt | date:'MMM d, y, h:mm a' }}\n </small>\n </div>\n <div class=\"col-md-6\">\n <small class=\"text-muted\">\n <i class=\"bi bi-calendar-check me-1\"></i>\n Updated: {{ pool.updatedAt | date:'MMM d, y, h:mm a' }}\n </small>\n </div>\n </div>\n </div>\n\n } @else {\n <div class=\"text-center py-4\">\n <i class=\"bi bi-exclamation-triangle text-warning display-4\"></i>\n <h5 class=\"mt-3 text-muted\">No Pool Data Available</h5>\n <p class=\"text-muted\">Pool information could not be loaded.</p>\n </div>\n }\n</div>\n\n<div class=\"modal-footer\">\n <button type=\"button\" class=\"btn btn-secondary\" (click)=\"onClose()\">\n <i class=\"bi bi-x-circle me-1\"></i>Close\n </button>\n</div>" }]
7551
+ }], ctorParameters: () => [{ type: i2.NgbActiveModal }], propDecorators: { pool: [{
7552
+ type: Input
7553
+ }] } });
7554
+
7555
+ class SamplePoolEditModal {
7556
+ pool;
7557
+ maxSampleCount = 100;
7558
+ poolSaved = new EventEmitter();
7559
+ editForm;
7560
+ isLoading = signal(false, ...(ngDevMode ? [{ debugName: "isLoading" }] : []));
7561
+ fb = inject(FormBuilder);
7562
+ activeModal = inject(NgbActiveModal);
7563
+ samplePoolService = inject(SamplePoolService);
7564
+ constructor() {
7565
+ this.editForm = this.fb.group({
7566
+ poolName: ['', [Validators.required, Validators.maxLength(255)]],
7567
+ poolDescription: [''],
7568
+ isReference: [false],
7569
+ pooledOnlySamplesText: [''],
7570
+ pooledAndIndependentSamplesText: ['']
7571
+ });
7572
+ }
7573
+ ngOnInit() {
7574
+ if (this.pool) {
7575
+ this.editForm.patchValue({
7576
+ poolName: this.pool.poolName,
7577
+ poolDescription: this.pool.poolDescription || '',
7578
+ isReference: this.pool.isReference,
7579
+ pooledOnlySamplesText: this.pool.pooledOnlySamples?.join(', ') || '',
7580
+ pooledAndIndependentSamplesText: this.pool.pooledAndIndependentSamples?.join(', ') || ''
7581
+ });
7582
+ }
7583
+ }
7584
+ onSubmit() {
7585
+ if (this.editForm.valid && this.pool.id) {
7586
+ this.isLoading.set(true);
7587
+ const formValue = this.editForm.value;
7588
+ // Parse sample arrays from text input
7589
+ const pooledOnlySamples = this.parseSampleNumbers(formValue.pooledOnlySamplesText);
7590
+ const pooledAndIndependentSamples = this.parseSampleNumbers(formValue.pooledAndIndependentSamplesText);
7591
+ const updateData = {
7592
+ poolName: formValue.poolName.trim(),
7593
+ poolDescription: formValue.poolDescription?.trim() || undefined,
7594
+ isReference: formValue.isReference,
7595
+ pooledOnlySamples: pooledOnlySamples,
7596
+ pooledAndIndependentSamples: pooledAndIndependentSamples
7597
+ };
7598
+ this.samplePoolService.updateSamplePool(this.pool.id, updateData).subscribe({
7599
+ next: (updatedPool) => {
7600
+ this.poolSaved.emit(updatedPool);
7601
+ this.activeModal.close(updatedPool);
7602
+ },
7603
+ error: (error) => {
7604
+ console.error('Error updating sample pool:', error);
7605
+ this.isLoading.set(false);
7606
+ }
7607
+ });
7608
+ }
7609
+ }
7610
+ onCancel() {
7611
+ this.activeModal.dismiss();
7612
+ }
7613
+ get title() {
7614
+ return `Edit ${this.pool?.poolName || 'Sample Pool'}`;
7615
+ }
7616
+ get hasChanges() {
7617
+ if (!this.pool)
7618
+ return false;
7619
+ const formValue = this.editForm.value;
7620
+ return (formValue.poolName !== this.pool.poolName ||
7621
+ formValue.poolDescription !== (this.pool.poolDescription || '') ||
7622
+ formValue.isReference !== this.pool.isReference ||
7623
+ formValue.pooledOnlySamplesText !== (this.pool.pooledOnlySamples?.join(', ') || '') ||
7624
+ formValue.pooledAndIndependentSamplesText !== (this.pool.pooledAndIndependentSamples?.join(', ') || ''));
7625
+ }
7626
+ parseSampleNumbers(text) {
7627
+ if (!text?.trim())
7628
+ return [];
7629
+ const results = [];
7630
+ text
7631
+ .split(',')
7632
+ .map(s => s.trim())
7633
+ .filter(s => s.length > 0)
7634
+ .forEach(s => {
7635
+ // Check if it's a range (e.g., "9-12")
7636
+ if (s.includes('-')) {
7637
+ const parts = s.split('-').map(p => p.trim());
7638
+ if (parts.length === 2) {
7639
+ const start = parseInt(parts[0], 10);
7640
+ const end = parseInt(parts[1], 10);
7641
+ if (!isNaN(start) && !isNaN(end) && start > 0 && end > 0 && start <= end && end <= this.maxSampleCount) {
7642
+ // Add all numbers in the range
7643
+ for (let i = start; i <= end; i++) {
7644
+ results.push(i);
7645
+ }
7646
+ }
7647
+ }
7648
+ }
7649
+ else {
7650
+ // Single number
7651
+ const num = parseInt(s, 10);
7652
+ if (!isNaN(num) && num > 0 && num <= this.maxSampleCount) {
7653
+ results.push(num);
7654
+ }
7655
+ }
7656
+ });
7657
+ // Remove duplicates and sort
7658
+ return [...new Set(results)].sort((a, b) => a - b);
7659
+ }
7660
+ // Form validation helpers
7661
+ isFieldInvalid(fieldName) {
7662
+ const field = this.editForm.get(fieldName);
7663
+ return !!(field && field.invalid && field.touched);
7664
+ }
7665
+ getFieldError(fieldName) {
7666
+ const field = this.editForm.get(fieldName);
7667
+ if (!field || !field.errors)
7668
+ return '';
7669
+ if (field.errors['required']) {
7670
+ return `${this.getFieldDisplayName(fieldName)} is required`;
7671
+ }
7672
+ if (field.errors['maxlength']) {
7673
+ return `${this.getFieldDisplayName(fieldName)} must be less than ${field.errors['maxlength'].requiredLength} characters`;
7674
+ }
7675
+ return 'Invalid value';
7676
+ }
7677
+ getFieldDisplayName(fieldName) {
7678
+ const displayNames = {
7679
+ 'poolName': 'Pool name',
7680
+ 'poolDescription': 'Description',
7681
+ 'pooledOnlySamplesText': 'Pooled only samples',
7682
+ 'pooledAndIndependentSamplesText': 'Pooled and independent samples'
7683
+ };
7684
+ return displayNames[fieldName] || fieldName;
7685
+ }
7686
+ // Helper methods for template
7687
+ get pooledOnlySamplesCount() {
7688
+ const text = this.editForm.get('pooledOnlySamplesText')?.value || '';
7689
+ return this.parseSampleNumbers(text).length;
7690
+ }
7691
+ get pooledAndIndependentSamplesCount() {
7692
+ const text = this.editForm.get('pooledAndIndependentSamplesText')?.value || '';
7693
+ return this.parseSampleNumbers(text).length;
7694
+ }
7695
+ get totalSamplesCount() {
7696
+ return this.pooledOnlySamplesCount + this.pooledAndIndependentSamplesCount;
7697
+ }
7698
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.7", ngImport: i0, type: SamplePoolEditModal, deps: [], target: i0.ɵɵFactoryTarget.Component });
7699
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.7", type: SamplePoolEditModal, isStandalone: true, selector: "ccv-sample-pool-edit-modal", inputs: { pool: "pool", maxSampleCount: "maxSampleCount" }, outputs: { poolSaved: "poolSaved" }, ngImport: i0, template: "<div class=\"modal-header\">\n <h4 class=\"modal-title\">\n <i class=\"bi bi-collection 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 \n <!-- Pool Name -->\n <div class=\"mb-3\">\n <label for=\"poolName\" class=\"form-label\">Pool Name *</label>\n <input \n type=\"text\" \n class=\"form-control\"\n id=\"poolName\"\n formControlName=\"poolName\"\n [class.is-invalid]=\"isFieldInvalid('poolName')\"\n placeholder=\"Enter pool name\">\n @if (isFieldInvalid('poolName')) {\n <div class=\"invalid-feedback\">\n {{ getFieldError('poolName') }}\n </div>\n }\n </div>\n\n <!-- Description -->\n <div class=\"mb-3\">\n <label for=\"poolDescription\" class=\"form-label\">Description</label>\n <textarea \n class=\"form-control\"\n id=\"poolDescription\"\n formControlName=\"poolDescription\"\n rows=\"2\"\n placeholder=\"Optional pool description\"></textarea>\n </div>\n\n <!-- Pool Type and Template Sample -->\n <div class=\"row\">\n <div class=\"col-md-6 mb-3\">\n <div class=\"form-check\">\n <input \n class=\"form-check-input\" \n type=\"checkbox\" \n id=\"isReference\" \n formControlName=\"isReference\">\n <label class=\"form-check-label\" for=\"isReference\">\n <strong>Reference Pool</strong>\n <div class=\"form-text\">Check if this is a reference pool</div>\n </label>\n </div>\n </div>\n </div>\n\n <!-- Sample Configuration -->\n <div class=\"mb-4\">\n <h6 class=\"text-muted mb-3\">Sample Configuration</h6>\n \n <!-- Pooled Only Samples -->\n <div class=\"mb-3\">\n <label for=\"pooledOnlySamples\" class=\"form-label\">\n Pooled Only Samples\n <span class=\"badge bg-primary ms-2\">{{ pooledOnlySamplesCount }} samples</span>\n </label>\n <input \n type=\"text\" \n class=\"form-control\"\n id=\"pooledOnlySamples\"\n formControlName=\"pooledOnlySamplesText\"\n placeholder=\"e.g., 1, 2, 3, 5-8\">\n <div class=\"form-text\">\n Enter sample numbers separated by commas. Ranges like \"5-8\" are supported.\n </div>\n </div>\n\n <!-- Pooled and Independent Samples -->\n <div class=\"mb-3\">\n <label for=\"pooledAndIndependentSamples\" class=\"form-label\">\n Pooled & Independent Samples\n <span class=\"badge bg-success ms-2\">{{ pooledAndIndependentSamplesCount }} samples</span>\n </label>\n <input \n type=\"text\" \n class=\"form-control\"\n id=\"pooledAndIndependentSamples\"\n formControlName=\"pooledAndIndependentSamplesText\"\n placeholder=\"e.g., 4, 9, 10-15\">\n <div class=\"form-text\">\n Enter sample numbers that are both pooled and run independently.\n </div>\n </div>\n\n <!-- Total Count Display -->\n <div class=\"alert alert-info\">\n <i class=\"bi bi-info-circle me-2\"></i>\n <strong>Total Samples:</strong> {{ totalSamplesCount }}\n @if (totalSamplesCount > maxSampleCount) {\n <span class=\"text-danger ms-2\">\n (Exceeds maximum of {{ maxSampleCount }})\n </span>\n }\n </div>\n </div>\n\n <!-- Character Count -->\n <div class=\"mb-2\">\n <div class=\"d-flex justify-content-between align-items-center\">\n <small class=\"text-muted\">\n Name: {{ editForm.get('poolName')?.value?.length || 0 }}/255 characters\n </small>\n @if (hasChanges) {\n <small class=\"text-info\">\n <i class=\"bi bi-info-circle me-1\"></i>Changes detected\n </small>\n }\n </div>\n </div>\n\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() || !hasChanges\">\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 Pool\n </button>\n</div>", 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.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.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 }] });
7700
+ }
7701
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.7", ngImport: i0, type: SamplePoolEditModal, decorators: [{
7702
+ type: Component,
7703
+ args: [{ selector: 'ccv-sample-pool-edit-modal', standalone: true, imports: [CommonModule, ReactiveFormsModule, NgbModule], template: "<div class=\"modal-header\">\n <h4 class=\"modal-title\">\n <i class=\"bi bi-collection 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 \n <!-- Pool Name -->\n <div class=\"mb-3\">\n <label for=\"poolName\" class=\"form-label\">Pool Name *</label>\n <input \n type=\"text\" \n class=\"form-control\"\n id=\"poolName\"\n formControlName=\"poolName\"\n [class.is-invalid]=\"isFieldInvalid('poolName')\"\n placeholder=\"Enter pool name\">\n @if (isFieldInvalid('poolName')) {\n <div class=\"invalid-feedback\">\n {{ getFieldError('poolName') }}\n </div>\n }\n </div>\n\n <!-- Description -->\n <div class=\"mb-3\">\n <label for=\"poolDescription\" class=\"form-label\">Description</label>\n <textarea \n class=\"form-control\"\n id=\"poolDescription\"\n formControlName=\"poolDescription\"\n rows=\"2\"\n placeholder=\"Optional pool description\"></textarea>\n </div>\n\n <!-- Pool Type and Template Sample -->\n <div class=\"row\">\n <div class=\"col-md-6 mb-3\">\n <div class=\"form-check\">\n <input \n class=\"form-check-input\" \n type=\"checkbox\" \n id=\"isReference\" \n formControlName=\"isReference\">\n <label class=\"form-check-label\" for=\"isReference\">\n <strong>Reference Pool</strong>\n <div class=\"form-text\">Check if this is a reference pool</div>\n </label>\n </div>\n </div>\n </div>\n\n <!-- Sample Configuration -->\n <div class=\"mb-4\">\n <h6 class=\"text-muted mb-3\">Sample Configuration</h6>\n \n <!-- Pooled Only Samples -->\n <div class=\"mb-3\">\n <label for=\"pooledOnlySamples\" class=\"form-label\">\n Pooled Only Samples\n <span class=\"badge bg-primary ms-2\">{{ pooledOnlySamplesCount }} samples</span>\n </label>\n <input \n type=\"text\" \n class=\"form-control\"\n id=\"pooledOnlySamples\"\n formControlName=\"pooledOnlySamplesText\"\n placeholder=\"e.g., 1, 2, 3, 5-8\">\n <div class=\"form-text\">\n Enter sample numbers separated by commas. Ranges like \"5-8\" are supported.\n </div>\n </div>\n\n <!-- Pooled and Independent Samples -->\n <div class=\"mb-3\">\n <label for=\"pooledAndIndependentSamples\" class=\"form-label\">\n Pooled & Independent Samples\n <span class=\"badge bg-success ms-2\">{{ pooledAndIndependentSamplesCount }} samples</span>\n </label>\n <input \n type=\"text\" \n class=\"form-control\"\n id=\"pooledAndIndependentSamples\"\n formControlName=\"pooledAndIndependentSamplesText\"\n placeholder=\"e.g., 4, 9, 10-15\">\n <div class=\"form-text\">\n Enter sample numbers that are both pooled and run independently.\n </div>\n </div>\n\n <!-- Total Count Display -->\n <div class=\"alert alert-info\">\n <i class=\"bi bi-info-circle me-2\"></i>\n <strong>Total Samples:</strong> {{ totalSamplesCount }}\n @if (totalSamplesCount > maxSampleCount) {\n <span class=\"text-danger ms-2\">\n (Exceeds maximum of {{ maxSampleCount }})\n </span>\n }\n </div>\n </div>\n\n <!-- Character Count -->\n <div class=\"mb-2\">\n <div class=\"d-flex justify-content-between align-items-center\">\n <small class=\"text-muted\">\n Name: {{ editForm.get('poolName')?.value?.length || 0 }}/255 characters\n </small>\n @if (hasChanges) {\n <small class=\"text-info\">\n <i class=\"bi bi-info-circle me-1\"></i>Changes detected\n </small>\n }\n </div>\n </div>\n\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() || !hasChanges\">\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 Pool\n </button>\n</div>" }]
7704
+ }], ctorParameters: () => [], propDecorators: { pool: [{
7705
+ type: Input
7706
+ }], maxSampleCount: [{
7707
+ type: Input
7708
+ }], poolSaved: [{
7709
+ type: Output
7710
+ }] } });
7711
+
7712
+ class SamplePoolCreateModal {
7713
+ fb;
7714
+ activeModal;
7715
+ samplePoolService;
7716
+ toastService;
7717
+ metadataTable;
7718
+ poolCreated = new EventEmitter();
7719
+ // Expose Math to template
7720
+ Math = Math;
7721
+ createForm;
7722
+ isLoading = signal(false, ...(ngDevMode ? [{ debugName: "isLoading" }] : []));
7723
+ // Sample selection management
7724
+ samples = signal([], ...(ngDevMode ? [{ debugName: "samples" }] : []));
7725
+ pooledOnlySamples = signal([], ...(ngDevMode ? [{ debugName: "pooledOnlySamples" }] : []));
7726
+ pooledAndIndependentSamples = signal([], ...(ngDevMode ? [{ debugName: "pooledAndIndependentSamples" }] : []));
7727
+ // UI state
7728
+ selectionMode = signal('manual', ...(ngDevMode ? [{ debugName: "selectionMode" }] : []));
7729
+ currentPage = signal(1, ...(ngDevMode ? [{ debugName: "currentPage" }] : []));
7730
+ samplesPerPage = 20;
7731
+ constructor(fb, activeModal, samplePoolService, toastService) {
7732
+ this.fb = fb;
7733
+ this.activeModal = activeModal;
7734
+ this.samplePoolService = samplePoolService;
7735
+ this.toastService = toastService;
7736
+ this.createForm = this.fb.group({
7737
+ poolName: ['', [Validators.required, Validators.maxLength(255)]],
7738
+ poolDescription: [''],
7739
+ isReference: [false],
7740
+ range_start: [1, [Validators.min(1)]],
7741
+ range_end: [10, [Validators.min(1)]],
7742
+ pooled_only_samples_text: [''],
7743
+ pooled_and_independent_samples_text: ['']
7744
+ });
7745
+ }
7746
+ ngOnInit() {
7747
+ if (!this.metadataTable) {
7748
+ this.activeModal.dismiss('No metadata table provided');
7749
+ return;
7750
+ }
7751
+ // Initialize sample list
7752
+ this.initializeSamples();
7753
+ // Update range end validator based on sample count
7754
+ const rangeEndControl = this.createForm.get('range_end');
7755
+ if (rangeEndControl) {
7756
+ rangeEndControl.setValidators([
7757
+ Validators.min(1),
7758
+ Validators.max(this.metadataTable.sampleCount)
7759
+ ]);
7760
+ rangeEndControl.updateValueAndValidity();
7761
+ }
7762
+ const rangeStartControl = this.createForm.get('range_start');
7763
+ if (rangeStartControl) {
7764
+ rangeStartControl.setValidators([
7765
+ Validators.min(1),
7766
+ Validators.max(this.metadataTable.sampleCount)
7767
+ ]);
7768
+ rangeStartControl.updateValueAndValidity();
7769
+ }
7770
+ }
7771
+ initializeSamples() {
7772
+ const sampleItems = [];
7773
+ const existingPooledSamples = this.getExistingPooledSamples();
7774
+ for (let i = 1; i <= this.metadataTable.sampleCount; i++) {
7775
+ sampleItems.push({
7776
+ sampleNumber: i,
7777
+ selected: false,
7778
+ pooled: existingPooledSamples.includes(i)
7779
+ });
7780
+ }
7781
+ this.samples.set(sampleItems);
7782
+ }
7783
+ getExistingPooledSamples() {
7784
+ if (!this.metadataTable.samplePools)
7785
+ return [];
7786
+ const pooledSamples = [];
7787
+ this.metadataTable.samplePools.forEach((pool) => {
7788
+ pooledSamples.push(...pool.pooled_only_samples || []);
7789
+ pooledSamples.push(...pool.pooled_and_independent_samples || []);
7790
+ });
7791
+ return [...new Set(pooledSamples)]; // Remove duplicates
7792
+ }
7793
+ // Sample selection methods
7794
+ toggleSampleSelection(sampleNumber) {
7795
+ const samples = this.samples();
7796
+ const sampleIndex = samples.findIndex(s => s.sampleNumber === sampleNumber);
7797
+ if (sampleIndex >= 0) {
7798
+ samples[sampleIndex].selected = !samples[sampleIndex].selected;
7799
+ this.samples.set([...samples]);
7800
+ this.updateSampleTextInputs();
7801
+ }
7802
+ }
7803
+ selectSamplesInRange() {
7804
+ const start = this.createForm.get('range_start')?.value || 1;
7805
+ const end = this.createForm.get('range_end')?.value || 1;
7806
+ if (start > end) {
7807
+ this.toastService.error('Start sample must be less than or equal to end sample');
7808
+ return;
7809
+ }
7810
+ const samples = this.samples();
7811
+ for (let i = start; i <= end; i++) {
7812
+ const sampleIndex = samples.findIndex(s => s.sampleNumber === i);
7813
+ if (sampleIndex >= 0) {
7814
+ samples[sampleIndex].selected = true;
7815
+ }
7816
+ }
7817
+ this.samples.set([...samples]);
7818
+ this.updateSampleTextInputs();
7819
+ this.toastService.success(`Selected samples ${start} to ${end}`);
7820
+ }
7821
+ clearAllSelections() {
7822
+ const samples = this.samples();
7823
+ samples.forEach(s => s.selected = false);
7824
+ this.samples.set([...samples]);
7825
+ this.updateSampleTextInputs();
7826
+ }
7827
+ updateSampleTextInputs() {
7828
+ const selectedSamples = this.samples().filter(s => s.selected).map(s => s.sampleNumber);
7829
+ const pooledOnly = this.pooledOnlySamples();
7830
+ const pooledAndIndependent = this.pooledAndIndependentSamples();
7831
+ // For now, put all selected samples in pooled_only
7832
+ this.createForm.patchValue({
7833
+ pooled_only_samples_text: selectedSamples.join(', '),
7834
+ pooled_and_independent_samples_text: pooledAndIndependent.join(', ')
7835
+ });
7836
+ }
7837
+ // Pool type assignment
7838
+ assignToPooledOnly() {
7839
+ const selectedSamples = this.samples().filter(s => s.selected).map(s => s.sampleNumber);
7840
+ this.pooledOnlySamples.set(selectedSamples);
7841
+ this.updateSampleTextInputs();
7842
+ }
7843
+ assignToPooledAndIndependent() {
7844
+ const selectedSamples = this.samples().filter(s => s.selected).map(s => s.sampleNumber);
7845
+ this.pooledAndIndependentSamples.set(selectedSamples);
7846
+ this.updateSampleTextInputs();
7847
+ }
7848
+ onSubmit() {
7849
+ if (this.createForm.valid) {
7850
+ this.isLoading.set(true);
7851
+ const formValue = this.createForm.value;
7852
+ // Parse sample arrays from text input or use current assignments
7853
+ const pooledOnlySamples = this.parseSampleNumbers(formValue.pooled_only_samples_text);
7854
+ const pooledAndIndependentSamples = this.parseSampleNumbers(formValue.pooled_and_independent_samples_text);
7855
+ if (pooledOnlySamples.length === 0 && pooledAndIndependentSamples.length === 0) {
7856
+ this.toastService.error('Please select at least one sample for the pool');
7857
+ this.isLoading.set(false);
7858
+ return;
7859
+ }
7860
+ const createData = {
7861
+ poolName: formValue.poolName.trim(),
7862
+ poolDescription: formValue.poolDescription?.trim() || undefined,
7863
+ isReference: formValue.isReference,
7864
+ pooledOnlySamples: pooledOnlySamples,
7865
+ pooledAndIndependentSamples: pooledAndIndependentSamples,
7866
+ metadataTable: this.metadataTable.id
7867
+ };
7868
+ this.samplePoolService.createSamplePool(createData).subscribe({
7869
+ next: (createdPool) => {
7870
+ this.toastService.success(`Sample pool "${createdPool.poolName}" created successfully`);
7871
+ this.poolCreated.emit(createdPool);
7872
+ this.activeModal.close(createdPool);
7873
+ },
7874
+ error: (error) => {
7875
+ console.error('Error creating sample pool:', error);
7876
+ this.toastService.error('Failed to create sample pool');
7877
+ this.isLoading.set(false);
7878
+ }
7879
+ });
7880
+ }
7881
+ }
7882
+ onCancel() {
7883
+ this.activeModal.dismiss();
7884
+ }
7885
+ // Helper methods
7886
+ parseSampleNumbers(text) {
7887
+ if (!text?.trim())
7888
+ return [];
7889
+ const numbers = [];
7890
+ const parts = text.split(',').map(s => s.trim()).filter(s => s.length > 0);
7891
+ parts.forEach(part => {
7892
+ if (part.includes('-')) {
7893
+ // Handle range like "5-8"
7894
+ const [start, end] = part.split('-').map(s => parseInt(s.trim(), 10));
7895
+ if (!isNaN(start) && !isNaN(end) && start <= end) {
7896
+ for (let i = start; i <= end; i++) {
7897
+ if (i > 0 && i <= this.metadataTable.sampleCount) {
7898
+ numbers.push(i);
7899
+ }
7900
+ }
7901
+ }
7902
+ }
7903
+ else {
7904
+ // Handle single number
7905
+ const num = parseInt(part, 10);
7906
+ if (!isNaN(num) && num > 0 && num <= this.metadataTable.sampleCount) {
7907
+ numbers.push(num);
7908
+ }
7909
+ }
7910
+ });
7911
+ return [...new Set(numbers)].sort((a, b) => a - b); // Remove duplicates and sort
7912
+ }
7913
+ // Computed properties for template
7914
+ get selectedSamplesCount() {
7915
+ return this.samples().filter(s => s.selected).length;
7916
+ }
7917
+ get totalPooledSamplesCount() {
7918
+ const pooledOnly = this.parseSampleNumbers(this.createForm.get('pooled_only_samples_text')?.value || '');
7919
+ const pooledAndIndependent = this.parseSampleNumbers(this.createForm.get('pooled_and_independent_samples_text')?.value || '');
7920
+ return pooledOnly.length + pooledAndIndependent.length;
7921
+ }
7922
+ get paginatedSamples() {
7923
+ const samples = this.samples();
7924
+ const start = (this.currentPage() - 1) * this.samplesPerPage;
7925
+ return samples.slice(start, start + this.samplesPerPage);
7926
+ }
7927
+ get totalPages() {
7928
+ return Math.ceil(this.samples().length / this.samplesPerPage);
7929
+ }
7930
+ // Form validation helpers
7931
+ isFieldInvalid(fieldName) {
7932
+ const field = this.createForm.get(fieldName);
7933
+ return !!(field && field.invalid && field.touched);
7934
+ }
7935
+ getFieldError(fieldName) {
7936
+ const field = this.createForm.get(fieldName);
7937
+ if (!field || !field.errors)
7938
+ return '';
7939
+ if (field.errors['required']) {
7940
+ return `${this.getFieldDisplayName(fieldName)} is required`;
7941
+ }
7942
+ if (field.errors['maxlength']) {
7943
+ return `${this.getFieldDisplayName(fieldName)} must be less than ${field.errors['maxlength'].requiredLength} characters`;
7944
+ }
7945
+ if (field.errors['min']) {
7946
+ return `${this.getFieldDisplayName(fieldName)} must be at least ${field.errors['min'].min}`;
7947
+ }
7948
+ if (field.errors['max']) {
7949
+ return `${this.getFieldDisplayName(fieldName)} must be no more than ${field.errors['max'].max}`;
7950
+ }
7951
+ return 'Invalid value';
7952
+ }
7953
+ getFieldDisplayName(fieldName) {
7954
+ const displayNames = {
7955
+ 'poolName': 'Pool name',
7956
+ 'poolDescription': 'Description',
7957
+ 'range_start': 'Range start',
7958
+ 'range_end': 'Range end'
7959
+ };
7960
+ return displayNames[fieldName] || fieldName;
7961
+ }
7962
+ // Pagination
7963
+ goToPage(page) {
7964
+ if (page >= 1 && page <= this.totalPages) {
7965
+ this.currentPage.set(page);
7966
+ }
7967
+ }
7968
+ previousPage() {
7969
+ if (this.currentPage() > 1) {
7970
+ this.currentPage.set(this.currentPage() - 1);
7971
+ }
7972
+ }
7973
+ nextPage() {
7974
+ if (this.currentPage() < this.totalPages) {
7975
+ this.currentPage.set(this.currentPage() + 1);
7976
+ }
7977
+ }
7978
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.7", ngImport: i0, type: SamplePoolCreateModal, deps: [{ token: i1.FormBuilder }, { token: i2.NgbActiveModal }, { token: SamplePoolService }, { token: i3.ToastService }], target: i0.ɵɵFactoryTarget.Component });
7979
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.7", type: SamplePoolCreateModal, isStandalone: true, selector: "ccv-sample-pool-create-modal", inputs: { metadataTable: "metadataTable" }, outputs: { poolCreated: "poolCreated" }, ngImport: i0, template: "<div class=\"modal-header\">\n <h4 class=\"modal-title\">\n <i class=\"bi bi-plus-circle me-2\"></i>Create Sample Pool\n </h4>\n <button type=\"button\" class=\"btn-close\" (click)=\"onCancel()\"></button>\n</div>\n\n<div class=\"modal-body\" style=\"max-height: 70vh; overflow-y: auto;\">\n <form [formGroup]=\"createForm\" (ngSubmit)=\"onSubmit()\">\n \n <!-- Pool Basic Info -->\n <div class=\"row mb-4\">\n <div class=\"col-12\">\n <h6 class=\"text-primary mb-3\">\n <i class=\"bi bi-info-circle me-2\"></i>Pool Information\n </h6>\n </div>\n \n <!-- Pool Name -->\n <div class=\"col-md-6 mb-3\">\n <label for=\"poolName\" class=\"form-label\">Pool Name *</label>\n <input \n type=\"text\" \n class=\"form-control\"\n id=\"poolName\"\n formControlName=\"poolName\"\n [class.is-invalid]=\"isFieldInvalid('poolName')\"\n placeholder=\"Enter pool name\">\n @if (isFieldInvalid('poolName')) {\n <div class=\"invalid-feedback\">\n {{ getFieldError('poolName') }}\n </div>\n }\n </div>\n\n\n <!-- Description -->\n <div class=\"col-12 mb-3\">\n <label for=\"poolDescription\" class=\"form-label\">Description</label>\n <textarea \n class=\"form-control\"\n id=\"poolDescription\"\n formControlName=\"poolDescription\"\n rows=\"2\"\n placeholder=\"Optional pool description\"></textarea>\n </div>\n\n <!-- Reference Pool Checkbox -->\n <div class=\"col-12 mb-3\">\n <div class=\"form-check\">\n <input \n class=\"form-check-input\" \n type=\"checkbox\" \n id=\"isReference\" \n formControlName=\"isReference\">\n <label class=\"form-check-label\" for=\"isReference\">\n <strong>Reference Pool</strong>\n <div class=\"form-text\">Check if this is a reference pool</div>\n </label>\n </div>\n </div>\n </div>\n\n <!-- Sample Selection Methods -->\n <div class=\"mb-4\">\n <h6 class=\"text-primary mb-3\">\n <i class=\"bi bi-check2-square me-2\"></i>Sample Selection\n </h6>\n\n <!-- Selection Mode Tabs -->\n <div class=\"btn-group mb-3\" role=\"group\">\n <input type=\"radio\" class=\"btn-check\" id=\"manualMode\" [value]=\"'manual'\" \n [checked]=\"selectionMode() === 'manual'\" (click)=\"selectionMode.set('manual')\">\n <label class=\"btn btn-outline-primary\" for=\"manualMode\">\n <i class=\"bi bi-hand-index me-1\"></i>Manual Selection\n </label>\n\n <input type=\"radio\" class=\"btn-check\" id=\"rangeMode\" [value]=\"'range'\" \n [checked]=\"selectionMode() === 'range'\" (click)=\"selectionMode.set('range')\">\n <label class=\"btn btn-outline-primary\" for=\"rangeMode\">\n <i class=\"bi bi-sliders me-1\"></i>Range Selection\n </label>\n </div>\n\n <!-- Range Selection Controls -->\n @if (selectionMode() === 'range') {\n <div class=\"card mb-3\">\n <div class=\"card-body\">\n <div class=\"row align-items-end\">\n <div class=\"col-md-4\">\n <label for=\"rangeStart\" class=\"form-label\">Start Sample</label>\n <input \n type=\"number\" \n class=\"form-control\"\n id=\"rangeStart\"\n formControlName=\"range_start\"\n min=\"1\"\n [max]=\"metadataTable.sampleCount\"\n [class.is-invalid]=\"isFieldInvalid('range_start')\">\n @if (isFieldInvalid('range_start')) {\n <div class=\"invalid-feedback\">\n {{ getFieldError('range_start') }}\n </div>\n }\n </div>\n <div class=\"col-md-4\">\n <label for=\"rangeEnd\" class=\"form-label\">End Sample</label>\n <input \n type=\"number\" \n class=\"form-control\"\n id=\"rangeEnd\"\n formControlName=\"range_end\"\n min=\"1\"\n [max]=\"metadataTable.sampleCount\"\n [class.is-invalid]=\"isFieldInvalid('range_end')\">\n @if (isFieldInvalid('range_end')) {\n <div class=\"invalid-feedback\">\n {{ getFieldError('range_end') }}\n </div>\n }\n </div>\n <div class=\"col-md-4\">\n <button type=\"button\" class=\"btn btn-success w-100\" (click)=\"selectSamplesInRange()\">\n <i class=\"bi bi-plus-square me-1\"></i>Select Range\n </button>\n </div>\n </div>\n <div class=\"form-text mt-2\">\n Select samples from {{ createForm.get('range_start')?.value || 1 }} \n to {{ createForm.get('range_end')?.value || 1 }} \n ({{ Math.abs((createForm.get('range_end')?.value || 1) - (createForm.get('range_start')?.value || 1)) + 1 }} samples)\n </div>\n </div>\n </div>\n }\n\n <!-- Sample Grid for Manual Selection -->\n @if (selectionMode() === 'manual') {\n <div class=\"card mb-3\">\n <div class=\"card-header d-flex justify-content-between align-items-center\">\n <span>\n <i class=\"bi bi-grid me-2\"></i>Select Samples \n <span class=\"badge bg-info ms-2\">{{ selectedSamplesCount }} selected</span>\n </span>\n <button type=\"button\" class=\"btn btn-sm btn-outline-danger\" (click)=\"clearAllSelections()\">\n <i class=\"bi bi-x-circle me-1\"></i>Clear All\n </button>\n </div>\n <div class=\"card-body\" style=\"max-height: 300px; overflow-y: auto;\">\n <!-- Sample Grid -->\n <div class=\"row g-1 mb-3\">\n @for (sample of paginatedSamples; track sample.sampleNumber) {\n <div class=\"col-2\">\n <button \n type=\"button\" \n class=\"btn btn-sm w-100\"\n [class.btn-primary]=\"sample.selected && !sample.pooled\"\n [class.btn-success]=\"sample.selected && sample.pooled\"\n [class.btn-outline-secondary]=\"!sample.selected && !sample.pooled\"\n [class.btn-outline-warning]=\"!sample.selected && sample.pooled\"\n [disabled]=\"sample.pooled\"\n (click)=\"toggleSampleSelection(sample.sampleNumber)\"\n [title]=\"sample.pooled ? 'Sample already in another pool' : 'Click to select/deselect'\">\n {{ sample.sampleNumber }}\n @if (sample.pooled) {\n <i class=\"bi bi-lock-fill ms-1\"></i>\n }\n </button>\n </div>\n }\n </div>\n\n <!-- Pagination -->\n @if (totalPages > 1) {\n <nav>\n <ul class=\"pagination pagination-sm justify-content-center\">\n <li class=\"page-item\" [class.disabled]=\"currentPage() === 1\">\n <button class=\"page-link\" type=\"button\" (click)=\"previousPage()\">Previous</button>\n </li>\n @for (page of [].constructor(Math.min(5, totalPages)); track $index) {\n @let pageNum = $index + Math.max(1, currentPage() - 2);\n @if (pageNum <= totalPages) {\n <li class=\"page-item\" [class.active]=\"pageNum === currentPage()\">\n <button class=\"page-link\" type=\"button\" (click)=\"goToPage(pageNum)\">{{ pageNum }}</button>\n </li>\n }\n }\n <li class=\"page-item\" [class.disabled]=\"currentPage() === totalPages\">\n <button class=\"page-link\" type=\"button\" (click)=\"nextPage()\">Next</button>\n </li>\n </ul>\n </nav>\n }\n\n <div class=\"form-text text-center\">\n Showing {{ ((currentPage() - 1) * samplesPerPage) + 1 }} - \n {{ Math.min(currentPage() * samplesPerPage, samples().length) }} \n of {{ samples().length }} samples\n </div>\n </div>\n </div>\n }\n\n <!-- Selection Summary -->\n @if (selectedSamplesCount > 0) {\n <div class=\"alert alert-info\">\n <div class=\"d-flex justify-content-between align-items-center\">\n <span>\n <i class=\"bi bi-info-circle me-2\"></i>\n <strong>{{ selectedSamplesCount }}</strong> samples selected\n </span>\n <div class=\"btn-group btn-group-sm\">\n <button type=\"button\" class=\"btn btn-outline-primary\" (click)=\"assignToPooledOnly()\">\n <i class=\"bi bi-arrow-right me-1\"></i>Pooled Only\n </button>\n <button type=\"button\" class=\"btn btn-outline-success\" (click)=\"assignToPooledAndIndependent()\">\n <i class=\"bi bi-arrow-right me-1\"></i>Pooled & Independent\n </button>\n </div>\n </div>\n </div>\n }\n </div>\n\n <!-- Sample Configuration -->\n <div class=\"mb-4\">\n <h6 class=\"text-primary mb-3\">\n <i class=\"bi bi-gear me-2\"></i>Sample Configuration\n </h6>\n \n <!-- Pooled Only Samples -->\n <div class=\"mb-3\">\n <label for=\"pooledOnlySamples\" class=\"form-label\">\n Pooled Only Samples\n <span class=\"badge bg-primary ms-2\">{{ parseSampleNumbers(createForm.get('pooled_only_samples_text')?.value || '').length }} samples</span>\n </label>\n <input \n type=\"text\" \n class=\"form-control\"\n id=\"pooledOnlySamples\"\n formControlName=\"pooled_only_samples_text\"\n placeholder=\"e.g., 1, 2, 3, 5-8\">\n <div class=\"form-text\">\n Enter sample numbers separated by commas. Ranges like \"5-8\" are supported.\n </div>\n </div>\n\n <!-- Pooled and Independent Samples -->\n <div class=\"mb-3\">\n <label for=\"pooledAndIndependentSamples\" class=\"form-label\">\n Pooled & Independent Samples\n <span class=\"badge bg-success ms-2\">{{ parseSampleNumbers(createForm.get('pooled_and_independent_samples_text')?.value || '').length }} samples</span>\n </label>\n <input \n type=\"text\" \n class=\"form-control\"\n id=\"pooledAndIndependentSamples\"\n formControlName=\"pooled_and_independent_samples_text\"\n placeholder=\"e.g., 4, 9, 10-15\">\n <div class=\"form-text\">\n Enter sample numbers that are both pooled and run independently.\n </div>\n </div>\n\n <!-- Total Count Display -->\n <div class=\"alert\" \n [class.alert-success]=\"totalPooledSamplesCount > 0 && totalPooledSamplesCount <= metadataTable.sampleCount\"\n [class.alert-warning]=\"totalPooledSamplesCount === 0\"\n [class.alert-danger]=\"totalPooledSamplesCount > metadataTable.sampleCount\">\n <i class=\"bi bi-info-circle me-2\"></i>\n <strong>Total Samples:</strong> {{ totalPooledSamplesCount }}\n @if (totalPooledSamplesCount > metadataTable.sampleCount) {\n <span class=\"text-danger ms-2\">\n (Exceeds maximum of {{ metadataTable.sampleCount }})\n </span>\n } @else if (totalPooledSamplesCount === 0) {\n <span class=\"text-warning ms-2\">\n (Please select at least one sample)\n </span>\n }\n </div>\n </div>\n\n <!-- Form Status -->\n <div class=\"d-flex justify-content-between align-items-center mb-2\">\n <small class=\"text-muted\">\n Name: {{ createForm.get('poolName')?.value?.length || 0 }}/255 characters\n </small>\n @if (totalPooledSamplesCount > 0) {\n <small class=\"text-success\">\n <i class=\"bi bi-check-circle me-1\"></i>Ready to create\n </small>\n }\n </div>\n\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]=\"createForm.invalid || isLoading() || totalPooledSamplesCount === 0 || totalPooledSamplesCount > metadataTable.sampleCount\">\n @if (isLoading()) {\n <span class=\"spinner-border spinner-border-sm me-2\"></span>\n }\n <i class=\"bi bi-plus-circle me-1\"></i>\n Create Pool\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.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.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.MaxValidator, selector: "input[type=number][max][formControlName],input[type=number][max][formControl],input[type=number][max][ngModel]", inputs: ["max"] }, { 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 }] });
7980
+ }
7981
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.7", ngImport: i0, type: SamplePoolCreateModal, decorators: [{
7982
+ type: Component,
7983
+ args: [{ selector: 'ccv-sample-pool-create-modal', standalone: true, imports: [CommonModule, ReactiveFormsModule, NgbModule], template: "<div class=\"modal-header\">\n <h4 class=\"modal-title\">\n <i class=\"bi bi-plus-circle me-2\"></i>Create Sample Pool\n </h4>\n <button type=\"button\" class=\"btn-close\" (click)=\"onCancel()\"></button>\n</div>\n\n<div class=\"modal-body\" style=\"max-height: 70vh; overflow-y: auto;\">\n <form [formGroup]=\"createForm\" (ngSubmit)=\"onSubmit()\">\n \n <!-- Pool Basic Info -->\n <div class=\"row mb-4\">\n <div class=\"col-12\">\n <h6 class=\"text-primary mb-3\">\n <i class=\"bi bi-info-circle me-2\"></i>Pool Information\n </h6>\n </div>\n \n <!-- Pool Name -->\n <div class=\"col-md-6 mb-3\">\n <label for=\"poolName\" class=\"form-label\">Pool Name *</label>\n <input \n type=\"text\" \n class=\"form-control\"\n id=\"poolName\"\n formControlName=\"poolName\"\n [class.is-invalid]=\"isFieldInvalid('poolName')\"\n placeholder=\"Enter pool name\">\n @if (isFieldInvalid('poolName')) {\n <div class=\"invalid-feedback\">\n {{ getFieldError('poolName') }}\n </div>\n }\n </div>\n\n\n <!-- Description -->\n <div class=\"col-12 mb-3\">\n <label for=\"poolDescription\" class=\"form-label\">Description</label>\n <textarea \n class=\"form-control\"\n id=\"poolDescription\"\n formControlName=\"poolDescription\"\n rows=\"2\"\n placeholder=\"Optional pool description\"></textarea>\n </div>\n\n <!-- Reference Pool Checkbox -->\n <div class=\"col-12 mb-3\">\n <div class=\"form-check\">\n <input \n class=\"form-check-input\" \n type=\"checkbox\" \n id=\"isReference\" \n formControlName=\"isReference\">\n <label class=\"form-check-label\" for=\"isReference\">\n <strong>Reference Pool</strong>\n <div class=\"form-text\">Check if this is a reference pool</div>\n </label>\n </div>\n </div>\n </div>\n\n <!-- Sample Selection Methods -->\n <div class=\"mb-4\">\n <h6 class=\"text-primary mb-3\">\n <i class=\"bi bi-check2-square me-2\"></i>Sample Selection\n </h6>\n\n <!-- Selection Mode Tabs -->\n <div class=\"btn-group mb-3\" role=\"group\">\n <input type=\"radio\" class=\"btn-check\" id=\"manualMode\" [value]=\"'manual'\" \n [checked]=\"selectionMode() === 'manual'\" (click)=\"selectionMode.set('manual')\">\n <label class=\"btn btn-outline-primary\" for=\"manualMode\">\n <i class=\"bi bi-hand-index me-1\"></i>Manual Selection\n </label>\n\n <input type=\"radio\" class=\"btn-check\" id=\"rangeMode\" [value]=\"'range'\" \n [checked]=\"selectionMode() === 'range'\" (click)=\"selectionMode.set('range')\">\n <label class=\"btn btn-outline-primary\" for=\"rangeMode\">\n <i class=\"bi bi-sliders me-1\"></i>Range Selection\n </label>\n </div>\n\n <!-- Range Selection Controls -->\n @if (selectionMode() === 'range') {\n <div class=\"card mb-3\">\n <div class=\"card-body\">\n <div class=\"row align-items-end\">\n <div class=\"col-md-4\">\n <label for=\"rangeStart\" class=\"form-label\">Start Sample</label>\n <input \n type=\"number\" \n class=\"form-control\"\n id=\"rangeStart\"\n formControlName=\"range_start\"\n min=\"1\"\n [max]=\"metadataTable.sampleCount\"\n [class.is-invalid]=\"isFieldInvalid('range_start')\">\n @if (isFieldInvalid('range_start')) {\n <div class=\"invalid-feedback\">\n {{ getFieldError('range_start') }}\n </div>\n }\n </div>\n <div class=\"col-md-4\">\n <label for=\"rangeEnd\" class=\"form-label\">End Sample</label>\n <input \n type=\"number\" \n class=\"form-control\"\n id=\"rangeEnd\"\n formControlName=\"range_end\"\n min=\"1\"\n [max]=\"metadataTable.sampleCount\"\n [class.is-invalid]=\"isFieldInvalid('range_end')\">\n @if (isFieldInvalid('range_end')) {\n <div class=\"invalid-feedback\">\n {{ getFieldError('range_end') }}\n </div>\n }\n </div>\n <div class=\"col-md-4\">\n <button type=\"button\" class=\"btn btn-success w-100\" (click)=\"selectSamplesInRange()\">\n <i class=\"bi bi-plus-square me-1\"></i>Select Range\n </button>\n </div>\n </div>\n <div class=\"form-text mt-2\">\n Select samples from {{ createForm.get('range_start')?.value || 1 }} \n to {{ createForm.get('range_end')?.value || 1 }} \n ({{ Math.abs((createForm.get('range_end')?.value || 1) - (createForm.get('range_start')?.value || 1)) + 1 }} samples)\n </div>\n </div>\n </div>\n }\n\n <!-- Sample Grid for Manual Selection -->\n @if (selectionMode() === 'manual') {\n <div class=\"card mb-3\">\n <div class=\"card-header d-flex justify-content-between align-items-center\">\n <span>\n <i class=\"bi bi-grid me-2\"></i>Select Samples \n <span class=\"badge bg-info ms-2\">{{ selectedSamplesCount }} selected</span>\n </span>\n <button type=\"button\" class=\"btn btn-sm btn-outline-danger\" (click)=\"clearAllSelections()\">\n <i class=\"bi bi-x-circle me-1\"></i>Clear All\n </button>\n </div>\n <div class=\"card-body\" style=\"max-height: 300px; overflow-y: auto;\">\n <!-- Sample Grid -->\n <div class=\"row g-1 mb-3\">\n @for (sample of paginatedSamples; track sample.sampleNumber) {\n <div class=\"col-2\">\n <button \n type=\"button\" \n class=\"btn btn-sm w-100\"\n [class.btn-primary]=\"sample.selected && !sample.pooled\"\n [class.btn-success]=\"sample.selected && sample.pooled\"\n [class.btn-outline-secondary]=\"!sample.selected && !sample.pooled\"\n [class.btn-outline-warning]=\"!sample.selected && sample.pooled\"\n [disabled]=\"sample.pooled\"\n (click)=\"toggleSampleSelection(sample.sampleNumber)\"\n [title]=\"sample.pooled ? 'Sample already in another pool' : 'Click to select/deselect'\">\n {{ sample.sampleNumber }}\n @if (sample.pooled) {\n <i class=\"bi bi-lock-fill ms-1\"></i>\n }\n </button>\n </div>\n }\n </div>\n\n <!-- Pagination -->\n @if (totalPages > 1) {\n <nav>\n <ul class=\"pagination pagination-sm justify-content-center\">\n <li class=\"page-item\" [class.disabled]=\"currentPage() === 1\">\n <button class=\"page-link\" type=\"button\" (click)=\"previousPage()\">Previous</button>\n </li>\n @for (page of [].constructor(Math.min(5, totalPages)); track $index) {\n @let pageNum = $index + Math.max(1, currentPage() - 2);\n @if (pageNum <= totalPages) {\n <li class=\"page-item\" [class.active]=\"pageNum === currentPage()\">\n <button class=\"page-link\" type=\"button\" (click)=\"goToPage(pageNum)\">{{ pageNum }}</button>\n </li>\n }\n }\n <li class=\"page-item\" [class.disabled]=\"currentPage() === totalPages\">\n <button class=\"page-link\" type=\"button\" (click)=\"nextPage()\">Next</button>\n </li>\n </ul>\n </nav>\n }\n\n <div class=\"form-text text-center\">\n Showing {{ ((currentPage() - 1) * samplesPerPage) + 1 }} - \n {{ Math.min(currentPage() * samplesPerPage, samples().length) }} \n of {{ samples().length }} samples\n </div>\n </div>\n </div>\n }\n\n <!-- Selection Summary -->\n @if (selectedSamplesCount > 0) {\n <div class=\"alert alert-info\">\n <div class=\"d-flex justify-content-between align-items-center\">\n <span>\n <i class=\"bi bi-info-circle me-2\"></i>\n <strong>{{ selectedSamplesCount }}</strong> samples selected\n </span>\n <div class=\"btn-group btn-group-sm\">\n <button type=\"button\" class=\"btn btn-outline-primary\" (click)=\"assignToPooledOnly()\">\n <i class=\"bi bi-arrow-right me-1\"></i>Pooled Only\n </button>\n <button type=\"button\" class=\"btn btn-outline-success\" (click)=\"assignToPooledAndIndependent()\">\n <i class=\"bi bi-arrow-right me-1\"></i>Pooled & Independent\n </button>\n </div>\n </div>\n </div>\n }\n </div>\n\n <!-- Sample Configuration -->\n <div class=\"mb-4\">\n <h6 class=\"text-primary mb-3\">\n <i class=\"bi bi-gear me-2\"></i>Sample Configuration\n </h6>\n \n <!-- Pooled Only Samples -->\n <div class=\"mb-3\">\n <label for=\"pooledOnlySamples\" class=\"form-label\">\n Pooled Only Samples\n <span class=\"badge bg-primary ms-2\">{{ parseSampleNumbers(createForm.get('pooled_only_samples_text')?.value || '').length }} samples</span>\n </label>\n <input \n type=\"text\" \n class=\"form-control\"\n id=\"pooledOnlySamples\"\n formControlName=\"pooled_only_samples_text\"\n placeholder=\"e.g., 1, 2, 3, 5-8\">\n <div class=\"form-text\">\n Enter sample numbers separated by commas. Ranges like \"5-8\" are supported.\n </div>\n </div>\n\n <!-- Pooled and Independent Samples -->\n <div class=\"mb-3\">\n <label for=\"pooledAndIndependentSamples\" class=\"form-label\">\n Pooled & Independent Samples\n <span class=\"badge bg-success ms-2\">{{ parseSampleNumbers(createForm.get('pooled_and_independent_samples_text')?.value || '').length }} samples</span>\n </label>\n <input \n type=\"text\" \n class=\"form-control\"\n id=\"pooledAndIndependentSamples\"\n formControlName=\"pooled_and_independent_samples_text\"\n placeholder=\"e.g., 4, 9, 10-15\">\n <div class=\"form-text\">\n Enter sample numbers that are both pooled and run independently.\n </div>\n </div>\n\n <!-- Total Count Display -->\n <div class=\"alert\" \n [class.alert-success]=\"totalPooledSamplesCount > 0 && totalPooledSamplesCount <= metadataTable.sampleCount\"\n [class.alert-warning]=\"totalPooledSamplesCount === 0\"\n [class.alert-danger]=\"totalPooledSamplesCount > metadataTable.sampleCount\">\n <i class=\"bi bi-info-circle me-2\"></i>\n <strong>Total Samples:</strong> {{ totalPooledSamplesCount }}\n @if (totalPooledSamplesCount > metadataTable.sampleCount) {\n <span class=\"text-danger ms-2\">\n (Exceeds maximum of {{ metadataTable.sampleCount }})\n </span>\n } @else if (totalPooledSamplesCount === 0) {\n <span class=\"text-warning ms-2\">\n (Please select at least one sample)\n </span>\n }\n </div>\n </div>\n\n <!-- Form Status -->\n <div class=\"d-flex justify-content-between align-items-center mb-2\">\n <small class=\"text-muted\">\n Name: {{ createForm.get('poolName')?.value?.length || 0 }}/255 characters\n </small>\n @if (totalPooledSamplesCount > 0) {\n <small class=\"text-success\">\n <i class=\"bi bi-check-circle me-1\"></i>Ready to create\n </small>\n }\n </div>\n\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]=\"createForm.invalid || isLoading() || totalPooledSamplesCount === 0 || totalPooledSamplesCount > metadataTable.sampleCount\">\n @if (isLoading()) {\n <span class=\"spinner-border spinner-border-sm me-2\"></span>\n }\n <i class=\"bi bi-plus-circle me-1\"></i>\n Create Pool\n </button>\n</div>\n" }]
7984
+ }], ctorParameters: () => [{ type: i1.FormBuilder }, { type: i2.NgbActiveModal }, { type: SamplePoolService }, { type: i3.ToastService }], propDecorators: { metadataTable: [{
7985
+ type: Input
7986
+ }], poolCreated: [{
7987
+ type: Output
7988
+ }] } });
7989
+
7990
+ class MetadataTableDetails {
7991
+ route;
7992
+ router;
7993
+ metadataTableService;
7994
+ asyncExportService;
7995
+ chunkedUploadService;
7996
+ samplePoolService;
7997
+ metadataColumnService;
7998
+ toastService;
7999
+ asyncTaskService;
8000
+ modalService;
8001
+ destroy$ = new Subject();
8002
+ fileInput;
8003
+ excelInput;
8004
+ Math = Math;
8005
+ onColumnFilterChange(value) {
8006
+ this.columnFilter.set(value);
8007
+ }
8008
+ clearColumnFilter() {
8009
+ this.columnFilter.set('');
8010
+ }
8011
+ get filteredColumnCount() {
8012
+ return this.sortedColumns().length;
8013
+ }
8014
+ get totalColumnCount() {
8015
+ const columns = this.table()?.columns || [];
8016
+ return columns.filter(col => col && col.name).length;
8017
+ }
8018
+ sortBy(field) {
8019
+ const currentField = this.sortField();
8020
+ const currentDirection = this.sortDirection();
8021
+ if (currentField === field) {
8022
+ this.sortDirection.set(currentDirection === 'asc' ? 'desc' : 'asc');
8023
+ }
8024
+ else {
8025
+ this.sortField.set(field);
8026
+ this.sortDirection.set('asc');
8027
+ }
8028
+ }
8029
+ getSortIcon(field) {
8030
+ const currentField = this.sortField();
8031
+ const currentDirection = this.sortDirection();
8032
+ if (currentField !== field) {
8033
+ return 'bi-arrow-down-up';
8034
+ }
8035
+ return currentDirection === 'asc' ? 'bi-sort-alpha-down' : 'bi-sort-alpha-up';
8036
+ }
8037
+ isSorted(field) {
8038
+ return this.sortField() === field;
8039
+ }
8040
+ isLoading = signal(false, ...(ngDevMode ? [{ debugName: "isLoading" }] : []));
8041
+ table = signal(null, ...(ngDevMode ? [{ debugName: "table" }] : []));
8042
+ tableId = signal(null, ...(ngDevMode ? [{ debugName: "tableId" }] : []));
8043
+ viewMode = signal('table', ...(ngDevMode ? [{ debugName: "viewMode" }] : []));
8044
+ currentPage = signal(1, ...(ngDevMode ? [{ debugName: "currentPage" }] : []));
8045
+ pageSize = signal(10, ...(ngDevMode ? [{ debugName: "pageSize" }] : []));
8046
+ columnFilter = signal('', ...(ngDevMode ? [{ debugName: "columnFilter" }] : []));
8047
+ sortField = signal('', ...(ngDevMode ? [{ debugName: "sortField" }] : []));
8048
+ sortDirection = signal('asc', ...(ngDevMode ? [{ debugName: "sortDirection" }] : []));
8049
+ hasColumns = computed(() => {
8050
+ const table = this.table();
8051
+ return table?.columns && table.columns.length > 0;
8052
+ }, ...(ngDevMode ? [{ debugName: "hasColumns" }] : []));
8053
+ hasPools = computed(() => {
8054
+ const table = this.table();
8055
+ return table?.samplePools && table.samplePools.length > 0;
8056
+ }, ...(ngDevMode ? [{ debugName: "hasPools" }] : []));
8057
+ sortedColumns = computed(() => {
8058
+ const columns = this.table()?.columns || [];
8059
+ const filter = this.columnFilter().toLowerCase();
8060
+ return [...columns]
8061
+ .filter(col => col && col.name)
8062
+ .filter(col => !filter || col.name.toLowerCase().includes(filter))
8063
+ .sort((a, b) => (a.columnPosition || 0) - (b.columnPosition || 0));
8064
+ }, ...(ngDevMode ? [{ debugName: "sortedColumns" }] : []));
8065
+ dragColumnsForList = signal([], ...(ngDevMode ? [{ debugName: "dragColumnsForList" }] : []));
8066
+ sortedPools = computed(() => {
8067
+ const pools = this.table()?.samplePools || [];
8068
+ return [...pools].sort((a, b) => a.poolName.localeCompare(b.poolName));
8069
+ }, ...(ngDevMode ? [{ debugName: "sortedPools" }] : []));
8070
+ tableRows = computed(() => {
8071
+ const table = this.table();
8072
+ if (!table || !this.hasColumns())
8073
+ return [];
8074
+ const rows = [];
8075
+ for (let i = 0; i < table.sampleCount; i++) {
8076
+ const sampleIndex = i + 1;
8077
+ const row = { _sampleIndex: sampleIndex };
8078
+ this.sortedColumns().forEach(column => {
8079
+ const value = this.getSampleColumnValue(column, sampleIndex);
8080
+ row[`col_${column.id}`] = value;
8081
+ });
8082
+ rows.push(row);
8083
+ }
8084
+ return rows;
8085
+ }, ...(ngDevMode ? [{ debugName: "tableRows" }] : []));
8086
+ poolTableRows = computed(() => {
8087
+ const table = this.table();
8088
+ if (!table || !this.hasPools() || !this.hasColumns())
8089
+ return [];
8090
+ const rows = [];
8091
+ this.sortedPools().forEach(pool => {
8092
+ const row = {
8093
+ _poolName: pool.poolName,
8094
+ _poolId: pool.id,
8095
+ _poolData: pool
8096
+ };
8097
+ this.sortedColumns().forEach(column => {
8098
+ let value = '';
8099
+ const poolColumn = pool.metadataColumns?.find((pc) => (pc.id && column.id && pc.id === column.id) || pc.name === column.name);
8100
+ if (poolColumn) {
8101
+ value = poolColumn.value || '';
8102
+ }
8103
+ row[`col_${column.id}`] = value;
8104
+ });
8105
+ rows.push(row);
8106
+ });
8107
+ return rows;
8108
+ }, ...(ngDevMode ? [{ debugName: "poolTableRows" }] : []));
8109
+ paginatedRows = computed(() => {
8110
+ const rows = this.tableRows();
8111
+ const page = this.currentPage();
8112
+ const size = this.pageSize();
8113
+ const start = (page - 1) * size;
8114
+ const end = start + size;
8115
+ return rows.slice(start, end);
8116
+ }, ...(ngDevMode ? [{ debugName: "paginatedRows" }] : []));
8117
+ totalPages = computed(() => {
8118
+ const rows = this.tableRows();
8119
+ const size = this.pageSize();
8120
+ return Math.ceil(rows.length / size);
8121
+ }, ...(ngDevMode ? [{ debugName: "totalPages" }] : []));
8122
+ constructor(route, router, metadataTableService, asyncExportService, chunkedUploadService, samplePoolService, metadataColumnService, toastService, asyncTaskService, modalService) {
8123
+ this.route = route;
8124
+ this.router = router;
8125
+ this.metadataTableService = metadataTableService;
8126
+ this.asyncExportService = asyncExportService;
8127
+ this.chunkedUploadService = chunkedUploadService;
8128
+ this.samplePoolService = samplePoolService;
8129
+ this.metadataColumnService = metadataColumnService;
8130
+ this.toastService = toastService;
8131
+ this.asyncTaskService = asyncTaskService;
8132
+ this.modalService = modalService;
8133
+ }
8134
+ ngOnInit() {
8135
+ this.route.params.subscribe(params => {
8136
+ const id = Number(params['id']);
8137
+ const mode = params['mode'] || 'list';
8138
+ if (id && !isNaN(id)) {
8139
+ this.tableId.set(id);
8140
+ this.viewMode.set(mode);
8141
+ this.loadTable(id);
8142
+ this.setupAsyncTaskRefreshListener(id);
8143
+ }
8144
+ else {
8145
+ this.toastService.error('Invalid table ID');
8146
+ this.navigateBack();
8147
+ }
8148
+ });
8149
+ }
8150
+ ngOnDestroy() {
8151
+ this.destroy$.next();
8152
+ this.destroy$.complete();
8153
+ }
8154
+ setupAsyncTaskRefreshListener(tableId) {
8155
+ this.asyncTaskService.metadataTableRefresh$.pipe(takeUntil$1(this.destroy$)).subscribe((refreshedTableId) => {
8156
+ if (refreshedTableId === tableId) {
8157
+ console.log(`Import task completed for table ${refreshedTableId}, refreshing table details`);
8158
+ this.toastService.info('Refreshing table data after successful import');
8159
+ this.loadTable(tableId);
8160
+ }
8161
+ });
8162
+ }
8163
+ loadTable(id) {
8164
+ this.isLoading.set(true);
8165
+ this.metadataTableService.getMetadataTable(id).subscribe({
8166
+ next: (table) => {
8167
+ this.isLoading.set(false);
8168
+ this.table.set(table);
8169
+ const naturalOrder = [...(table.columns || [])].sort((a, b) => (a.columnPosition || 0) - (b.columnPosition || 0));
8170
+ this.dragColumnsForList.set(naturalOrder);
8171
+ },
8172
+ error: (error) => {
8173
+ this.isLoading.set(false);
8174
+ console.error('Error loading metadata table:', error);
8175
+ const errorMsg = error?.error?.detail || 'Table not found';
8176
+ this.toastService.error(errorMsg);
8177
+ if (error.status === 404) {
8178
+ this.navigateBack();
8179
+ }
8180
+ }
8181
+ });
8182
+ }
8183
+ navigateBack() {
8184
+ this.router.navigate(['/metadata-tables']);
8185
+ }
8186
+ switchToListMode() {
8187
+ const tableId = this.tableId();
8188
+ if (tableId) {
8189
+ this.router.navigate(['/metadata-tables', tableId, 'list']);
8190
+ }
8191
+ }
8192
+ switchToTableMode() {
8193
+ const tableId = this.tableId();
8194
+ if (tableId) {
8195
+ this.router.navigate(['/metadata-tables', tableId, 'table']);
8196
+ }
8197
+ }
8198
+ onPageChange(page) {
8199
+ this.currentPage.set(page);
8200
+ }
8201
+ onPageSizeChange(size) {
8202
+ this.pageSize.set(size);
8203
+ this.currentPage.set(1); // Reset to first page when changing page size
8204
+ }
8205
+ editTable() {
8206
+ const table = this.table();
8207
+ if (!table || !table.canEdit)
8208
+ return;
8209
+ const modalRef = this.modalService.open(MetadataTableEditModal, {
8210
+ size: 'lg',
8211
+ centered: true
8212
+ });
8213
+ modalRef.componentInstance.table = table;
8214
+ modalRef.componentInstance.tableSaved.subscribe((updatedTable) => {
8215
+ // Update the local table data
8216
+ this.table.set(updatedTable);
8217
+ this.toastService.success(`Table "${updatedTable.name}" updated successfully!`);
8218
+ });
8219
+ modalRef.result.catch(() => {
8220
+ // Modal was dismissed - no action needed
8221
+ });
8222
+ }
8223
+ triggerFileInput() {
8224
+ console.log('triggerFileInput called');
8225
+ this.fileInput.nativeElement.click();
8226
+ }
8227
+ triggerExcelInput() {
8228
+ console.log('triggerExcelInput called');
8229
+ this.excelInput.nativeElement.click();
8230
+ }
8231
+ importSdrf(event) {
8232
+ console.log('importSdrf called', event);
8233
+ const input = event.target;
8234
+ if (!input.files || input.files.length === 0) {
8235
+ console.log('No files selected');
8236
+ return;
8237
+ }
8238
+ const file = input.files[0];
8239
+ const table = this.table();
8240
+ if (!table)
8241
+ return;
8242
+ // Validate file type
8243
+ if (!file.name.toLowerCase().endsWith('.txt') && !file.name.toLowerCase().endsWith('.tsv')) {
8244
+ this.toastService.error('Please select a valid SDRF file (.txt or .tsv)');
8245
+ input.value = ''; // Reset input
8246
+ return;
8247
+ }
8248
+ if (confirm(`Import SDRF data into table "${table.name}"?\n\nThis will add new columns and may modify existing data.`)) {
8249
+ input.value = ''; // Reset input early
8250
+ // Use chunked upload service for import with progress tracking
8251
+ this.isLoading.set(true);
8252
+ console.log('Starting SDRF import using chunked upload:', {
8253
+ fileName: file.name,
8254
+ fileSize: file.size,
8255
+ metadataTableId: table.id
8256
+ });
8257
+ // Use chunked upload - it now triggers async import task on completion
8258
+ this.chunkedUploadService.uploadFileInChunks(file, 1024 * 1024, // 1MB chunks
8259
+ {
8260
+ metadataTableId: table.id,
8261
+ createPools: true,
8262
+ replaceExisting: false,
8263
+ onProgress: (progress) => {
8264
+ console.log(`SDRF upload progress: ${Math.round(progress)}%`);
8265
+ }
8266
+ }).subscribe({
8267
+ next: (result) => {
8268
+ this.isLoading.set(false);
8269
+ console.log('SDRF chunked upload completed:', result);
8270
+ // If a taskId is returned, monitor the async import task
8271
+ if (result?.taskId) {
8272
+ console.log('Starting async task monitoring for taskId:', result.taskId);
8273
+ this.toastService.success(`SDRF import task queued successfully! Task ID: ${result.taskId}`);
8274
+ // Mark task for monitoring and start real-time updates (same pattern as export)
8275
+ this.asyncTaskService.monitorTask(result.taskId);
8276
+ this.asyncTaskService.startRealtimeUpdates();
8277
+ }
8278
+ else {
8279
+ // Fallback for immediate processing (backward compatibility)
8280
+ this.toastService.success('SDRF file imported successfully!');
8281
+ this.loadTable(table.id);
8282
+ }
8283
+ },
8284
+ error: (error) => {
8285
+ this.isLoading.set(false);
8286
+ console.error('Error importing SDRF:', error);
8287
+ const errorMsg = error?.error?.detail || error?.error?.message || 'Failed to import SDRF file';
8288
+ this.toastService.error(errorMsg);
8289
+ }
8290
+ });
8291
+ }
8292
+ else {
8293
+ input.value = ''; // Reset input if cancelled
8294
+ }
8295
+ }
8296
+ importExcel(event) {
8297
+ const input = event.target;
8298
+ if (!input.files || input.files.length === 0)
8299
+ return;
8300
+ const file = input.files[0];
8301
+ const table = this.table();
8302
+ if (!table)
8303
+ return;
8304
+ // Validate file type
8305
+ if (!file.name.toLowerCase().endsWith('.xlsx') && !file.name.toLowerCase().endsWith('.xls')) {
8306
+ this.toastService.error('Please select a valid Excel file (.xlsx or .xls)');
8307
+ input.value = ''; // Reset input
8308
+ return;
8309
+ }
8310
+ if (confirm(`Import Excel data into table "${table.name}"?\n\nThis will update existing columns and may modify data.`)) {
8311
+ input.value = ''; // Reset input early
8312
+ // Use chunked upload service for import with progress tracking
8313
+ this.isLoading.set(true);
8314
+ console.log('Starting Excel import using chunked upload:', {
8315
+ fileName: file.name,
8316
+ fileSize: file.size,
8317
+ metadataTableId: table.id
8318
+ });
8319
+ // Use chunked upload - it now triggers async import task on completion
8320
+ this.chunkedUploadService.uploadFileInChunks(file, 1024 * 1024, // 1MB chunks
8321
+ {
8322
+ metadataTableId: table.id,
8323
+ createPools: true,
8324
+ replaceExisting: false,
8325
+ onProgress: (progress) => {
8326
+ console.log(`Excel upload progress: ${Math.round(progress)}%`);
8327
+ }
8328
+ }).subscribe({
8329
+ next: (result) => {
8330
+ this.isLoading.set(false);
8331
+ console.log('Excel chunked upload completed:', result);
8332
+ // If a taskId is returned, monitor the async import task
8333
+ if (result?.taskId) {
8334
+ console.log('Starting async task monitoring for taskId:', result.taskId);
8335
+ this.toastService.success(`Excel import task queued successfully! Task ID: ${result.taskId}`);
8336
+ // Mark task for monitoring and start real-time updates (same pattern as export)
8337
+ this.asyncTaskService.monitorTask(result.taskId);
8338
+ this.asyncTaskService.startRealtimeUpdates();
8339
+ }
8340
+ else {
8341
+ // Fallback for immediate processing (backward compatibility)
8342
+ this.toastService.success('Excel file imported successfully!');
8343
+ this.loadTable(table.id);
8344
+ }
8345
+ },
8346
+ error: (error) => {
8347
+ this.isLoading.set(false);
8348
+ console.error('Error importing Excel:', error);
8349
+ const errorMsg = error?.error?.detail || error?.error?.message || 'Failed to import Excel file';
8350
+ this.toastService.error(errorMsg);
8351
+ }
8352
+ });
8353
+ }
8354
+ else {
8355
+ input.value = ''; // Reset input if cancelled
8356
+ }
8357
+ }
8358
+ exportToSdrf(format = 'sdrf') {
8359
+ const table = this.table();
8360
+ if (!table || !table.columns || table.columns.length === 0) {
8361
+ this.toastService.error('No columns available for export');
8362
+ return;
8363
+ }
8364
+ if (format === 'excel') {
8365
+ // Show Excel export options modal
8366
+ const modalRef = this.modalService.open(ExcelExportModalComponent, {
8367
+ size: 'lg',
8368
+ backdrop: 'static'
8369
+ });
8370
+ modalRef.result.then((options) => {
8371
+ this.performExcelExport(table, options);
8372
+ }).catch(() => {
8373
+ // Modal was dismissed - no action needed
8374
+ });
8375
+ }
8376
+ else {
8377
+ // Direct SDRF export
8378
+ this.performSdrfExport(table);
8379
+ }
8380
+ }
8381
+ performExcelExport(table, options) {
8382
+ const columnIds = table.columns.map(col => col.id);
8383
+ // Prepare lab group IDs based on options
8384
+ let labGroupIds = undefined;
8385
+ if (options.includeLabGroups === 'selected') {
8386
+ labGroupIds = options.selectedLabGroupIds;
8387
+ }
8388
+ else if (options.includeLabGroups === 'all') {
8389
+ // Pass empty array to signal "include all lab groups"
8390
+ labGroupIds = [];
8391
+ }
8392
+ // For 'none', labGroupIds stays undefined
8393
+ // Use async export only
8394
+ this.asyncExportService.excelTemplate({
8395
+ metadataTableId: table.id,
8396
+ metadataColumnIds: columnIds,
8397
+ sampleNumber: table.sampleCount,
8398
+ exportFormat: 'excel',
8399
+ includePools: options.includePools,
8400
+ labGroupIds: labGroupIds
8401
+ }).subscribe({
8402
+ next: (response) => {
8403
+ this.toastService.success(`Excel export queued successfully! Task ID: ${response.taskId}`);
8404
+ // Start monitoring tasks if not already started
8405
+ this.asyncTaskService.startRealtimeUpdates();
8406
+ },
8407
+ error: (error) => {
8408
+ console.error('Error queuing Excel export:', error);
8409
+ const errorMsg = error?.error?.detail || error?.error?.message || 'Failed to queue Excel export';
8410
+ this.toastService.error(errorMsg);
8411
+ }
8412
+ });
8413
+ }
8414
+ performSdrfExport(table) {
8415
+ const columnIds = table.columns.map(col => col.id);
8416
+ // Use async export only
8417
+ this.asyncExportService.sdrfFile({
8418
+ metadataTableId: table.id,
8419
+ metadataColumnIds: columnIds,
8420
+ sampleNumber: table.sampleCount,
8421
+ exportFormat: 'sdrf',
8422
+ includePools: true
8423
+ }).subscribe({
8424
+ next: (response) => {
8425
+ this.toastService.success(`SDRF export queued successfully! Task ID: ${response.taskId}`);
8426
+ // Start monitoring tasks if not already started
8427
+ this.asyncTaskService.startRealtimeUpdates();
8428
+ },
8429
+ error: (error) => {
8430
+ console.error('Error queuing SDRF export:', error);
8431
+ const errorMsg = error?.error?.detail || error?.error?.message || 'Failed to queue SDRF export';
8432
+ this.toastService.error(errorMsg);
8433
+ }
8434
+ });
8435
+ }
8436
+ handleExportRequest(exportRequest, format) {
8437
+ exportRequest.subscribe({
8438
+ next: (response) => {
8439
+ this.isLoading.set(false);
8440
+ this.toastService.success(`${format.toUpperCase()} export task queued successfully! Task ID: ${response.taskId}`);
8441
+ // Start monitoring tasks if not already started
8442
+ this.asyncTaskService.startRealtimeUpdates();
8443
+ },
8444
+ error: (error) => {
8445
+ this.isLoading.set(false);
8446
+ console.error('Error exporting table:', error);
8447
+ const errorMsg = error?.error?.detail || 'Failed to export table';
8448
+ this.toastService.error(errorMsg);
8449
+ }
8450
+ });
8451
+ }
8452
+ deleteTable() {
8453
+ const table = this.table();
8454
+ if (!table)
8455
+ return;
8456
+ const confirmMessage = `Are you sure you want to delete the table "${table.name}"?\n\nThis action cannot be undone.`;
8457
+ if (confirm(confirmMessage)) {
8458
+ this.isLoading.set(true);
8459
+ this.metadataTableService.deleteMetadataTable(table.id).subscribe({
8460
+ next: () => {
8461
+ this.toastService.success(`Table "${table.name}" deleted successfully!`);
8462
+ this.navigateBack();
8463
+ },
8464
+ error: (error) => {
8465
+ this.isLoading.set(false);
8466
+ console.error('Error deleting metadata table:', error);
8467
+ const errorMsg = error?.error?.detail || error?.message || 'Failed to delete table. Please try again.';
8468
+ this.toastService.error(errorMsg);
8469
+ }
8470
+ });
8471
+ }
8472
+ }
8473
+ getTableStatusBadge() {
8474
+ const table = this.table();
8475
+ if (!table)
8476
+ return '';
8477
+ if (table.isPublished)
8478
+ return 'Published';
8479
+ if (table.isLocked)
8480
+ return 'Locked';
8481
+ return 'Draft';
8482
+ }
8483
+ getTableStatusClass() {
8484
+ const table = this.table();
8485
+ if (!table)
8486
+ return '';
8487
+ if (table.isPublished)
8488
+ return 'bg-success';
8489
+ if (table.isLocked)
8490
+ return 'bg-warning';
8491
+ return 'bg-secondary';
8492
+ }
8493
+ getColumnTypeIcon(column) {
8494
+ const type = column.type?.toLowerCase() || '';
8495
+ if (type.includes(ColumnType.CHARACTERISTICS))
8496
+ return 'bi-tags';
8497
+ if (type.includes(ColumnType.FACTOR_VALUE.replace('_', ' ')))
8498
+ return 'bi-sliders';
8499
+ if (type.includes(ColumnType.COMMENT))
8500
+ return 'bi-chat-left-text';
8501
+ if (type.includes(ColumnType.SOURCE_NAME.replace('_', ' ')))
8502
+ return 'bi-diagram-3';
8503
+ if (type === ColumnType.SPECIAL)
8504
+ return 'bi-star';
8505
+ return 'bi-circle';
8506
+ }
8507
+ getColumnTypeClass(column) {
8508
+ const type = column.type?.toLowerCase() || '';
8509
+ if (type.includes(ColumnType.CHARACTERISTICS))
8510
+ return 'text-primary';
8511
+ if (type.includes(ColumnType.FACTOR_VALUE.replace('_', ' ')))
8512
+ return 'text-success';
8513
+ if (type.includes(ColumnType.COMMENT))
8514
+ return 'text-info';
8515
+ if (type.includes(ColumnType.SOURCE_NAME.replace('_', ' ')))
8516
+ return 'text-warning';
8517
+ if (type === ColumnType.SPECIAL)
8518
+ return 'text-danger';
8519
+ return 'text-muted';
8520
+ }
8521
+ getColumnHeaderClass(column) {
8522
+ let baseClass = '';
8523
+ const type = column.type?.toLowerCase() || '';
8524
+ if (type.includes(ColumnType.CHARACTERISTICS))
8525
+ baseClass = 'bg-primary-subtle text-primary-emphasis';
8526
+ else if (type.includes(ColumnType.FACTOR_VALUE.replace('_', ' ')))
8527
+ baseClass = 'bg-success-subtle text-success-emphasis';
8528
+ else if (type.includes(ColumnType.COMMENT))
8529
+ baseClass = 'bg-info-subtle text-info-emphasis';
8530
+ else if (type.includes(ColumnType.SOURCE_NAME.replace('_', ' ')))
8531
+ baseClass = 'bg-warning-subtle text-warning-emphasis';
8532
+ else if (type === ColumnType.SPECIAL)
8533
+ baseClass = 'bg-danger-subtle text-danger-emphasis';
8534
+ else
8535
+ baseClass = 'bg-body-secondary text-body';
8536
+ // Add hidden column styling
8537
+ if (column.hidden) {
8538
+ baseClass += ' opacity-50 text-decoration-line-through';
8539
+ }
8540
+ return baseClass;
8541
+ }
8542
+ getShortColumnName(column) {
8543
+ let name = column.name;
8544
+ // Handle different column types
8545
+ const type = column.type?.toLowerCase() || '';
8546
+ if (type.includes(ColumnType.CHARACTERISTICS)) {
8547
+ name = name.replace(/^characteristics\s*\[/i, '').replace(/\]$/, '');
8548
+ }
8549
+ else if (type.includes(ColumnType.FACTOR_VALUE.replace('_', ' '))) {
8550
+ name = name.replace(/^factor\s*value\s*\[/i, '').replace(/\]$/, '');
8551
+ }
8552
+ else if (type.includes(ColumnType.COMMENT)) {
8553
+ name = name.replace(/^comment\s*\[/i, '').replace(/\]$/, '');
8554
+ }
8555
+ else if (type === ColumnType.SPECIAL) {
8556
+ // Special columns have no prefix or brackets - use name as is
8557
+ return name;
8558
+ }
8559
+ // Remove any remaining square brackets for other types
8560
+ name = name.replace(/[\[\]]/g, '');
8561
+ // Clean up extra spaces
8562
+ name = name.trim();
8563
+ return name || column.name; // Fallback to original name if cleaning results in empty string
8564
+ }
8565
+ hasModifiers(column) {
8566
+ return !!(column.modifiers && Array.isArray(column.modifiers) && column.modifiers.length > 0);
8567
+ }
8568
+ getColumnValueDisplay(column) {
8569
+ const defaultValue = column.value || '';
8570
+ if (!this.hasModifiers(column)) {
8571
+ return defaultValue || 'No default value';
8572
+ }
8573
+ return defaultValue ? `Default: ${defaultValue}` : 'Default: (empty)';
8574
+ }
8575
+ getModifiersDisplay(column) {
8576
+ if (!this.hasModifiers(column)) {
8577
+ return [];
8578
+ }
8579
+ return column.modifiers.map(modifier => `Samples ${modifier.samples}: "${modifier.value}"`);
8580
+ }
8581
+ // Sample Pool Methods
8582
+ getPoolSampleCount(pool) {
8583
+ return pool.pooledOnlySamples.length + pool.pooledAndIndependentSamples.length;
8584
+ }
8585
+ getPoolSampleDisplay(pool) {
8586
+ const pooledOnly = pool.pooledOnlySamples.length;
8587
+ const pooledAndIndependent = pool.pooledAndIndependentSamples.length;
8588
+ const total = pooledOnly + pooledAndIndependent;
8589
+ if (pooledOnly > 0 && pooledAndIndependent > 0) {
8590
+ return `${total} samples (${pooledOnly} pool-only, ${pooledAndIndependent} shared)`;
8591
+ }
8592
+ else if (pooledOnly > 0) {
8593
+ return `${pooledOnly} pool-only samples`;
8594
+ }
8595
+ else if (pooledAndIndependent > 0) {
8596
+ return `${pooledAndIndependent} shared samples`;
8597
+ }
8598
+ else {
8599
+ return '0 samples';
8600
+ }
8601
+ }
8602
+ getPoolSamples(pool) {
8603
+ const allSamples = [...pool.pooledOnlySamples, ...pool.pooledAndIndependentSamples].sort((a, b) => a - b);
8604
+ if (allSamples.length === 0)
8605
+ return 'None';
8606
+ // Group consecutive numbers into ranges
8607
+ const ranges = [];
8608
+ let start = allSamples[0];
8609
+ let end = allSamples[0];
8610
+ for (let i = 1; i < allSamples.length; i++) {
8611
+ if (allSamples[i] === end + 1) {
8612
+ end = allSamples[i];
8613
+ }
8614
+ else {
8615
+ ranges.push(start === end ? start.toString() : `${start}-${end}`);
8616
+ start = end = allSamples[i];
8617
+ }
8618
+ }
8619
+ ranges.push(start === end ? start.toString() : `${start}-${end}`);
8620
+ return ranges.join(', ');
8621
+ }
8622
+ getPoolTypeIcon(pool) {
8623
+ return pool.isReference ? 'bi-star-fill' : 'bi-layers';
8624
+ }
8625
+ getPoolTypeClass(pool) {
8626
+ return pool.isReference ? 'text-warning' : 'text-primary';
8627
+ }
8628
+ getPoolTypeLabel(pool) {
8629
+ return pool.isReference ? 'Reference Pool' : 'Sample Pool';
8630
+ }
8631
+ viewPoolDetails(pool) {
8632
+ const modalRef = this.modalService.open(SamplePoolDetailsModal, {
8633
+ size: 'lg',
8634
+ centered: true
8635
+ });
8636
+ modalRef.componentInstance.pool = pool;
8637
+ }
8638
+ editPool(pool) {
8639
+ const table = this.table();
8640
+ if (!table || !table.canEdit)
8641
+ return;
8642
+ const modalRef = this.modalService.open(SamplePoolEditModal, {
8643
+ size: 'lg',
8644
+ centered: true
8645
+ });
8646
+ modalRef.componentInstance.pool = pool;
8647
+ modalRef.componentInstance.maxSampleCount = table.sampleCount;
8648
+ modalRef.componentInstance.poolSaved.subscribe((updatedPool) => {
8649
+ // Reload the entire table to sync all pool-related changes
8650
+ this.loadTable(table.id);
8651
+ this.toastService.success(`Pool "${updatedPool.poolName}" updated successfully!`);
8652
+ });
8653
+ }
8654
+ createPool() {
8655
+ const table = this.table();
8656
+ if (!table || !table.canEdit)
8657
+ return;
8658
+ const modalRef = this.modalService.open(SamplePoolCreateModal, {
8659
+ size: 'xl',
8660
+ centered: true,
8661
+ backdrop: 'static'
8662
+ });
8663
+ modalRef.componentInstance.metadataTable = table;
8664
+ modalRef.componentInstance.poolCreated.subscribe((createdPool) => {
8665
+ // Reload the entire table to sync all pool-related changes
8666
+ this.loadTable(table.id);
8667
+ this.toastService.success(`Pool "${createdPool.poolName}" created successfully!`);
8668
+ });
8669
+ modalRef.result.catch(() => {
8670
+ // Modal was dismissed - no action needed
8671
+ });
8672
+ }
8673
+ deletePool(pool) {
8674
+ const table = this.table();
8675
+ if (!table || !pool.id)
8676
+ return;
8677
+ const confirmMessage = `Are you sure you want to delete the pool "${pool.poolName}"?\n\nThis action cannot be undone.`;
8678
+ if (confirm(confirmMessage)) {
8679
+ this.samplePoolService.deleteSamplePool(pool.id).subscribe({
8680
+ next: () => {
8681
+ // Reload the entire table to sync all pool-related changes
8682
+ this.loadTable(table.id);
8683
+ this.toastService.success(`Pool "${pool.poolName}" deleted successfully!`);
8684
+ },
8685
+ error: (error) => {
8686
+ console.error('Error deleting pool:', error);
8687
+ this.toastService.error(`Failed to delete pool "${pool.poolName}"`);
8688
+ }
8689
+ });
8690
+ }
8691
+ }
8692
+ // Metadata value editing methods
8693
+ editColumnValue(column) {
8694
+ const table = this.table();
8695
+ if (!table || !table.canEdit || !column.id)
8696
+ return;
8697
+ // Prepare sample data for multi-sample editing
8698
+ const sampleData = this.tableRows().map(row => ({
8699
+ index: row._sampleIndex,
8700
+ value: row[`col_${column.id}`] || column.value || '',
8701
+ sourceName: row.sourceName || `Sample ${row._sampleIndex}`
8702
+ }));
8703
+ const config = {
8704
+ columnId: column.id,
8705
+ columnName: column.name,
8706
+ columnType: column.type,
8707
+ ontologyType: column.ontologyType,
8708
+ enableTypeahead: true,
8709
+ currentValue: column.value,
8710
+ context: 'table',
8711
+ tableId: table.id,
8712
+ enableMultiSampleEdit: true,
8713
+ sampleData: sampleData,
8714
+ maxSampleCount: table.sampleCount
8715
+ };
8716
+ const modalRef = this.modalService.open(MetadataValueEditModal, {
8717
+ size: 'xl',
8718
+ backdrop: 'static'
8719
+ });
8720
+ modalRef.componentInstance.config = config;
8721
+ // Debug log to check if multi-sample editing is enabled
8722
+ console.log('Multi-sample editing config:', {
8723
+ enableMultiSampleEdit: config.enableMultiSampleEdit,
8724
+ sampleDataLength: config.sampleData?.length,
8725
+ hasMultiSampleEdit: config.enableMultiSampleEdit && config.sampleData && config.sampleData.length > 0
8726
+ });
8727
+ modalRef.componentInstance.valueSaved.subscribe((result) => {
8728
+ let apiData;
8729
+ if (typeof result === 'string') {
8730
+ // Simple string value - update default
8731
+ apiData = {
8732
+ value: result,
8733
+ value_type: 'default'
8734
+ };
8735
+ }
8736
+ else {
8737
+ // Multi-sample edit - update specific samples
8738
+ apiData = {
8739
+ value: result.value,
8740
+ sample_indices: result.sampleIndices,
8741
+ value_type: 'sample_specific'
8742
+ };
8743
+ }
8744
+ // Use the API to update the column value
8745
+ const requestData = {
8746
+ value: typeof result === 'string' ? result : result.value,
8747
+ sampleIndices: typeof result === 'string' ? undefined : result.sampleIndices,
8748
+ valueType: typeof result === 'string' ? 'default' : 'sample_specific'
8749
+ };
8750
+ this.metadataColumnService.updateColumnValue(column.id, requestData).subscribe({
8751
+ next: (response) => {
8752
+ // Update the local column data with the returned column
8753
+ const currentTable = this.table();
8754
+ if (currentTable && currentTable.columns && response.column) {
8755
+ const updatedColumns = currentTable.columns.map(col => col.id === column.id ? response.column : col);
8756
+ this.table.set({ ...currentTable, columns: updatedColumns });
8757
+ }
8758
+ this.toastService.success('Column value updated successfully!');
8759
+ modalRef.componentInstance.onClose();
8760
+ },
8761
+ error: (error) => {
8762
+ console.error('Error updating column value:', error);
8763
+ this.toastService.error('Failed to update column value');
8764
+ }
8765
+ });
8766
+ });
8767
+ }
8768
+ // Toggle column hidden property
8769
+ toggleColumnHidden(column) {
8770
+ const table = this.table();
8771
+ if (!table || !table.canEdit || !column.id)
8772
+ return;
8773
+ const newHiddenValue = !column.hidden;
8774
+ this.metadataColumnService.patchMetadataColumn(column.id, { hidden: newHiddenValue }).subscribe({
8775
+ next: (updatedColumn) => {
8776
+ // Update the local column data
8777
+ const currentTable = this.table();
8778
+ if (currentTable && currentTable.columns) {
8779
+ const updatedColumns = currentTable.columns.map(col => col.id === column.id ? updatedColumn : col);
8780
+ this.table.set({ ...currentTable, columns: updatedColumns });
8781
+ // Reload the table data to sync pool columns
8782
+ this.loadTable(table.id);
8783
+ }
8784
+ this.toastService.success(`Column "${column.name}" visibility updated successfully!`);
8785
+ },
8786
+ error: (error) => {
8787
+ console.error('Error updating column hidden property:', error);
8788
+ this.toastService.error('Failed to update column visibility');
8789
+ }
8790
+ });
8791
+ }
8792
+ // View column details
8793
+ viewColumnDetails(column) {
8794
+ // For now, we'll show an alert with column details
8795
+ // This can be enhanced with a proper modal later
8796
+ const details = [
8797
+ `Name: ${column.name}`,
8798
+ `Type: ${column.type}`,
8799
+ `Hidden: ${column.hidden ? 'Yes' : 'No'}`,
8800
+ `Mandatory: ${column.mandatory ? 'Yes' : 'No'}`,
8801
+ `Read Only: ${column.readonly ? 'Yes' : 'No'}`,
8802
+ `Default Value: ${column.value || 'None'}`,
8803
+ `Ontology Type: ${column.ontologyType || 'None'}`,
8804
+ `Enable Typeahead: ${column.enableTypeahead ? 'Yes' : 'No'}`
8805
+ ].join('\n');
8806
+ alert(`Column Details:\n\n${details}`);
8807
+ }
8808
+ // Edit column settings
8809
+ editColumnSettings(column) {
8810
+ const table = this.table();
8811
+ if (!table || !table.canEdit || !column.id)
8812
+ return;
8813
+ const modalRef = this.modalService.open(ColumnEditModal, {
8814
+ size: 'lg',
8815
+ backdrop: 'static',
8816
+ keyboard: false
8817
+ });
8818
+ // Configure modal for editing existing column
8819
+ modalRef.componentInstance.column = column;
8820
+ modalRef.componentInstance.templateId = null;
8821
+ modalRef.componentInstance.isEdit = true;
8822
+ // Handle column save
8823
+ modalRef.componentInstance.columnSaved.subscribe((columnData) => {
8824
+ this.updateColumnSettings(column.id, columnData);
8825
+ modalRef.componentInstance.onClose();
8826
+ });
8827
+ }
8828
+ // Update column settings
8829
+ updateColumnSettings(columnId, columnData) {
8830
+ this.metadataColumnService.patchMetadataColumn(columnId, columnData).subscribe({
8831
+ next: (updatedColumn) => {
8832
+ // Update the local column data
8833
+ const currentTable = this.table();
8834
+ if (currentTable && currentTable.columns) {
8835
+ const updatedColumns = currentTable.columns.map(col => col.id === columnId ? updatedColumn : col);
8836
+ this.table.set({ ...currentTable, columns: updatedColumns });
8837
+ }
8838
+ this.toastService.success(`Column "${updatedColumn.name}" updated successfully!`);
8839
+ // Reload the table to sync any changes
8840
+ this.loadTable(currentTable.id);
8841
+ },
8842
+ error: (error) => {
8843
+ console.error('Error updating column settings:', error);
8844
+ this.toastService.error('Failed to update column settings');
8845
+ }
8846
+ });
8847
+ }
8848
+ // Pool metadata value editing method
8849
+ editPoolColumnValue(pool, column) {
8850
+ const table = this.table();
8851
+ if (!table || !table.canEdit)
8852
+ return;
8853
+ // Find the pool's metadata column that corresponds to this table column
8854
+ const poolColumn = pool.metadataColumns?.find(pc => pc.name === column.name);
8855
+ if (!poolColumn || !poolColumn.id)
8856
+ return;
8857
+ const config = {
8858
+ columnId: poolColumn.id,
8859
+ columnName: poolColumn.name,
8860
+ columnType: poolColumn.type,
8861
+ ontologyType: poolColumn.ontologyType,
8862
+ enableTypeahead: true,
8863
+ currentValue: poolColumn.value,
8864
+ context: 'pool',
8865
+ tableId: table.id,
8866
+ poolId: pool.id
8867
+ };
8868
+ const modalRef = this.modalService.open(MetadataValueEditModal, {
8869
+ size: 'lg',
8870
+ backdrop: 'static'
8871
+ });
8872
+ modalRef.componentInstance.config = config;
8873
+ modalRef.componentInstance.valueSaved.subscribe((newValue) => {
8874
+ // Update the pool's metadata column value locally
8875
+ const currentTable = this.table();
8876
+ if (currentTable && currentTable.samplePools) {
8877
+ const updatedPools = currentTable.samplePools.map(p => {
8878
+ if (p.id === pool.id) {
8879
+ const updatedMetadataColumns = p.metadataColumns.map((mc) => mc.id === poolColumn.id ? { ...mc, value: newValue } : mc);
8880
+ return { ...p, metadataColumns: updatedMetadataColumns };
8881
+ }
8882
+ return p;
8883
+ });
8884
+ this.table.set({ ...currentTable, samplePools: updatedPools });
8885
+ }
8886
+ this.toastService.success('Pool column value updated successfully!');
8887
+ modalRef.componentInstance.onClose();
8888
+ });
8889
+ }
8890
+ // Sample-specific metadata value editing method
8891
+ editSampleColumnValue(column, sampleIndex) {
8892
+ const table = this.table();
8893
+ if (!table || !table.canEdit || !column.id)
8894
+ return;
8895
+ // Get current value for this sample (could be default or modified)
8896
+ const currentValue = this.getSampleColumnValue(column, sampleIndex);
8897
+ const config = {
8898
+ columnId: column.id,
8899
+ columnName: column.name,
8900
+ columnType: column.type,
8901
+ ontologyType: column.ontologyType,
8902
+ enableTypeahead: true,
8903
+ currentValue: currentValue,
8904
+ context: 'table',
8905
+ tableId: table.id
8906
+ };
8907
+ const modalRef = this.modalService.open(MetadataValueEditModal, {
8908
+ size: 'lg',
8909
+ backdrop: 'static'
8910
+ });
8911
+ modalRef.componentInstance.config = config;
8912
+ modalRef.componentInstance.valueSaved.subscribe((newValue) => {
8913
+ // Use the API to update the sample-specific value
8914
+ this.metadataColumnService.updateColumnValue(column.id, {
8915
+ value: newValue,
8916
+ sampleIndices: [sampleIndex],
8917
+ valueType: 'sample_specific'
8918
+ }).subscribe({
8919
+ next: (response) => {
8920
+ // Update the local column data with the returned column
8921
+ const currentTable = this.table();
8922
+ if (currentTable && currentTable.columns && response.column) {
8923
+ const updatedColumns = currentTable.columns.map(col => col.id === column.id ? response.column : col);
8924
+ this.table.set({ ...currentTable, columns: updatedColumns });
8925
+ }
8926
+ this.toastService.success(`Sample ${sampleIndex} value updated successfully!`);
8927
+ modalRef.componentInstance.onClose();
8928
+ },
8929
+ error: (error) => {
8930
+ console.error('Error updating sample value:', error);
8931
+ this.toastService.error(`Failed to update sample ${sampleIndex} value`);
8932
+ }
8933
+ });
8934
+ });
8935
+ }
8936
+ // Helper method to get current value for a specific sample
8937
+ getSampleColumnValue(column, sampleIndex) {
8938
+ // Check if this sample has a specific modifier
8939
+ if (column.modifiers && column.modifiers.length > 0) {
8940
+ for (const modifier of column.modifiers) {
8941
+ if (modifier?.samples) {
8942
+ const sampleRanges = modifier.samples.split(',').map((s) => s.trim()).filter((s) => s.length > 0);
8943
+ for (const range of sampleRanges) {
8944
+ if (range.includes('-')) {
8945
+ const parts = range.split('-').map((n) => n.trim());
8946
+ if (parts.length === 2) {
8947
+ const start = parseInt(parts[0]);
8948
+ const end = parseInt(parts[1]);
8949
+ if (!isNaN(start) && !isNaN(end) && sampleIndex >= start && sampleIndex <= end) {
8950
+ return modifier.value || '';
8951
+ }
8952
+ }
8953
+ }
8954
+ else {
8955
+ const sampleNum = parseInt(range);
8956
+ if (!isNaN(sampleNum) && sampleNum === sampleIndex) {
8957
+ return modifier.value || '';
8958
+ }
8959
+ }
8960
+ }
8961
+ }
8962
+ }
8963
+ }
8964
+ // Return default value if no modifier found
8965
+ return column.value || '';
8966
+ }
8967
+ // Remove column method
8968
+ removeColumn(column) {
8969
+ const table = this.table();
8970
+ if (!table || !table.canEdit || !column.id)
8971
+ return;
8972
+ const confirmMessage = `Are you sure you want to remove the column "${column.name}"?\n\nThis action cannot be undone and will remove all data in this column.`;
8973
+ if (confirm(confirmMessage)) {
8974
+ this.metadataColumnService.deleteMetadataColumn(column.id).subscribe({
8975
+ next: () => {
8976
+ // Remove the column from the local table data
8977
+ const currentTable = this.table();
8978
+ if (currentTable && currentTable.columns) {
8979
+ const updatedColumns = currentTable.columns.filter(col => col.id !== column.id);
8980
+ this.table.set({ ...currentTable, columns: updatedColumns });
8981
+ }
8982
+ this.toastService.success(`Column "${column.name}" removed successfully!`);
8983
+ },
8984
+ error: (error) => {
8985
+ console.error('Error removing column:', error);
8986
+ this.toastService.error(`Failed to remove column "${column.name}"`);
8987
+ }
8988
+ });
8989
+ }
8990
+ }
8991
+ /**
8992
+ * Open modal to add a new column with automatic reordering
8993
+ */
8994
+ openAddColumnModal() {
8995
+ const modalRef = this.modalService.open(ColumnEditModal, {
8996
+ size: 'lg',
8997
+ backdrop: 'static',
8998
+ keyboard: false
8999
+ });
9000
+ // Configure modal for adding new column
9001
+ modalRef.componentInstance.column = null;
9002
+ modalRef.componentInstance.templateId = null;
9003
+ modalRef.componentInstance.isEdit = false;
9004
+ // Handle column save
9005
+ modalRef.componentInstance.columnSaved.subscribe((columnData) => {
9006
+ this.addColumnWithAutoReorder(columnData);
9007
+ modalRef.componentInstance.onClose();
9008
+ });
9009
+ }
9010
+ /**
9011
+ * Add a new column using the new API with automatic reordering
9012
+ */
9013
+ addColumnWithAutoReorder(columnData) {
9014
+ const currentTable = this.table();
9015
+ if (!currentTable) {
9016
+ this.toastService.error('No table selected');
9017
+ return;
9018
+ }
9019
+ this.isLoading.set(true);
9020
+ this.metadataTableService.addColumnWithAutoReorder(currentTable.id, {
9021
+ columnData: columnData
9022
+ }).subscribe({
9023
+ next: (response) => {
9024
+ // Update the local table data with the new column
9025
+ const updatedColumns = [...(currentTable.columns || []), response.column];
9026
+ this.table.set({ ...currentTable, columns: updatedColumns });
9027
+ // Show success message with reordering info
9028
+ let message = `Column "${response.column.name}" added successfully!`;
9029
+ if (response.reordered) {
9030
+ message += ` Columns reordered using ${response.schemaIdsUsed.length} schema(s).`;
9031
+ }
9032
+ this.toastService.success(message);
9033
+ // Refresh the table to get the latest column positions
9034
+ this.loadTable(currentTable.id);
9035
+ },
9036
+ error: (error) => {
9037
+ console.error('Error adding column:', error);
9038
+ this.toastService.error('Failed to add column');
9039
+ },
9040
+ complete: () => {
9041
+ this.isLoading.set(false);
9042
+ }
9043
+ });
9044
+ }
9045
+ /**
9046
+ * Reorder columns by schema selection
9047
+ */
9048
+ reorderColumns() {
9049
+ const currentTable = this.table();
9050
+ if (!currentTable) {
9051
+ this.toastService.error('No table selected');
9052
+ return;
9053
+ }
9054
+ const modalRef = this.modalService.open(SchemaSelectionModal, {
9055
+ size: 'lg',
9056
+ backdrop: 'static'
9057
+ });
9058
+ modalRef.componentInstance.title = 'Reorder Columns by Schema';
9059
+ modalRef.componentInstance.description = `Select schemas to reorder columns in "${currentTable.name}". Columns will be arranged based on the selected schema order.`;
9060
+ modalRef.result.then((result) => {
9061
+ if (result && result.selectedSchemaIds) {
9062
+ this.performColumnReorder(currentTable.id, result.selectedSchemaIds);
9063
+ }
9064
+ }).catch(() => {
9065
+ // Modal was dismissed - no action needed
9066
+ });
9067
+ }
9068
+ /**
9069
+ * Perform the actual column reordering with async task monitoring
9070
+ */
9071
+ performColumnReorder(tableId, schemaIds) {
9072
+ this.isLoading.set(true);
9073
+ this.metadataTableService.reorderColumnsBySchemaAsync(tableId, schemaIds).subscribe({
9074
+ next: (response) => {
9075
+ console.log('Column reorder task started:', response);
9076
+ this.toastService.success(`Column reorder task queued successfully! Task ID: ${response.taskId}`);
9077
+ // Mark task for monitoring and start real-time updates (same pattern as imports)
9078
+ this.asyncTaskService.monitorTask(response.taskId);
9079
+ this.asyncTaskService.startRealtimeUpdates();
9080
+ this.isLoading.set(false);
9081
+ },
9082
+ error: (error) => {
9083
+ console.error('Error starting column reorder:', error);
9084
+ this.toastService.error('Failed to start column reordering');
9085
+ this.isLoading.set(false);
9086
+ }
9087
+ });
9088
+ }
9089
+ onColumnDrop(event) {
9090
+ if (event.previousIndex === event.currentIndex)
9091
+ return;
9092
+ const currentTable = this.table();
9093
+ if (!currentTable?.canEdit)
9094
+ return;
9095
+ const dragColumns = [...this.dragColumnsForList()];
9096
+ moveItemInArray(dragColumns, event.previousIndex, event.currentIndex);
9097
+ // Update columnPosition for all columns in the new order
9098
+ dragColumns.forEach((col, index) => {
9099
+ col.columnPosition = index;
9100
+ });
9101
+ this.dragColumnsForList.set(dragColumns);
9102
+ // Update the main table data too
9103
+ const updatedTableColumns = [...(currentTable.columns || [])];
9104
+ dragColumns.forEach((dragCol, index) => {
9105
+ const tableColIndex = updatedTableColumns.findIndex(col => col.id === dragCol.id);
9106
+ if (tableColIndex !== -1) {
9107
+ updatedTableColumns[tableColIndex] = { ...updatedTableColumns[tableColIndex], columnPosition: index };
9108
+ }
9109
+ });
9110
+ this.table.update(table => table ? { ...table, columns: updatedTableColumns } : null);
9111
+ const draggedColumn = dragColumns[event.currentIndex];
9112
+ const newPosition = event.currentIndex;
9113
+ console.log(`Moving column "${draggedColumn.name}" to position ${newPosition}`);
9114
+ this.metadataTableService.reorderColumn(currentTable.id, draggedColumn.id, newPosition).subscribe({
9115
+ next: () => {
9116
+ this.toastService.success('Column reordered successfully');
9117
+ this.loadTable(currentTable.id);
9118
+ },
9119
+ error: () => {
9120
+ this.toastService.error('Failed to reorder column');
9121
+ this.loadTable(currentTable.id);
9122
+ }
9123
+ });
9124
+ }
9125
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.7", ngImport: i0, type: MetadataTableDetails, deps: [{ token: i1$1.ActivatedRoute }, { token: i1$1.Router }, { token: MetadataTableService }, { token: AsyncExportService }, { token: ChunkedUploadService }, { token: SamplePoolService }, { token: MetadataColumnService }, { token: i3.ToastService }, { token: AsyncTaskUIService }, { token: i2.NgbModal }], target: i0.ɵɵFactoryTarget.Component });
9126
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.7", type: MetadataTableDetails, isStandalone: true, selector: "ccv-metadata-table-details", viewQueries: [{ propertyName: "fileInput", first: true, predicate: ["fileInput"], descendants: true }, { propertyName: "excelInput", first: true, predicate: ["excelInput"], descendants: true }], ngImport: i0, template: "<div class=\"metadata-table-details-container\">\n <!-- Header Section with Breadcrumbs -->\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 <!-- Breadcrumb Navigation -->\n <nav aria-label=\"breadcrumb\" class=\"me-3\">\n <ol class=\"breadcrumb mb-0\">\n <li class=\"breadcrumb-item\">\n <a routerLink=\"/metadata-tables\" class=\"text-decoration-none\">\n <i class=\"bi bi-table me-1\"></i>Metadata Tables\n </a>\n </li>\n <li class=\"breadcrumb-item active\" aria-current=\"page\">\n @if (table()) {\n {{ table()!.name }}\n } @else {\n Table Details\n }\n </li>\n </ol>\n </nav>\n\n <!-- Table Status Badge -->\n @if (table()) {\n <span class=\"badge ms-2\" [class]=\"getTableStatusClass()\">\n {{ getTableStatusBadge() }}\n </span>\n }\n </div>\n\n <!-- Action Buttons -->\n <div class=\"d-flex gap-2\">\n <button\n type=\"button\"\n class=\"btn btn-outline-secondary btn-sm\"\n (click)=\"navigateBack()\"\n title=\"Back to Tables\">\n <i class=\"bi bi-arrow-left me-1\"></i>Back\n </button>\n\n <!-- View Mode Toggle -->\n @if (hasColumns()) {\n <div class=\"btn-group\" role=\"group\">\n <button\n type=\"button\"\n class=\"btn btn-sm\"\n [class]=\"viewMode() === 'list' ? 'btn-primary' : 'btn-outline-primary'\"\n (click)=\"switchToListMode()\"\n title=\"Column List View\">\n <i class=\"bi bi-list-ul me-1\"></i>List\n </button>\n <button\n type=\"button\"\n class=\"btn btn-sm\"\n [class]=\"viewMode() === 'table' ? 'btn-primary' : 'btn-outline-primary'\"\n (click)=\"switchToTableMode()\"\n title=\"Table Data View\">\n <i class=\"bi bi-table me-1\"></i>Table\n </button>\n </div>\n }\n <!-- Import Dropdown -->\n @if (table() && table()!.canEdit) {\n <div class=\"btn-group\" ngbDropdown>\n <!-- Hidden file inputs -->\n <input\n #fileInput\n type=\"file\"\n accept=\".txt,.tsv\"\n style=\"display: none;\"\n (change)=\"importSdrf($event)\">\n <input\n #excelInput\n type=\"file\"\n accept=\".xlsx,.xls\"\n style=\"display: none;\"\n (change)=\"importExcel($event)\">\n \n <button\n type=\"button\"\n class=\"btn btn-outline-success btn-sm dropdown-toggle\"\n ngbDropdownToggle\n [disabled]=\"isLoading()\"\n title=\"Import Data\">\n <i class=\"bi bi-upload me-1\"></i>Import\n </button>\n <div class=\"dropdown-menu\" ngbDropdownMenu>\n <a class=\"dropdown-item\" href=\"#\" (click)=\"triggerFileInput(); $event.preventDefault()\">\n <i class=\"bi bi-file-earmark-text me-2\"></i>Import SDRF File\n </a>\n <a class=\"dropdown-item\" href=\"#\" (click)=\"triggerExcelInput(); $event.preventDefault()\">\n <i class=\"bi bi-file-earmark-spreadsheet me-2\"></i>Import Excel Template\n </a>\n </div>\n </div>\n }\n\n <!-- Export Dropdown -->\n @if (table() && hasColumns()) {\n <div class=\"btn-group\" ngbDropdown>\n <button\n type=\"button\"\n class=\"btn btn-outline-info btn-sm dropdown-toggle\"\n ngbDropdownToggle\n [disabled]=\"isLoading()\"\n title=\"Export Table\">\n <i class=\"bi bi-download me-1\"></i>Export\n </button>\n <div class=\"dropdown-menu dropdown-menu-end\" ngbDropdownMenu>\n <a class=\"dropdown-item\" href=\"#\" (click)=\"exportToSdrf('sdrf'); $event.preventDefault()\">\n <i class=\"bi bi-file-earmark-text me-2\"></i>Export as SDRF\n </a>\n <a class=\"dropdown-item\" href=\"#\" (click)=\"exportToSdrf('excel'); $event.preventDefault()\">\n <i class=\"bi bi-file-earmark-spreadsheet me-2\"></i>Export as Excel\n </a>\n </div>\n </div>\n }\n\n <!-- Edit Button -->\n @if (table() && table()!.canEdit) {\n <button\n type=\"button\"\n class=\"btn btn-outline-primary btn-sm\"\n (click)=\"editTable()\"\n [disabled]=\"isLoading()\"\n title=\"Edit Table\">\n <i class=\"bi bi-pencil me-1\"></i>Edit\n </button>\n }\n\n <!-- Reorder Columns Button -->\n @if (table() && table()!.canEdit && hasColumns()) {\n <button\n type=\"button\"\n class=\"btn btn-outline-warning btn-sm\"\n (click)=\"reorderColumns()\"\n [disabled]=\"isLoading()\"\n title=\"Reorder Columns by Schema\">\n <i class=\"bi bi-arrows-move me-1\"></i>Reorder\n </button>\n }\n\n <!-- Create Pool Button -->\n @if (table() && table()!.canEdit && hasColumns()) {\n <button\n type=\"button\"\n class=\"btn btn-success btn-sm\"\n (click)=\"createPool()\"\n [disabled]=\"isLoading()\"\n title=\"Create Sample Pool\">\n <i class=\"bi bi-plus-circle me-1\"></i>Create Pool\n </button>\n }\n\n <!-- More Actions Dropdown -->\n @if (table() && table()!.canEdit) {\n <div class=\"btn-group\" ngbDropdown>\n <button\n type=\"button\"\n class=\"btn btn-outline-danger btn-sm dropdown-toggle\"\n ngbDropdownToggle\n [disabled]=\"isLoading()\">\n <i class=\"bi bi-three-dots-vertical\"></i>\n </button>\n <div class=\"dropdown-menu dropdown-menu-end\" ngbDropdownMenu>\n <a class=\"dropdown-item text-danger\" href=\"#\" (click)=\"deleteTable(); $event.preventDefault()\">\n <i class=\"bi bi-trash me-2\"></i>Delete Table\n </a>\n </div>\n </div>\n }\n </div>\n </div>\n </div>\n </nav>\n\n <!-- Loading Spinner -->\n @if (isLoading()) {\n <div class=\"d-flex justify-content-center align-items-center\" style=\"height: 400px;\">\n <div class=\"spinner-border text-primary\" role=\"status\">\n <span class=\"visually-hidden\">Loading...</span>\n </div>\n </div>\n }\n\n <!-- Table Details Content -->\n @if (table() && !isLoading()) {\n <div class=\"table-details-content p-4\">\n\n <!-- Table Information Card -->\n <div class=\"card mb-4\">\n <div class=\"card-header\">\n <div class=\"d-flex align-items-center justify-content-between\">\n <h5 class=\"mb-0\">\n <i class=\"bi bi-info-circle me-2\"></i>Table Information\n </h5>\n <small class=\"text-muted\">\n Created {{ table()!.createdAt | date:'medium' }}\n </small>\n </div>\n </div>\n <div class=\"card-body\">\n <div class=\"row\">\n <div class=\"col-md-8\">\n <h3 class=\"fw-bold mb-3\">{{ table()!.name }}</h3>\n @if (table()!.description) {\n <p class=\"text-muted mb-3\">{{ table()!.description }}</p>\n }\n <div class=\"row\">\n <div class=\"col-md-6\">\n <div class=\"d-flex align-items-center mb-2\">\n <i class=\"bi bi-hash text-muted me-2\"></i>\n <span class=\"fw-semibold me-2\">Sample Count:</span>\n <span class=\"badge bg-primary\">{{ table()!.sampleCount }}</span>\n </div>\n <div class=\"d-flex align-items-center mb-2\">\n <i class=\"bi bi-columns-gap text-muted me-2\"></i>\n <span class=\"fw-semibold me-2\">Columns:</span>\n <span class=\"badge bg-info\">{{ table()!.columns?.length || 0 }}</span>\n </div>\n </div>\n <div class=\"col-md-6\">\n @if (table()!.ownerUsername) {\n <div class=\"d-flex align-items-center mb-2\">\n <i class=\"bi bi-person text-muted me-2\"></i>\n <span class=\"fw-semibold me-2\">Owner:</span>\n <span>{{ table()!.ownerUsername }}</span>\n </div>\n }\n @if (table()!.labGroupName) {\n <div class=\"d-flex align-items-center mb-2\">\n <i class=\"bi bi-people text-muted me-2\"></i>\n <span class=\"fw-semibold me-2\">Lab Group:</span>\n <span>{{ table()!.labGroupName }}</span>\n </div>\n }\n <div class=\"d-flex align-items-center mb-2\">\n <i class=\"bi bi-tag text-muted me-2\"></i>\n <span class=\"fw-semibold me-2\">Version:</span>\n <span class=\"badge bg-light text-dark\">{{ table()!.version || 'v1.0' }}</span>\n </div>\n </div>\n </div>\n </div>\n <div class=\"col-md-4 text-end\">\n <div class=\"d-flex flex-column gap-2\">\n @if (table()!.isLocked) {\n <span class=\"badge bg-warning\">\n <i class=\"bi bi-lock me-1\"></i>Locked\n </span>\n }\n @if (table()!.isPublished) {\n <span class=\"badge bg-success\">\n <i class=\"bi bi-cloud-check me-1\"></i>Published\n </span>\n }\n </div>\n </div>\n </div>\n </div>\n </div>\n\n <!-- Sample Pool Table -->\n @if (viewMode() === 'table' && hasPools()) {\n <div class=\"card mb-4\">\n <div class=\"card-header\">\n <h5 class=\"mb-0\">\n <i class=\"bi bi-layers me-2\"></i>Sample Pool Table\n <span class=\"badge bg-warning ms-2\">{{ sortedPools().length }} pools</span>\n </h5>\n </div>\n <div class=\"card-body p-0\">\n <div class=\"table-responsive\">\n <table class=\"table table-hover mb-0\">\n <thead class=\"table-light\">\n <tr>\n <th scope=\"col\" style=\"width: 80px;\" class=\"bg-secondary bg-opacity-10 text-secondary\">Pool</th>\n @for (column of sortedColumns(); track column.id) {\n <th scope=\"col\" [class]=\"getColumnHeaderClass(column)\" [title]=\"column.name\">\n <div class=\"header-cell d-flex justify-content-between align-items-center\">\n <span>{{ getShortColumnName(column) }}</span>\n @if (table()!.canEdit) {\n <div class=\"header-actions\">\n <button\n type=\"button\"\n class=\"btn btn-sm btn-outline-primary header-edit-btn me-1\"\n (click)=\"editColumnValue(column)\"\n [title]=\"'Edit default value for ' + column.name\">\n <i class=\"bi bi-pencil\"></i>\n </button>\n </div>\n }\n </div>\n </th>\n }\n </tr>\n </thead>\n <tbody>\n @for (poolRow of poolTableRows(); track poolRow._poolId) {\n <tr class=\"pool-row\" [class.editable]=\"table()!.canEdit\">\n <td class=\"text-muted fw-semibold\" \n [class.clickable]=\"table()!.canEdit\"\n (click)=\"table()!.canEdit ? editPool(poolRow._poolData) : null\"\n [title]=\"table()!.canEdit ? 'Click to edit pool: ' + poolRow._poolName : poolRow._poolName\">\n <i class=\"bi bi-layers me-2\"></i>{{ poolRow._poolName }}\n @if (table()!.canEdit) {\n <i class=\"bi bi-pencil-square ms-1 text-primary\" style=\"font-size: 0.8em;\"></i>\n }\n </td>\n @for (column of sortedColumns(); track column.id) {\n <td\n class=\"pool-cell\"\n [class.opacity-50]=\"column.hidden\"\n [attr.data-pool-id]=\"poolRow._poolId\"\n [attr.data-column-id]=\"column.id\"\n (click)=\"table()!.canEdit ? editPoolColumnValue(poolRow._poolData, column) : null\"\n [title]=\"table()!.canEdit ? 'Click to edit ' + column.name + ' for pool ' + poolRow._poolName : ''\">\n {{ poolRow['col_' + column.id] || '-' }}\n </td>\n }\n </tr>\n }\n </tbody>\n </table>\n </div>\n </div>\n </div>\n }\n\n <!-- Columns/Table Section -->\n <div class=\"card\">\n <div class=\"card-header\">\n <div class=\"d-flex align-items-center justify-content-between\">\n <h5 class=\"mb-0\">\n @if (viewMode() === 'list') {\n <i class=\"bi bi-list-columns me-2\"></i>Table Columns\n } @else {\n <i class=\"bi bi-table me-2\"></i>Table Data\n }\n </h5>\n \n <!-- Column Filter -->\n @if (hasColumns()) {\n <div class=\"d-flex align-items-center me-3\">\n <div class=\"input-group input-group-sm\" style=\"width: 250px;\">\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 columns...\"\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 ms-2\">\n {{ filteredColumnCount }}/{{ totalColumnCount }}\n </small>\n }\n </div>\n }\n \n <div class=\"d-flex gap-2 align-items-center\">\n @if (viewMode() === 'list') {\n <span class=\"badge bg-primary\">{{ sortedColumns().length }} columns</span>\n @if (table() && table()!.canEdit) {\n <button\n type=\"button\"\n class=\"btn btn-success btn-sm\"\n (click)=\"openAddColumnModal()\"\n [disabled]=\"isLoading()\"\n title=\"Add new column with automatic reordering\">\n <i class=\"bi bi-plus-circle me-1\"></i>Add Column\n </button>\n }\n @if (table() && table()!.canEdit && sortedColumns().length === 0) {\n <small class=\"text-muted\">Import SDRF to add columns</small>\n }\n } @else {\n <span class=\"badge bg-info\">{{ tableRows().length }} samples</span>\n @if (hasPools()) {\n <span class=\"badge bg-warning\">{{ sortedPools().length }} pools</span>\n }\n <span class=\"badge bg-primary\">{{ sortedColumns().length }} columns</span>\n @if (table() && table()!.canEdit) {\n <button\n type=\"button\"\n class=\"btn btn-success btn-sm\"\n (click)=\"openAddColumnModal()\"\n [disabled]=\"isLoading()\"\n title=\"Add new column with automatic reordering\">\n <i class=\"bi bi-plus-circle me-1\"></i>Add Column\n </button>\n }\n }\n </div>\n </div>\n </div>\n\n @if (hasColumns()) {\n <!-- List Mode: Column Definitions -->\n @if (viewMode() === 'list') {\n <div class=\"table-responsive\">\n <table class=\"table table-hover mb-0\">\n <thead>\n <tr>\n <th scope=\"col\" style=\"width: 40px;\">\n @if (table()!.canEdit) {\n <i class=\"bi bi-arrows-move text-muted\" title=\"Drag to reorder\"></i>\n }\n </th>\n <th scope=\"col\" style=\"width: 60px;\" \n class=\"sortable\" \n (click)=\"sortBy('position')\"\n [class.sorted]=\"isSorted('position')\">\n <div class=\"d-flex align-items-center justify-content-between\">\n <span>#</span>\n <i class=\"bi ms-1\" [class]=\"getSortIcon('position')\"></i>\n </div>\n </th>\n <th scope=\"col\" \n class=\"sortable\" \n (click)=\"sortBy('name')\"\n [class.sorted]=\"isSorted('name')\">\n <div class=\"d-flex align-items-center justify-content-between\">\n <span>Column Name</span>\n <i class=\"bi ms-1\" [class]=\"getSortIcon('name')\"></i>\n </div>\n </th>\n <th scope=\"col\" \n class=\"sortable\" \n (click)=\"sortBy('type')\"\n [class.sorted]=\"isSorted('type')\">\n <div class=\"d-flex align-items-center justify-content-between\">\n <span>Type</span>\n <i class=\"bi ms-1\" [class]=\"getSortIcon('type')\"></i>\n </div>\n </th>\n <th scope=\"col\" \n class=\"sortable\" \n (click)=\"sortBy('value')\"\n [class.sorted]=\"isSorted('value')\">\n <div class=\"d-flex align-items-center justify-content-between\">\n <span>Value</span>\n <i class=\"bi ms-1\" [class]=\"getSortIcon('value')\"></i>\n </div>\n </th>\n <th scope=\"col\">Properties</th>\n <th scope=\"col\" style=\"width: 100px;\">Actions</th>\n </tr>\n </thead>\n <tbody cdkDropList (cdkDropListDropped)=\"onColumnDrop($event)\" [cdkDropListData]=\"dragColumnsForList()\">\n @for (column of dragColumnsForList(); track column.id) {\n <tr cdkDrag [cdkDragData]=\"column\" [cdkDragDisabled]=\"!table()!.canEdit\">\n <td class=\"text-center\">\n @if (table()!.canEdit) {\n <i class=\"bi bi-grip-vertical drag-handle\" cdkDragHandle></i>\n }\n </td>\n <td class=\"text-muted fw-semibold\">\n {{ (column.columnPosition || 0) + 1 }}\n </td>\n <td>\n <div class=\"d-flex align-items-center\">\n <i class=\"me-2\" [class]=\"getColumnTypeIcon(column) + ' ' + getColumnTypeClass(column)\"></i>\n <span class=\"fw-semibold\">{{ column.name }}</span>\n </div>\n </td>\n <td>\n <span class=\"badge bg-light text-dark\">{{ column.type }}</span>\n </td>\n <td>\n <div>\n <!-- Default Value -->\n <div class=\"fw-semibold\">{{ getColumnValueDisplay(column) }}</div>\n\n <!-- Sample-specific Modifiers -->\n @if (hasModifiers(column)) {\n <div class=\"mt-1\">\n @for (modifierDisplay of getModifiersDisplay(column); track $index) {\n <small class=\"badge bg-light text-dark me-1 mb-1 d-inline-block\"\n style=\"font-size: 0.7rem;\"\n [title]=\"modifierDisplay\">\n {{ modifierDisplay }}\n </small>\n }\n </div>\n }\n </div>\n </td>\n <td>\n <div class=\"d-flex gap-1 flex-wrap\">\n @if (column.mandatory) {\n <span class=\"badge bg-danger\" title=\"Required field\">Required</span>\n }\n @if (column.readonly) {\n <span class=\"badge bg-secondary\" title=\"Read-only field\">Read-only</span>\n }\n @if (column.hidden) {\n <span class=\"badge bg-warning\" title=\"Hidden field\">Hidden</span>\n }\n @if (column.autoGenerated) {\n <span class=\"badge bg-info\" title=\"Auto-generated field\">Auto</span>\n }\n @if (column.ontologyType) {\n <span class=\"badge bg-success\" title=\"Ontology: {{ column.ontologyType }}\">\n {{ column.ontologyType }}\n </span>\n }\n </div>\n </td>\n <td>\n <div class=\"btn-group btn-group-sm\">\n <button type=\"button\" class=\"btn btn-outline-primary\"\n (click)=\"viewColumnDetails(column)\"\n title=\"View column details\">\n <i class=\"bi bi-eye\"></i>\n </button>\n @if (table()!.canEdit) {\n <button type=\"button\" \n class=\"btn\"\n [class]=\"column.hidden ? 'btn-outline-warning' : 'btn-outline-success'\"\n (click)=\"toggleColumnHidden(column)\"\n [title]=\"column.hidden ? 'Show column' : 'Hide column'\">\n <i class=\"bi\" [class]=\"column.hidden ? 'bi-eye-slash' : 'bi-eye'\"></i>\n </button>\n <button type=\"button\" class=\"btn btn-outline-success\"\n (click)=\"editColumnValue(column)\"\n title=\"Edit default value\">\n <i class=\"bi bi-pencil-square\"></i>\n </button>\n <button type=\"button\" class=\"btn btn-outline-secondary\"\n (click)=\"editColumnSettings(column)\"\n title=\"Edit column settings\">\n <i class=\"bi bi-gear\"></i>\n </button>\n }\n </div>\n </td>\n </tr>\n }\n </tbody>\n </table>\n </div>\n }\n\n <!-- Table Data -->\n @if (viewMode() === 'table') {\n <div class=\"card-body p-0\">\n <div class=\"table-responsive\">\n <table class=\"table table-hover mb-0\">\n <thead class=\"table-light\">\n <tr>\n <th scope=\"col\" style=\"width: 80px;\" class=\"bg-secondary bg-opacity-10 text-secondary\">#</th>\n @for (column of sortedColumns(); track column.id) {\n <th scope=\"col\" [class]=\"getColumnHeaderClass(column)\" [title]=\"column.name\">\n <div class=\"header-cell d-flex justify-content-between align-items-center\">\n <span>{{ getShortColumnName(column) }}</span>\n @if (table()!.canEdit) {\n <div class=\"header-actions\">\n <button\n type=\"button\"\n class=\"btn btn-sm header-edit-btn me-1\"\n [class]=\"column.hidden ? 'btn-outline-warning' : 'btn-outline-success'\"\n (click)=\"toggleColumnHidden(column)\"\n [title]=\"column.hidden ? 'Show column' : 'Hide column'\">\n <i class=\"bi\" [class]=\"column.hidden ? 'bi-eye-slash' : 'bi-eye'\"></i>\n </button>\n <button\n type=\"button\"\n class=\"btn btn-sm btn-outline-primary header-edit-btn me-1\"\n (click)=\"editColumnValue(column)\"\n [title]=\"'Edit default value for ' + column.name\">\n <i class=\"bi bi-pencil\"></i>\n </button>\n <button\n type=\"button\"\n class=\"btn btn-sm btn-outline-danger header-edit-btn\"\n (click)=\"removeColumn(column)\"\n [title]=\"'Remove ' + column.name + ' column'\">\n <i class=\"bi bi-trash\"></i>\n </button>\n </div>\n }\n </div>\n </th>\n }\n </tr>\n </thead>\n <tbody>\n @for (row of paginatedRows(); track row._sampleIndex) {\n <tr class=\"sample-row\" [class.editable]=\"table()!.canEdit\">\n <td class=\"text-muted fw-semibold\">{{ row._sampleIndex }}</td>\n @for (column of sortedColumns(); track column.id) {\n <td\n class=\"sample-cell\"\n [class.opacity-50]=\"column.hidden\"\n [attr.data-sample-index]=\"row._sampleIndex\"\n [attr.data-column-id]=\"column.id\"\n (click)=\"table()!.canEdit ? editSampleColumnValue(column, row._sampleIndex) : null\"\n [title]=\"table()!.canEdit ? 'Click to edit ' + column.name + ' for sample ' + row._sampleIndex : ''\">\n {{ row['col_' + column.id] || '-' }}\n </td>\n }\n </tr>\n }\n </tbody>\n </table>\n </div>\n\n <!-- Table Mode Pagination -->\n @if (viewMode() === 'table' && totalPages() > 1) {\n <div class=\"d-flex justify-content-between align-items-center p-3 border-top\">\n <div class=\"d-flex align-items-center gap-2\">\n <span class=\"text-muted\">Rows per page:</span>\n <select class=\"form-select form-select-sm\" 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 </div>\n <ngb-pagination\n [collectionSize]=\"tableRows().length\"\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 <span class=\"text-muted\">\n Showing {{ (currentPage() - 1) * pageSize() + 1 }}-{{ Math.min(currentPage() * pageSize(), tableRows().length) }}\n of {{ tableRows().length }} samples\n </span>\n </div>\n }\n </div>\n }\n } @else {\n <div class=\"card-body text-center py-5\">\n <div class=\"text-muted\">\n <i class=\"bi bi-inbox display-1 mb-3\"></i>\n <h5>No Columns Found</h5>\n <p>This table doesn't have any columns defined yet.</p>\n @if (table()!.canEdit) {\n <button type=\"button\" class=\"btn btn-primary\" (click)=\"openAddColumnModal()\">\n <i class=\"bi bi-plus me-1\"></i>Add First Column\n </button>\n }\n </div>\n </div>\n }\n </div>\n\n </div>\n }\n</div>\n", styles: ["@charset \"UTF-8\";.header-cell{position:relative}.header-cell .header-actions{opacity:0;transition:opacity .15s ease}thead th:hover .header-actions{opacity:1}.header-edit-btn{--bs-btn-padding-y: .125rem;--bs-btn-padding-x: .25rem;--bs-btn-font-size: .65rem;border:none!important}.header-edit-btn.btn-outline-primary:hover{background-color:var(--bs-primary);border-color:var(--bs-primary)}.header-edit-btn.btn-outline-danger:hover{background-color:var(--bs-danger);border-color:var(--bs-danger)}.sample-row.editable .sample-cell{cursor:pointer;transition:background-color .1s ease}.sample-row.editable .sample-cell:hover{background-color:#28a74514;position:relative}.sample-row.editable .sample-cell:hover:after{content:\"\\270e\";position:absolute;right:4px;top:50%;transform:translateY(-50%);color:#28a74599;font-size:.75em;pointer-events:none}.pool-row.editable td.clickable{cursor:pointer;transition:background-color .1s ease}.pool-row.editable td.clickable:hover{background-color:#0dcaf01a;color:var(--bs-info)}.pool-row.editable td.clickable:hover .bi-pencil-square{opacity:.8}.pool-row.editable td.clickable .bi-pencil-square{opacity:.4;transition:opacity .1s ease}.pool-row.editable .pool-cell{cursor:pointer;transition:background-color .1s ease}.pool-row.editable .pool-cell:hover{background-color:#ffc10726;position:relative}.pool-row.editable .pool-cell:hover:after{content:\"\\270e\";position:absolute;right:4px;top:50%;transform:translateY(-50%);color:#ffc107cc;font-size:.75em;pointer-events:none}@media (max-width: 768px){.sample-row.editable .sample-cell,.pool-row.editable .pool-cell{transition:none}}.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}.drag-handle{cursor:grab;opacity:.6;transition:opacity .2s ease}.drag-handle:hover{opacity:1}.drag-handle:active{cursor:grabbing}.cdk-drag-preview{box-sizing:border-box;border-radius:4px;box-shadow:0 5px 5px -3px #0003,0 8px 10px 1px #00000024,0 3px 14px 2px #0000001f;background:var(--bs-light);border:1px solid var(--bs-border-color)}.cdk-drag-preview .header-cell{padding:.5rem;font-size:.875rem;font-weight:500}.cdk-drag-placeholder{opacity:.5;background:rgba(var(--bs-primary-rgb),.1)}.cdk-drag-animating{transition:transform .25s cubic-bezier(0,0,.2,1)}.cdk-drop-list-dragging th:not(.cdk-drag-placeholder){transition:transform .25s cubic-bezier(0,0,.2,1)}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: RouterModule }, { kind: "directive", type: i1$1.RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "routerLink"] }, { 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: "component", type: i2.NgbPagination, selector: "ngb-pagination", inputs: ["disabled", "boundaryLinks", "directionLinks", "ellipses", "rotate", "collectionSize", "maxSize", "page", "pageSize", "size"], outputs: ["pageChange"] }, { kind: "ngmodule", type: DragDropModule }, { kind: "directive", type: i5$1.CdkDropList, selector: "[cdkDropList], cdk-drop-list", inputs: ["cdkDropListConnectedTo", "cdkDropListData", "cdkDropListOrientation", "id", "cdkDropListLockAxis", "cdkDropListDisabled", "cdkDropListSortingDisabled", "cdkDropListEnterPredicate", "cdkDropListSortPredicate", "cdkDropListAutoScrollDisabled", "cdkDropListAutoScrollStep", "cdkDropListElementContainer", "cdkDropListHasAnchor"], outputs: ["cdkDropListDropped", "cdkDropListEntered", "cdkDropListExited", "cdkDropListSorted"], exportAs: ["cdkDropList"] }, { kind: "directive", type: i5$1.CdkDrag, selector: "[cdkDrag]", inputs: ["cdkDragData", "cdkDragLockAxis", "cdkDragRootElement", "cdkDragBoundary", "cdkDragStartDelay", "cdkDragFreeDragPosition", "cdkDragDisabled", "cdkDragConstrainPosition", "cdkDragPreviewClass", "cdkDragPreviewContainer", "cdkDragScale"], outputs: ["cdkDragStarted", "cdkDragReleased", "cdkDragEnded", "cdkDragEntered", "cdkDragExited", "cdkDragDropped", "cdkDragMoved"], exportAs: ["cdkDrag"] }, { kind: "directive", type: i5$1.CdkDragHandle, selector: "[cdkDragHandle]", inputs: ["cdkDragHandleDisabled"] }, { kind: "pipe", type: i5.DatePipe, name: "date" }] });
9127
+ }
9128
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.7", ngImport: i0, type: MetadataTableDetails, decorators: [{
9129
+ type: Component,
9130
+ args: [{ selector: 'ccv-metadata-table-details', standalone: true, imports: [CommonModule, RouterModule, NgbModule, DragDropModule], template: "<div class=\"metadata-table-details-container\">\n <!-- Header Section with Breadcrumbs -->\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 <!-- Breadcrumb Navigation -->\n <nav aria-label=\"breadcrumb\" class=\"me-3\">\n <ol class=\"breadcrumb mb-0\">\n <li class=\"breadcrumb-item\">\n <a routerLink=\"/metadata-tables\" class=\"text-decoration-none\">\n <i class=\"bi bi-table me-1\"></i>Metadata Tables\n </a>\n </li>\n <li class=\"breadcrumb-item active\" aria-current=\"page\">\n @if (table()) {\n {{ table()!.name }}\n } @else {\n Table Details\n }\n </li>\n </ol>\n </nav>\n\n <!-- Table Status Badge -->\n @if (table()) {\n <span class=\"badge ms-2\" [class]=\"getTableStatusClass()\">\n {{ getTableStatusBadge() }}\n </span>\n }\n </div>\n\n <!-- Action Buttons -->\n <div class=\"d-flex gap-2\">\n <button\n type=\"button\"\n class=\"btn btn-outline-secondary btn-sm\"\n (click)=\"navigateBack()\"\n title=\"Back to Tables\">\n <i class=\"bi bi-arrow-left me-1\"></i>Back\n </button>\n\n <!-- View Mode Toggle -->\n @if (hasColumns()) {\n <div class=\"btn-group\" role=\"group\">\n <button\n type=\"button\"\n class=\"btn btn-sm\"\n [class]=\"viewMode() === 'list' ? 'btn-primary' : 'btn-outline-primary'\"\n (click)=\"switchToListMode()\"\n title=\"Column List View\">\n <i class=\"bi bi-list-ul me-1\"></i>List\n </button>\n <button\n type=\"button\"\n class=\"btn btn-sm\"\n [class]=\"viewMode() === 'table' ? 'btn-primary' : 'btn-outline-primary'\"\n (click)=\"switchToTableMode()\"\n title=\"Table Data View\">\n <i class=\"bi bi-table me-1\"></i>Table\n </button>\n </div>\n }\n <!-- Import Dropdown -->\n @if (table() && table()!.canEdit) {\n <div class=\"btn-group\" ngbDropdown>\n <!-- Hidden file inputs -->\n <input\n #fileInput\n type=\"file\"\n accept=\".txt,.tsv\"\n style=\"display: none;\"\n (change)=\"importSdrf($event)\">\n <input\n #excelInput\n type=\"file\"\n accept=\".xlsx,.xls\"\n style=\"display: none;\"\n (change)=\"importExcel($event)\">\n \n <button\n type=\"button\"\n class=\"btn btn-outline-success btn-sm dropdown-toggle\"\n ngbDropdownToggle\n [disabled]=\"isLoading()\"\n title=\"Import Data\">\n <i class=\"bi bi-upload me-1\"></i>Import\n </button>\n <div class=\"dropdown-menu\" ngbDropdownMenu>\n <a class=\"dropdown-item\" href=\"#\" (click)=\"triggerFileInput(); $event.preventDefault()\">\n <i class=\"bi bi-file-earmark-text me-2\"></i>Import SDRF File\n </a>\n <a class=\"dropdown-item\" href=\"#\" (click)=\"triggerExcelInput(); $event.preventDefault()\">\n <i class=\"bi bi-file-earmark-spreadsheet me-2\"></i>Import Excel Template\n </a>\n </div>\n </div>\n }\n\n <!-- Export Dropdown -->\n @if (table() && hasColumns()) {\n <div class=\"btn-group\" ngbDropdown>\n <button\n type=\"button\"\n class=\"btn btn-outline-info btn-sm dropdown-toggle\"\n ngbDropdownToggle\n [disabled]=\"isLoading()\"\n title=\"Export Table\">\n <i class=\"bi bi-download me-1\"></i>Export\n </button>\n <div class=\"dropdown-menu dropdown-menu-end\" ngbDropdownMenu>\n <a class=\"dropdown-item\" href=\"#\" (click)=\"exportToSdrf('sdrf'); $event.preventDefault()\">\n <i class=\"bi bi-file-earmark-text me-2\"></i>Export as SDRF\n </a>\n <a class=\"dropdown-item\" href=\"#\" (click)=\"exportToSdrf('excel'); $event.preventDefault()\">\n <i class=\"bi bi-file-earmark-spreadsheet me-2\"></i>Export as Excel\n </a>\n </div>\n </div>\n }\n\n <!-- Edit Button -->\n @if (table() && table()!.canEdit) {\n <button\n type=\"button\"\n class=\"btn btn-outline-primary btn-sm\"\n (click)=\"editTable()\"\n [disabled]=\"isLoading()\"\n title=\"Edit Table\">\n <i class=\"bi bi-pencil me-1\"></i>Edit\n </button>\n }\n\n <!-- Reorder Columns Button -->\n @if (table() && table()!.canEdit && hasColumns()) {\n <button\n type=\"button\"\n class=\"btn btn-outline-warning btn-sm\"\n (click)=\"reorderColumns()\"\n [disabled]=\"isLoading()\"\n title=\"Reorder Columns by Schema\">\n <i class=\"bi bi-arrows-move me-1\"></i>Reorder\n </button>\n }\n\n <!-- Create Pool Button -->\n @if (table() && table()!.canEdit && hasColumns()) {\n <button\n type=\"button\"\n class=\"btn btn-success btn-sm\"\n (click)=\"createPool()\"\n [disabled]=\"isLoading()\"\n title=\"Create Sample Pool\">\n <i class=\"bi bi-plus-circle me-1\"></i>Create Pool\n </button>\n }\n\n <!-- More Actions Dropdown -->\n @if (table() && table()!.canEdit) {\n <div class=\"btn-group\" ngbDropdown>\n <button\n type=\"button\"\n class=\"btn btn-outline-danger btn-sm dropdown-toggle\"\n ngbDropdownToggle\n [disabled]=\"isLoading()\">\n <i class=\"bi bi-three-dots-vertical\"></i>\n </button>\n <div class=\"dropdown-menu dropdown-menu-end\" ngbDropdownMenu>\n <a class=\"dropdown-item text-danger\" href=\"#\" (click)=\"deleteTable(); $event.preventDefault()\">\n <i class=\"bi bi-trash me-2\"></i>Delete Table\n </a>\n </div>\n </div>\n }\n </div>\n </div>\n </div>\n </nav>\n\n <!-- Loading Spinner -->\n @if (isLoading()) {\n <div class=\"d-flex justify-content-center align-items-center\" style=\"height: 400px;\">\n <div class=\"spinner-border text-primary\" role=\"status\">\n <span class=\"visually-hidden\">Loading...</span>\n </div>\n </div>\n }\n\n <!-- Table Details Content -->\n @if (table() && !isLoading()) {\n <div class=\"table-details-content p-4\">\n\n <!-- Table Information Card -->\n <div class=\"card mb-4\">\n <div class=\"card-header\">\n <div class=\"d-flex align-items-center justify-content-between\">\n <h5 class=\"mb-0\">\n <i class=\"bi bi-info-circle me-2\"></i>Table Information\n </h5>\n <small class=\"text-muted\">\n Created {{ table()!.createdAt | date:'medium' }}\n </small>\n </div>\n </div>\n <div class=\"card-body\">\n <div class=\"row\">\n <div class=\"col-md-8\">\n <h3 class=\"fw-bold mb-3\">{{ table()!.name }}</h3>\n @if (table()!.description) {\n <p class=\"text-muted mb-3\">{{ table()!.description }}</p>\n }\n <div class=\"row\">\n <div class=\"col-md-6\">\n <div class=\"d-flex align-items-center mb-2\">\n <i class=\"bi bi-hash text-muted me-2\"></i>\n <span class=\"fw-semibold me-2\">Sample Count:</span>\n <span class=\"badge bg-primary\">{{ table()!.sampleCount }}</span>\n </div>\n <div class=\"d-flex align-items-center mb-2\">\n <i class=\"bi bi-columns-gap text-muted me-2\"></i>\n <span class=\"fw-semibold me-2\">Columns:</span>\n <span class=\"badge bg-info\">{{ table()!.columns?.length || 0 }}</span>\n </div>\n </div>\n <div class=\"col-md-6\">\n @if (table()!.ownerUsername) {\n <div class=\"d-flex align-items-center mb-2\">\n <i class=\"bi bi-person text-muted me-2\"></i>\n <span class=\"fw-semibold me-2\">Owner:</span>\n <span>{{ table()!.ownerUsername }}</span>\n </div>\n }\n @if (table()!.labGroupName) {\n <div class=\"d-flex align-items-center mb-2\">\n <i class=\"bi bi-people text-muted me-2\"></i>\n <span class=\"fw-semibold me-2\">Lab Group:</span>\n <span>{{ table()!.labGroupName }}</span>\n </div>\n }\n <div class=\"d-flex align-items-center mb-2\">\n <i class=\"bi bi-tag text-muted me-2\"></i>\n <span class=\"fw-semibold me-2\">Version:</span>\n <span class=\"badge bg-light text-dark\">{{ table()!.version || 'v1.0' }}</span>\n </div>\n </div>\n </div>\n </div>\n <div class=\"col-md-4 text-end\">\n <div class=\"d-flex flex-column gap-2\">\n @if (table()!.isLocked) {\n <span class=\"badge bg-warning\">\n <i class=\"bi bi-lock me-1\"></i>Locked\n </span>\n }\n @if (table()!.isPublished) {\n <span class=\"badge bg-success\">\n <i class=\"bi bi-cloud-check me-1\"></i>Published\n </span>\n }\n </div>\n </div>\n </div>\n </div>\n </div>\n\n <!-- Sample Pool Table -->\n @if (viewMode() === 'table' && hasPools()) {\n <div class=\"card mb-4\">\n <div class=\"card-header\">\n <h5 class=\"mb-0\">\n <i class=\"bi bi-layers me-2\"></i>Sample Pool Table\n <span class=\"badge bg-warning ms-2\">{{ sortedPools().length }} pools</span>\n </h5>\n </div>\n <div class=\"card-body p-0\">\n <div class=\"table-responsive\">\n <table class=\"table table-hover mb-0\">\n <thead class=\"table-light\">\n <tr>\n <th scope=\"col\" style=\"width: 80px;\" class=\"bg-secondary bg-opacity-10 text-secondary\">Pool</th>\n @for (column of sortedColumns(); track column.id) {\n <th scope=\"col\" [class]=\"getColumnHeaderClass(column)\" [title]=\"column.name\">\n <div class=\"header-cell d-flex justify-content-between align-items-center\">\n <span>{{ getShortColumnName(column) }}</span>\n @if (table()!.canEdit) {\n <div class=\"header-actions\">\n <button\n type=\"button\"\n class=\"btn btn-sm btn-outline-primary header-edit-btn me-1\"\n (click)=\"editColumnValue(column)\"\n [title]=\"'Edit default value for ' + column.name\">\n <i class=\"bi bi-pencil\"></i>\n </button>\n </div>\n }\n </div>\n </th>\n }\n </tr>\n </thead>\n <tbody>\n @for (poolRow of poolTableRows(); track poolRow._poolId) {\n <tr class=\"pool-row\" [class.editable]=\"table()!.canEdit\">\n <td class=\"text-muted fw-semibold\" \n [class.clickable]=\"table()!.canEdit\"\n (click)=\"table()!.canEdit ? editPool(poolRow._poolData) : null\"\n [title]=\"table()!.canEdit ? 'Click to edit pool: ' + poolRow._poolName : poolRow._poolName\">\n <i class=\"bi bi-layers me-2\"></i>{{ poolRow._poolName }}\n @if (table()!.canEdit) {\n <i class=\"bi bi-pencil-square ms-1 text-primary\" style=\"font-size: 0.8em;\"></i>\n }\n </td>\n @for (column of sortedColumns(); track column.id) {\n <td\n class=\"pool-cell\"\n [class.opacity-50]=\"column.hidden\"\n [attr.data-pool-id]=\"poolRow._poolId\"\n [attr.data-column-id]=\"column.id\"\n (click)=\"table()!.canEdit ? editPoolColumnValue(poolRow._poolData, column) : null\"\n [title]=\"table()!.canEdit ? 'Click to edit ' + column.name + ' for pool ' + poolRow._poolName : ''\">\n {{ poolRow['col_' + column.id] || '-' }}\n </td>\n }\n </tr>\n }\n </tbody>\n </table>\n </div>\n </div>\n </div>\n }\n\n <!-- Columns/Table Section -->\n <div class=\"card\">\n <div class=\"card-header\">\n <div class=\"d-flex align-items-center justify-content-between\">\n <h5 class=\"mb-0\">\n @if (viewMode() === 'list') {\n <i class=\"bi bi-list-columns me-2\"></i>Table Columns\n } @else {\n <i class=\"bi bi-table me-2\"></i>Table Data\n }\n </h5>\n \n <!-- Column Filter -->\n @if (hasColumns()) {\n <div class=\"d-flex align-items-center me-3\">\n <div class=\"input-group input-group-sm\" style=\"width: 250px;\">\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 columns...\"\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 ms-2\">\n {{ filteredColumnCount }}/{{ totalColumnCount }}\n </small>\n }\n </div>\n }\n \n <div class=\"d-flex gap-2 align-items-center\">\n @if (viewMode() === 'list') {\n <span class=\"badge bg-primary\">{{ sortedColumns().length }} columns</span>\n @if (table() && table()!.canEdit) {\n <button\n type=\"button\"\n class=\"btn btn-success btn-sm\"\n (click)=\"openAddColumnModal()\"\n [disabled]=\"isLoading()\"\n title=\"Add new column with automatic reordering\">\n <i class=\"bi bi-plus-circle me-1\"></i>Add Column\n </button>\n }\n @if (table() && table()!.canEdit && sortedColumns().length === 0) {\n <small class=\"text-muted\">Import SDRF to add columns</small>\n }\n } @else {\n <span class=\"badge bg-info\">{{ tableRows().length }} samples</span>\n @if (hasPools()) {\n <span class=\"badge bg-warning\">{{ sortedPools().length }} pools</span>\n }\n <span class=\"badge bg-primary\">{{ sortedColumns().length }} columns</span>\n @if (table() && table()!.canEdit) {\n <button\n type=\"button\"\n class=\"btn btn-success btn-sm\"\n (click)=\"openAddColumnModal()\"\n [disabled]=\"isLoading()\"\n title=\"Add new column with automatic reordering\">\n <i class=\"bi bi-plus-circle me-1\"></i>Add Column\n </button>\n }\n }\n </div>\n </div>\n </div>\n\n @if (hasColumns()) {\n <!-- List Mode: Column Definitions -->\n @if (viewMode() === 'list') {\n <div class=\"table-responsive\">\n <table class=\"table table-hover mb-0\">\n <thead>\n <tr>\n <th scope=\"col\" style=\"width: 40px;\">\n @if (table()!.canEdit) {\n <i class=\"bi bi-arrows-move text-muted\" title=\"Drag to reorder\"></i>\n }\n </th>\n <th scope=\"col\" style=\"width: 60px;\" \n class=\"sortable\" \n (click)=\"sortBy('position')\"\n [class.sorted]=\"isSorted('position')\">\n <div class=\"d-flex align-items-center justify-content-between\">\n <span>#</span>\n <i class=\"bi ms-1\" [class]=\"getSortIcon('position')\"></i>\n </div>\n </th>\n <th scope=\"col\" \n class=\"sortable\" \n (click)=\"sortBy('name')\"\n [class.sorted]=\"isSorted('name')\">\n <div class=\"d-flex align-items-center justify-content-between\">\n <span>Column Name</span>\n <i class=\"bi ms-1\" [class]=\"getSortIcon('name')\"></i>\n </div>\n </th>\n <th scope=\"col\" \n class=\"sortable\" \n (click)=\"sortBy('type')\"\n [class.sorted]=\"isSorted('type')\">\n <div class=\"d-flex align-items-center justify-content-between\">\n <span>Type</span>\n <i class=\"bi ms-1\" [class]=\"getSortIcon('type')\"></i>\n </div>\n </th>\n <th scope=\"col\" \n class=\"sortable\" \n (click)=\"sortBy('value')\"\n [class.sorted]=\"isSorted('value')\">\n <div class=\"d-flex align-items-center justify-content-between\">\n <span>Value</span>\n <i class=\"bi ms-1\" [class]=\"getSortIcon('value')\"></i>\n </div>\n </th>\n <th scope=\"col\">Properties</th>\n <th scope=\"col\" style=\"width: 100px;\">Actions</th>\n </tr>\n </thead>\n <tbody cdkDropList (cdkDropListDropped)=\"onColumnDrop($event)\" [cdkDropListData]=\"dragColumnsForList()\">\n @for (column of dragColumnsForList(); track column.id) {\n <tr cdkDrag [cdkDragData]=\"column\" [cdkDragDisabled]=\"!table()!.canEdit\">\n <td class=\"text-center\">\n @if (table()!.canEdit) {\n <i class=\"bi bi-grip-vertical drag-handle\" cdkDragHandle></i>\n }\n </td>\n <td class=\"text-muted fw-semibold\">\n {{ (column.columnPosition || 0) + 1 }}\n </td>\n <td>\n <div class=\"d-flex align-items-center\">\n <i class=\"me-2\" [class]=\"getColumnTypeIcon(column) + ' ' + getColumnTypeClass(column)\"></i>\n <span class=\"fw-semibold\">{{ column.name }}</span>\n </div>\n </td>\n <td>\n <span class=\"badge bg-light text-dark\">{{ column.type }}</span>\n </td>\n <td>\n <div>\n <!-- Default Value -->\n <div class=\"fw-semibold\">{{ getColumnValueDisplay(column) }}</div>\n\n <!-- Sample-specific Modifiers -->\n @if (hasModifiers(column)) {\n <div class=\"mt-1\">\n @for (modifierDisplay of getModifiersDisplay(column); track $index) {\n <small class=\"badge bg-light text-dark me-1 mb-1 d-inline-block\"\n style=\"font-size: 0.7rem;\"\n [title]=\"modifierDisplay\">\n {{ modifierDisplay }}\n </small>\n }\n </div>\n }\n </div>\n </td>\n <td>\n <div class=\"d-flex gap-1 flex-wrap\">\n @if (column.mandatory) {\n <span class=\"badge bg-danger\" title=\"Required field\">Required</span>\n }\n @if (column.readonly) {\n <span class=\"badge bg-secondary\" title=\"Read-only field\">Read-only</span>\n }\n @if (column.hidden) {\n <span class=\"badge bg-warning\" title=\"Hidden field\">Hidden</span>\n }\n @if (column.autoGenerated) {\n <span class=\"badge bg-info\" title=\"Auto-generated field\">Auto</span>\n }\n @if (column.ontologyType) {\n <span class=\"badge bg-success\" title=\"Ontology: {{ column.ontologyType }}\">\n {{ column.ontologyType }}\n </span>\n }\n </div>\n </td>\n <td>\n <div class=\"btn-group btn-group-sm\">\n <button type=\"button\" class=\"btn btn-outline-primary\"\n (click)=\"viewColumnDetails(column)\"\n title=\"View column details\">\n <i class=\"bi bi-eye\"></i>\n </button>\n @if (table()!.canEdit) {\n <button type=\"button\" \n class=\"btn\"\n [class]=\"column.hidden ? 'btn-outline-warning' : 'btn-outline-success'\"\n (click)=\"toggleColumnHidden(column)\"\n [title]=\"column.hidden ? 'Show column' : 'Hide column'\">\n <i class=\"bi\" [class]=\"column.hidden ? 'bi-eye-slash' : 'bi-eye'\"></i>\n </button>\n <button type=\"button\" class=\"btn btn-outline-success\"\n (click)=\"editColumnValue(column)\"\n title=\"Edit default value\">\n <i class=\"bi bi-pencil-square\"></i>\n </button>\n <button type=\"button\" class=\"btn btn-outline-secondary\"\n (click)=\"editColumnSettings(column)\"\n title=\"Edit column settings\">\n <i class=\"bi bi-gear\"></i>\n </button>\n }\n </div>\n </td>\n </tr>\n }\n </tbody>\n </table>\n </div>\n }\n\n <!-- Table Data -->\n @if (viewMode() === 'table') {\n <div class=\"card-body p-0\">\n <div class=\"table-responsive\">\n <table class=\"table table-hover mb-0\">\n <thead class=\"table-light\">\n <tr>\n <th scope=\"col\" style=\"width: 80px;\" class=\"bg-secondary bg-opacity-10 text-secondary\">#</th>\n @for (column of sortedColumns(); track column.id) {\n <th scope=\"col\" [class]=\"getColumnHeaderClass(column)\" [title]=\"column.name\">\n <div class=\"header-cell d-flex justify-content-between align-items-center\">\n <span>{{ getShortColumnName(column) }}</span>\n @if (table()!.canEdit) {\n <div class=\"header-actions\">\n <button\n type=\"button\"\n class=\"btn btn-sm header-edit-btn me-1\"\n [class]=\"column.hidden ? 'btn-outline-warning' : 'btn-outline-success'\"\n (click)=\"toggleColumnHidden(column)\"\n [title]=\"column.hidden ? 'Show column' : 'Hide column'\">\n <i class=\"bi\" [class]=\"column.hidden ? 'bi-eye-slash' : 'bi-eye'\"></i>\n </button>\n <button\n type=\"button\"\n class=\"btn btn-sm btn-outline-primary header-edit-btn me-1\"\n (click)=\"editColumnValue(column)\"\n [title]=\"'Edit default value for ' + column.name\">\n <i class=\"bi bi-pencil\"></i>\n </button>\n <button\n type=\"button\"\n class=\"btn btn-sm btn-outline-danger header-edit-btn\"\n (click)=\"removeColumn(column)\"\n [title]=\"'Remove ' + column.name + ' column'\">\n <i class=\"bi bi-trash\"></i>\n </button>\n </div>\n }\n </div>\n </th>\n }\n </tr>\n </thead>\n <tbody>\n @for (row of paginatedRows(); track row._sampleIndex) {\n <tr class=\"sample-row\" [class.editable]=\"table()!.canEdit\">\n <td class=\"text-muted fw-semibold\">{{ row._sampleIndex }}</td>\n @for (column of sortedColumns(); track column.id) {\n <td\n class=\"sample-cell\"\n [class.opacity-50]=\"column.hidden\"\n [attr.data-sample-index]=\"row._sampleIndex\"\n [attr.data-column-id]=\"column.id\"\n (click)=\"table()!.canEdit ? editSampleColumnValue(column, row._sampleIndex) : null\"\n [title]=\"table()!.canEdit ? 'Click to edit ' + column.name + ' for sample ' + row._sampleIndex : ''\">\n {{ row['col_' + column.id] || '-' }}\n </td>\n }\n </tr>\n }\n </tbody>\n </table>\n </div>\n\n <!-- Table Mode Pagination -->\n @if (viewMode() === 'table' && totalPages() > 1) {\n <div class=\"d-flex justify-content-between align-items-center p-3 border-top\">\n <div class=\"d-flex align-items-center gap-2\">\n <span class=\"text-muted\">Rows per page:</span>\n <select class=\"form-select form-select-sm\" 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 </div>\n <ngb-pagination\n [collectionSize]=\"tableRows().length\"\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 <span class=\"text-muted\">\n Showing {{ (currentPage() - 1) * pageSize() + 1 }}-{{ Math.min(currentPage() * pageSize(), tableRows().length) }}\n of {{ tableRows().length }} samples\n </span>\n </div>\n }\n </div>\n }\n } @else {\n <div class=\"card-body text-center py-5\">\n <div class=\"text-muted\">\n <i class=\"bi bi-inbox display-1 mb-3\"></i>\n <h5>No Columns Found</h5>\n <p>This table doesn't have any columns defined yet.</p>\n @if (table()!.canEdit) {\n <button type=\"button\" class=\"btn btn-primary\" (click)=\"openAddColumnModal()\">\n <i class=\"bi bi-plus me-1\"></i>Add First Column\n </button>\n }\n </div>\n </div>\n }\n </div>\n\n </div>\n }\n</div>\n", styles: ["@charset \"UTF-8\";.header-cell{position:relative}.header-cell .header-actions{opacity:0;transition:opacity .15s ease}thead th:hover .header-actions{opacity:1}.header-edit-btn{--bs-btn-padding-y: .125rem;--bs-btn-padding-x: .25rem;--bs-btn-font-size: .65rem;border:none!important}.header-edit-btn.btn-outline-primary:hover{background-color:var(--bs-primary);border-color:var(--bs-primary)}.header-edit-btn.btn-outline-danger:hover{background-color:var(--bs-danger);border-color:var(--bs-danger)}.sample-row.editable .sample-cell{cursor:pointer;transition:background-color .1s ease}.sample-row.editable .sample-cell:hover{background-color:#28a74514;position:relative}.sample-row.editable .sample-cell:hover:after{content:\"\\270e\";position:absolute;right:4px;top:50%;transform:translateY(-50%);color:#28a74599;font-size:.75em;pointer-events:none}.pool-row.editable td.clickable{cursor:pointer;transition:background-color .1s ease}.pool-row.editable td.clickable:hover{background-color:#0dcaf01a;color:var(--bs-info)}.pool-row.editable td.clickable:hover .bi-pencil-square{opacity:.8}.pool-row.editable td.clickable .bi-pencil-square{opacity:.4;transition:opacity .1s ease}.pool-row.editable .pool-cell{cursor:pointer;transition:background-color .1s ease}.pool-row.editable .pool-cell:hover{background-color:#ffc10726;position:relative}.pool-row.editable .pool-cell:hover:after{content:\"\\270e\";position:absolute;right:4px;top:50%;transform:translateY(-50%);color:#ffc107cc;font-size:.75em;pointer-events:none}@media (max-width: 768px){.sample-row.editable .sample-cell,.pool-row.editable .pool-cell{transition:none}}.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}.drag-handle{cursor:grab;opacity:.6;transition:opacity .2s ease}.drag-handle:hover{opacity:1}.drag-handle:active{cursor:grabbing}.cdk-drag-preview{box-sizing:border-box;border-radius:4px;box-shadow:0 5px 5px -3px #0003,0 8px 10px 1px #00000024,0 3px 14px 2px #0000001f;background:var(--bs-light);border:1px solid var(--bs-border-color)}.cdk-drag-preview .header-cell{padding:.5rem;font-size:.875rem;font-weight:500}.cdk-drag-placeholder{opacity:.5;background:rgba(var(--bs-primary-rgb),.1)}.cdk-drag-animating{transition:transform .25s cubic-bezier(0,0,.2,1)}.cdk-drop-list-dragging th:not(.cdk-drag-placeholder){transition:transform .25s cubic-bezier(0,0,.2,1)}\n"] }]
9131
+ }], ctorParameters: () => [{ type: i1$1.ActivatedRoute }, { type: i1$1.Router }, { type: MetadataTableService }, { type: AsyncExportService }, { type: ChunkedUploadService }, { type: SamplePoolService }, { type: MetadataColumnService }, { type: i3.ToastService }, { type: AsyncTaskUIService }, { type: i2.NgbModal }], propDecorators: { fileInput: [{
9132
+ type: ViewChild,
9133
+ args: ['fileInput']
9134
+ }], excelInput: [{
9135
+ type: ViewChild,
9136
+ args: ['excelInput']
9137
+ }] } });
9138
+
7408
9139
  class CupcakeVanilla {
7409
9140
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.7", ngImport: i0, type: CupcakeVanilla, deps: [], target: i0.ɵɵFactoryTarget.Component });
7410
9141
  static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.7", type: CupcakeVanilla, isStandalone: true, selector: "ccv-cupcake-vanilla", ngImport: i0, template: `
@@ -7430,5 +9161,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.7", ngImpor
7430
9161
  * Generated bundle index. Do not edit.
7431
9162
  */
7432
9163
 
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 };
9164
+ 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, MetadataTableDetails, MetadataTableEditModal, MetadataTableService, MetadataTableTemplateEditModal, MetadataTableTemplateService, MetadataTableTemplates, MetadataValidationModal, MetadataValueEditModal, NotificationService, NotificationType, OFFICIAL_SDRF_COLUMNS, ONTOLOGY_TYPE_CONFIGS, OntologySearchService, OntologyType, OntologyTypeLabels, OntologyUtils, SDRFDefaultsService, SamplePoolCreateModal, SamplePoolDetailsModal, SamplePoolEditModal, SamplePoolService, SchemaSelectionModal, SchemaService, SdrfAgeInput, SdrfCleavageInput, SdrfColumnCategory, SdrfModificationInput, SdrfSpikedCompoundInput, SdrfSyntaxService, SearchReplaceModal, TASK_STATUS_COLORS, TASK_STATUS_LABELS, TASK_TYPE_LABELS, TableCreationModalComponent, TemplatePermissionLabels, TemplatePermissionLevel, TemplateSchemaSelectionModal, ValidationResultsModal, Websocket, getRequiredSdrfColumns, getSdrfColumnByName, getSdrfColumnsByCategory, isSdrfColumnRequired, isUnimodFullData };
7434
9165
  //# sourceMappingURL=noatgnu-cupcake-vanilla.mjs.map