@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.
- package/fesm2022/noatgnu-cupcake-vanilla.mjs +1243 -3
- package/fesm2022/noatgnu-cupcake-vanilla.mjs.map +1 -1
- package/index.d.ts +435 -240
- package/package.json +1 -1
|
@@ -3,16 +3,17 @@ import { Injectable, signal, computed, EventEmitter, inject, Output, Input, Comp
|
|
|
3
3
|
import * as i3 from '@noatgnu/cupcake-core';
|
|
4
4
|
import { BaseApiService, WebSocketService, NotificationService as NotificationService$1, LabGroupService, ToastService, ResourceVisibility } from '@noatgnu/cupcake-core';
|
|
5
5
|
import { switchMap, map, tap, takeUntil, filter as filter$1 } from 'rxjs/operators';
|
|
6
|
-
import { from, Observable, throwError, NEVER, debounceTime, distinctUntilChanged, filter, switchMap as switchMap$1, map as map$1, catchError, of, tap as tap$1, Subject, BehaviorSubject } from 'rxjs';
|
|
6
|
+
import { from, Observable, throwError, NEVER, debounceTime, distinctUntilChanged, filter, switchMap as switchMap$1, map as map$1, catchError, of, tap as tap$1, Subject, BehaviorSubject, firstValueFrom } from 'rxjs';
|
|
7
7
|
import jsSHA from 'jssha';
|
|
8
8
|
import * as i5 from '@angular/common';
|
|
9
9
|
import { CommonModule } from '@angular/common';
|
|
10
10
|
import * as i1 from '@angular/forms';
|
|
11
11
|
import { FormBuilder, Validators, ReactiveFormsModule, FormsModule } from '@angular/forms';
|
|
12
12
|
import * as i2 from '@ng-bootstrap/ng-bootstrap';
|
|
13
|
-
import { NgbTypeahead, NgbModule, NgbNavModule, NgbActiveModal, NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
|
13
|
+
import { NgbTypeahead, NgbModule, NgbNavModule, NgbActiveModal, NgbModal, NgbPaginationModule } from '@ng-bootstrap/ng-bootstrap';
|
|
14
14
|
import * as i5$1 from '@angular/cdk/drag-drop';
|
|
15
15
|
import { moveItemInArray, DragDropModule } from '@angular/cdk/drag-drop';
|
|
16
|
+
import { RouterModule } from '@angular/router';
|
|
16
17
|
|
|
17
18
|
var OntologyType;
|
|
18
19
|
(function (OntologyType) {
|
|
@@ -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
|