@noatgnu/cupcake-vanilla 1.2.9 → 1.2.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,16 +3,17 @@ import { Injectable, signal, computed, EventEmitter, inject, Output, Input, Comp
3
3
  import * as i3 from '@noatgnu/cupcake-core';
4
4
  import { BaseApiService, WebSocketService, NotificationService as NotificationService$1, LabGroupService, ToastService, ResourceVisibility } from '@noatgnu/cupcake-core';
5
5
  import { switchMap, map, tap, takeUntil, filter as filter$1 } from 'rxjs/operators';
6
- import { from, Observable, throwError, NEVER, debounceTime, distinctUntilChanged, filter, switchMap as switchMap$1, map as map$1, catchError, of, tap as tap$1, Subject, BehaviorSubject } from 'rxjs';
6
+ import { from, Observable, throwError, NEVER, debounceTime, distinctUntilChanged, filter, switchMap as switchMap$1, map as map$1, catchError, of, tap as tap$1, Subject, BehaviorSubject, firstValueFrom } from 'rxjs';
7
7
  import jsSHA from 'jssha';
8
8
  import * as i5 from '@angular/common';
9
9
  import { CommonModule } from '@angular/common';
10
10
  import * as i1 from '@angular/forms';
11
11
  import { FormBuilder, Validators, ReactiveFormsModule, FormsModule } from '@angular/forms';
12
12
  import * as i2 from '@ng-bootstrap/ng-bootstrap';
13
- import { NgbTypeahead, NgbModule, NgbNavModule, NgbActiveModal, NgbModal } from '@ng-bootstrap/ng-bootstrap';
13
+ import { NgbTypeahead, NgbModule, NgbNavModule, NgbActiveModal, NgbModal, NgbPaginationModule } from '@ng-bootstrap/ng-bootstrap';
14
14
  import * as i5$1 from '@angular/cdk/drag-drop';
15
15
  import { moveItemInArray, DragDropModule } from '@angular/cdk/drag-drop';
16
+ import { RouterModule } from '@angular/router';
16
17
 
17
18
  var OntologyType;
18
19
  (function (OntologyType) {
@@ -6165,6 +6166,1245 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.3", ngImpor
6165
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" }]
6166
6167
  }], ctorParameters: () => [{ type: i1.FormBuilder }, { type: MetadataTableTemplateService }, { type: i3.LabGroupService }, { type: i2.NgbModal }, { type: i3.ToastService }, { type: AsyncTaskUIService }] });
6167
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
+
6168
7408
  class CupcakeVanilla {
6169
7409
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.3", ngImport: i0, type: CupcakeVanilla, deps: [], target: i0.ɵɵFactoryTarget.Component });
6170
7410
  static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.3", type: CupcakeVanilla, isStandalone: true, selector: "ccv-cupcake-vanilla", ngImport: i0, template: `
@@ -6190,5 +7430,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.3", ngImpor
6190
7430
  * Generated bundle index. Do not edit.
6191
7431
  */
6192
7432
 
6193
- 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 };
6194
7434
  //# sourceMappingURL=noatgnu-cupcake-vanilla.mjs.map