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